diff --git a/.github/workflows/automated-testing.yml b/.github/workflows/automated-testing.yml deleted file mode 100644 index 5b553a3..0000000 --- a/.github/workflows/automated-testing.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Automated testing - -on: [push] - -jobs: - build-linux: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: 3.10.4 - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - name: Install dependencies - run: | - conda env update --file environment.yml --name base - pip install . - - name: Lint with flake8 - run: | - conda install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - conda install pytest - pytest diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b6e4761..0000000 --- a/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7545fa7 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# nafuma + diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/about.md b/docs/about.md deleted file mode 100644 index 755a797..0000000 --- a/docs/about.md +++ /dev/null @@ -1,9 +0,0 @@ -# About - -This package contains data processing, analysis and viewing tools written in Python for several different activities related to inorganic materials chemistry conducted in the NAFUMA-group at the University of Oslo. It is written with the intention of creating a reproducible workflow for documentation purposes, with a focus on interactivity in the data exploration process. - -As of now (08-04-22), the intention is to include tools for XRD-, XANES- and electrochemistry-analysis, however other modules might be added as well. - - - - diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 1be3af8..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,57 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'NAFUMA' -copyright = '2022, Rasmus Vester Thøgersen & Halvor Høen Hval' -author = 'Rasmus Vester Thøgersen & Halvor Høen Hval' - -# The full version, including alpha/beta/rc tags -release = '0.2' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['myst_parser'] -source_suffix = ['.rst', '.md'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -html_sidebars = {'**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html']} diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index c775e23..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. NAFUMA documentation master file, created by - sphinx-quickstart on Fri Apr 8 15:32:14 2022. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to NAFUMA's documentation! -================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - about - installation - modules/modules - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index 252dc5b..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,25 +0,0 @@ -# Installation - -This package is not available on any package repositories, but can be installed by cloning the repository from GitHub and installing via ```pip install``` from the root folder: - -``` -$ git clone git@github.com:rasmusthog/nafuma.git -$ cd nafuma -$ pip install . -``` -If you are planning on making changes to the code base, you might want to consider installing it in develop-mode in order for changes to take effect without reinstalling by including the ```-e``` flag: - -``` -pip install -e . -``` - -As of now (v0.2, 08-04-22), the installer will not install any dependencies. It is recommended that you use `conda` to create an environment from `environment.yml` in the root folder: - -``` -$ conda env create --name --file environment.yml -$ conda activate -``` - -(remember to also get rid of <> when substituting your environment name). - -This should get you up and running! diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 8084272..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/modules/electrochemistry.md b/docs/modules/electrochemistry.md deleted file mode 100644 index b962bd1..0000000 --- a/docs/modules/electrochemistry.md +++ /dev/null @@ -1,3 +0,0 @@ -# Electrochemistry - -This is a placeholder diff --git a/docs/modules/modules.rst b/docs/modules/modules.rst deleted file mode 100644 index 1f659da..0000000 --- a/docs/modules/modules.rst +++ /dev/null @@ -1,12 +0,0 @@ -Modules -================================== - -.. toctree:: - :maxdepth: 1 - :caption: Contents - - xrd.md - xanes.md - electrochemistry.md - - diff --git a/docs/modules/xanes.md b/docs/modules/xanes.md deleted file mode 100644 index c18eec6..0000000 --- a/docs/modules/xanes.md +++ /dev/null @@ -1 +0,0 @@ -# XANES diff --git a/docs/modules/xrd.md b/docs/modules/xrd.md deleted file mode 100644 index 58e8c3c..0000000 --- a/docs/modules/xrd.md +++ /dev/null @@ -1,130 +0,0 @@ -# XRD - -This module contains functions to view diffractogram data from several different sources. The Some features include: - -- Allows the user to plot the data in wavelength independent parameters (d, 1/d, q, q{math}`^2`, q{math}`^4`), or translated to CuK{math}`\alpha` or MoK{math}`\alpha` allowing comparison between diffractograms obtained with different wavelengths -- Plotting in interactive mode within Jupyter Notebook using the `ipywidgets`-package allowing real-time change of (certain) parameters -- Plotting reflection ticks and/or reflection indices from multiple simulated reflection tables (generated by VESTA) for comparison -- Plotting series of diffractograms in stacked mode (including ability to rotate the view for a 3D-view) or as a heatmap - - - -## 1 Compatible file formats - -The module is partially built as a wrapper around [pyFAI](https://github.com/silx-kit/pyFAI) (Fast Azimuthal Integrator) developed at the ESRF for integrating 2D diffractograms from the detectors they have. Given a suitable calibration file (`.poni`), the XRD-module will automatically integrate any file pyFAI can integrate. Upon running in interactive mode, the integration is only done once, but it is advised to perform integration of many diffractograms in a separate processing step and saving the results as `.xy`-files, as the integration will run again each time the function is called. - -In addition to this, it can also read the `.brml`-files produced by Bruker-instruments in the RECX-lab at the University of Oslo. - -## 2 Basic usage - -Plotting diffractograms is done by calling the `xrd.plot.plot_diffractogram()`-function, which takes two dictionaries as arguments: `data`, containing all data specific information and `options` which allows customisation of a range of different parameters. The `options`-argument is optional, and the function will contains a bunch of default values to make an as good plot as possible to begin with. - -**Example #1: Single diffractogram** - -```py -import nafuma.xrd as xrd - -data = { - 'path': 'path/to/data/diffractogram.brml' -} - -options = { - 'reflections_data': [ - {'path': 'reflections_phase_1.txt', 'min_alpha': 0.1, 'reflection_indices': 4, 'label': 'Phase 1', 'text_colour': 'black'}, - {'path': 'reflections_phase_2.txt', 'min_alpha': 0.1, 'reflections_indices': 4, 'label': 'Phase 2', 'text_colour': 'red'} - ], - 'hide_y_ticklabels': True, - 'hide_y_ticks': True -} - - -diff, fig, ax = xrd.plot.plot_diffractogram(data=data, options=options) -``` - -The return value `diff` is a list containing one `pandas.DataFrame` per diffractogram passed, in the above example only one. `fig` and `ax` are `matplotlib.pyplot.Figure`- and `matplotlib.pyplot.Axes`-objects, respectively. - -**Example #2: 2D diffractogram from ESRF requiring integration** - -```py -import nafuma.xrd as xrd - -data = { - 'path': 'path/to/data/2d_diffractogram.edf', - 'calibrant': 'path/to/calibrant/calibrant.poni', - 'nbins': 3000 -} - -diff, _ = xrd.plot.plot_diffractogram(data=data, options=options) -``` - -In this case we did not specify any options and will thus only use default values, and we stored both `fig` and `ax` in the variable `_` as we do not intend to use these. - -**Example #3: Plotting with interactive mode** - -This will can be done within a Jupyter Notebook, and will allow the user to tweak certain parameters real-time instead of having to recall the function every time. - -```py -import nafuma.xrd as xrd - -data = { - 'path': 'path/to/data/diffractogram.brml' -} - -options = { - 'interactive': True -} - - -diff, _ = xrd.plot.plot_diffractogram(data=data, options=options) -``` - -**Example #4: Plotting multiple diffractograms as stacked plots** - -Instead of passing just a string, you can pass a lsit of filenames. This will be plotted sequentially, with offsets, if desired (`offset_x` and `offset_y`). Default values of `offset_y` is 1 if less than 10 diffractograms have been passed, and 0.1 if more than 10 diffractograms are passed. When plotting series data (e.g. from *in situ* or *operando* measurements), a smaller offset is suitable. Keep in mind that these values only makes sense when the diffractograms are normalised (`'normalise': True`) - if not, the default offsets will be way too small to be noticeable. - -```py -import nafuma.xrd as xrd - -data = { - 'path': ['path/to/data/diffractogram_1.brml', 'path/to/data/diffractogram_2.brml'] -} - - -options = { - 'offset_y': 0.1, - 'offset_x': 0.05, -} - - -diff, _ = xrd.plot.plot_diffractogram(data=data, options=options) -``` - - -**Example #5: Plotting series data as heatmap** - -This differs very little from above, except that heatmaps are probably nonesense if not used on series data, and that you don't want offset in heatmaps. - -```py -import nafuma.xrd as xrd - -list_of_data = ['data_1.brml', 'data_2.brml'. ...., 'data_n.brml'] - -data = { - 'path': lists_of_data -} - - -options = { - 'heatmap': True -} - - -diff, _ = xrd.plot.plot_diffractogram(data=data, options=options) -``` - - - - - - - diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 04c88d5..0000000 --- a/environment.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: nafuma -channels: - - diffpy - - defaults - - conda-forge -dependencies: - - ipywidgets - - seaborn - - sympy - - matplotlib - - pytest - - numpy - - pandas - - palettable - - pyfai -prefix: C:\Users\rasmusvt\Anaconda3\envs\nafuma diff --git a/nafuma/__init__.py b/nafuma/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/nafuma/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/nafuma/auxillary.py b/nafuma/auxillary.py deleted file mode 100644 index 4931a79..0000000 --- a/nafuma/auxillary.py +++ /dev/null @@ -1,188 +0,0 @@ -import json -import numpy as np -import os -import shutil - -import time -from datetime import datetime - -def update_options(options, default_options, required_options=None): - ''' Takes a dictionary of options along with a list of required options and dictionary of default options, and sets all keyval-pairs of options that is not already defined to the default values''' - #FIXME This has been updated so that required_options is not needed. But lots of scripts still passes required_options, so for now it is still accepted, but has a default value and remains unused. Needs to go through all scripts to stop passing of this variable to remove it. - - - for option in default_options.keys(): - if option not in options.keys(): - options[option] = default_options[option] - - - return options - -def save_options(options, path, ignore=None): - ''' Saves any options dictionary to a JSON-file in the specified path''' - - options_copy = options.copy() - - if ignore: - if not isinstance(ignore, list): - ignore = [ignore] - - for i in ignore: - options_copy[i] = 'Removed' - - - if not os.path.isdir(os.path.dirname(path)): - if os.path.dirname(path): - os.makedirs(os.path.dirname(path)) - - - with open(path, 'w') as f: - json.dump(options_copy, f, skipkeys=True, indent=4) - - -def load_options(path): - ''' Loads JSON-file into a dictionary''' - - with open(path, 'r') as f: - options = json.load(f) - - return(options) - - - -def ceil(a, roundto=1): - - fac = 1/roundto - - a = np.ceil(a*fac) / fac - - return a - -def floor(a, roundto=1): - - fac = 1/roundto - - a = np.floor(a*fac) / fac - - return a - - - -def write_log(message, options={}): - - - required_options = ['logfile'] - default_options = { - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S.log")}' - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - if not os.path.isdir(os.path.dirname(options['logfile'])): - os.makedirs(os.path.dirname(options['logfile'])) - - - now = datetime.now().strftime('%Y/%m/%d %H:%M:%S') - message = f'[{now}] {message} \n' - - - with open(options['logfile'], 'a') as f: - f.write(message) - - -#Function that "collects" all the files in a folder, only accepting .dat-files from xanes-measurements -def get_filenames(path, ext, filter=''): - ''' Collects all filenames from specified path with a specificed extension - - Input: - path: path to find all filenames (relative or absolute) - ext: extension (including ".")''' - - filenames = [os.path.join(path, filename) for filename in os.listdir(path) if os.path.isfile(os.path.join(path, filename)) and filename.endswith(ext) and filter in filename] - - return filenames - -def move_list_element_last(filenames,string): - for i,file in enumerate(filenames): - if string in file: - del filenames[i] - filenames.append(file) - return filenames - - - -def backup_file(filename, backup_dir): - # Creates backup-folder if it does not exist - if not os.path.isdir(backup_dir): - os.makedirs(backup_dir) - - - # Get a list of all previous backup files with the same basename as well as the creation time for the - prev_backup_files = [file for file in os.listdir(backup_dir) if os.path.basename(filename.split('.')[0]) in file] - creation_time = datetime.strptime(time.ctime(os.path.getmtime(filename)), '%a %b %d %H:%M:%S %Y').strftime("%Y-%m-%d_%H-%M-%S") - ext = '.' + filename.split('.')[-1] - - dst_basename = creation_time + '_' + filename.split('.')[0] + '_' + f'{len(prev_backup_files)}'.zfill(4) + ext - dst = os.path.join(backup_dir, dst_basename) - - - shutil.copy(filename, dst) - - -def get_unique(full_list): - - unique_list = [] - - for entry in full_list: - if not entry in unique_list: - unique_list.append(entry) - - return unique_list - - -def swap_values(options: dict, key1, key2): - - if not isinstance(key1,list): - key1 = [key1] - if not isinstance(key2,list): - key2 = [key2] - - assert len(key1) == len(key2) - - for k1, k2 in zip(key1, key2): - options[k1], options[k2] = options[k2], options[k1] - - - return options - - - -def find_neighbours(value, df, colname, start=0, end=-1): - ''' Finds closest match to a given value in colname of df. If there is an exact match, returns index of this value. Else, it returns the nearest neighbors (upper and lower)''' - - df = df.iloc[start:end] - - exactmatch = df[df[colname] == value] - if not exactmatch.empty: - return exactmatch.index.values[0] - else: - lower_df = df[df[colname] < value][colname] - upper_df = df[df[colname] > value][colname] - - - if not lower_df.empty: - lowerneighbour_ind = lower_df.idxmax() - else: - lowerneighbour_ind = np.nan - - if not upper_df.empty: - upperneighbour_ind = upper_df.idxmin() - else: - upperneighbour_ind = np.nan - - return [lowerneighbour_ind, upperneighbour_ind] - - -def isnan(value): - - return value!=value diff --git a/nafuma/dft/__init__.py b/nafuma/dft/__init__.py deleted file mode 100644 index 7aa3743..0000000 --- a/nafuma/dft/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import electrons, io, structure, phonons \ No newline at end of file diff --git a/nafuma/dft/electrons.py b/nafuma/dft/electrons.py deleted file mode 100644 index d99bf71..0000000 --- a/nafuma/dft/electrons.py +++ /dev/null @@ -1,1228 +0,0 @@ -import re -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import os -import linecache -import nafuma.dft as dft -import nafuma.auxillary as aux -import nafuma.plotting as btp - -from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) -import importlib -import mpl_toolkits.axisartist as axisartist -from cycler import cycler -import itertools -import matplotlib.patches as mpatches - - - - -def count_electrons(pdos, orbital, interval=None, r=None, scale=None): - ''' Counts electrons the specified oribtals from a projected density of states DataFrame. Interval can be specified, as well as a scaling factor and whether the number should be rounded. - Inputs: - dos: either an individual DOS (as read from read_pdos()), or a list of DOSes. If a single DataFrame is passed, it will be appended to a list - orbital: list of which orbitals to count the electrons from - interval: a list specifying where to start counting from (lower limit) to where to stop counting (upper limit) in eV - r: Number of decimals points the number should be rounded to - scale: A scaling factor to scale the number of electrons to a desired size, e.g. if you have a set containing two atoms per unit cell and you want to know how many electrons per atom there is - - Output: - nelec: The total number of electrons given your choices - nelec_dos: A list where each element is the total number of electrons per DOS passed (e.g. you pass three PDOS from three individual atoms, then you will get total electron count per atom) - nelec_orbitals: A list of lists, where each list contains the number of electrons per orbital specified (e.g. you pass three PDOS from three individual atoms, you will get three lists each containing electron count per orbital specified) - ''' - - - - if not type(orbital) == list: - orbital = [orbital] - - if not type(pdos) == list: - pdos = [pdos] - - - nelec = 0 - nelec_per_dos = [] - nelec_per_orbital = [] - - for d in pdos: - - energy = d["Energy"] - - nelec_orbitals = [] - - for o in orbital: - orbital_dos = d[o] - dE = (energy.max()-energy.min()) / len(energy) - - - if not interval: - interval = [energy.min(), energy.max()] - - emin, emax = interval[0], interval[1] - - - nelec_orbital = 0 - for od, e in zip(orbital_dos, energy): - if e > emin and e < emax: - nelec_orbital += np.abs(od)*dE - #print(nelec_orbital) - - nelec += nelec_orbital - nelec_orbitals.append(nelec_orbital) - - - # Scale the values if specified - if scale: - - for ind, nelec_orbital in enumerate(nelec_orbitals): - nelec_orbitals[ind] = nelec_orbital * scale - - - # If rounding is specified, does so to the electron count per DOS and the electron count per orbital - if r: - # First sums the electron count per orbital, and then round this number - nelec_dos = np.round(sum(nelec_orbitals), r) - - # Then each individual orbital electron count - for ind, nelec_orbital in enumerate(nelec_orbitals): - nelec_orbitals[ind] = np.round(nelec_orbital, r) - - # If no rounding is specified, just adds the electron count per orbital together - else: - nelec_dos = sum(nelec_orbitals) - - # Appends the total electron count for this DOS to the list of all individual DOS electron count - nelec_per_dos.append(nelec_dos) - - # Appends the list of orbital electron counts to the list of all the individual DOS orbital electron count (phew...) - nelec_per_orbital.append(nelec_orbitals) - - - # The total electron count is then scaled in the end. At this point the other values will have been scaled already - if scale: - nelec = nelec * scale - - # And lastly round if this is specified. Again, the electron counts in the lists are already rounded so they don't have to be rounded again - if r: - nelec = np.round(nelec, r) - - return nelec, [nelec_per_dos, nelec_per_orbital] - - - -def integrate_coop(coopcar, interval=None, r=None, scale=None, interactions=None, kind='individual', up=True, down=True, collapse=False): - ''' As of now does not support not passing in interactions. Very much copy and paste from the plotting function - not every choice here might make sense for integration of COOP''' - - coopcar, coop_interactions = dft.io.read_coop(coopcar, collapse=collapse) - - # If interactions has been specified - if interactions: - - # Make interactions into a list of lists for correct looping below - if type(interactions[0]) != list: - interactions_list = [interactions] - else: - interactions_list = interactions - - for ind, interactions in enumerate(interactions_list): - - # Determine which columns to integrate if collapse is enabled - if collapse: - to_integrate = [2*(i-1)+3 for i in interactions] - - - # Make mean column for integration if mean mode is enabeld (is this sensible to include?) - if kind == 'avg' or kind == 'average' or kind == 'mean': - coopcar["mean"] = coopcar.iloc[:, to_integrate].mean(axis=1) - to_integrate = [coopcar.columns.get_loc('mean')] - - # Determine which columns to integrate if collapse is disabled and both up and down should be plotted - elif up and down: - to_integrate_up = [2*(i-1)+3 for i in interactions] - to_integrate_down = [2*(i-1)+5 +2*len(coop_interactions) for i in interactions] - to_integrate = to_integrate_up + to_integrate_down - - if kind == 'avg' or kind == 'average' or kind == 'mean': - coopcar["mean_up"] = coopcar.iloc[:, to_integrate_up].mean(axis=1) - coopcar["mean_down"] = coopcar.iloc[:, to_integrate_down].mean(axis=1) - to_integrate = [coopcar.columns.get_loc('mean_up'), coopcar.columns.get_loc('mean_down')] - - # Determine which columns to plot if collapse is disabled and only up should be plotted - elif up: - to_integrate = [2*(i-1)+3 for i in interactions] - - if kind == 'avg' or kind == 'average' or kind == 'mean': - coopcar["mean_up"] = coopcar.iloc[:, to_integrate].mean(axis=1) - to_integrate = [coopcar.columns.get_loc('mean_up')] - - - # Determine which columns to plot if collapse is disabled and only down should be plotted - elif down: - to_integrate = [2*(i-1)+5 +2*len(coop_interactions) for i in interactions] - - if kind == 'avg' or kind == 'average' or kind == 'mean': - coopcar["mean_down"] = coopcar.iloc[:, to_integrate].mean(axis=1) - to_integrate = [coopcar.columns.get_loc('mean_down')] - - - - bonding = 0 - antibonding = 0 - bonding_interactions = [] - antibonding_interactions = [] - difference_interactions = [] - percentage_bonding_interactions = [] - - - for integrate in to_integrate: - - bonding_interaction = 0 - antibonding_interaction = 0 - - coop = coopcar.iloc[:, integrate] - - energy = coopcar["Energy"] - dE = (energy.max()-energy.min()) / len(energy) - - # Sets interval to everything below the Fermi-level by default if not specified in function call - if not interval: - interval = [energy.min(), 0] - - emin, emax = interval[0], interval[1] - - - for c, e in zip(coop, energy): - if e > emin and e < emax: - if c > 0: - bonding_interaction += c*dE - elif c < 0: - antibonding_interaction += np.abs(c)*dE - - - bonding += bonding_interaction - antibonding += antibonding_interaction - - difference_interaction = bonding_interaction - antibonding_interaction - percentage_bonding_interaction = bonding_interaction / (bonding_interaction + antibonding_interaction) * 100 - - if scale: - bonding_interaction = bonding_interaction * scale - antibonding_interaction = antibonding_interaction * scale - difference_interaction = difference_interaction * scale - - if r: - bonding_interaction = np.round(bonding_interaction, r) - antibonding_interaction = np.round(antibonding_interaction, r) - difference_interaction = np.round(difference_interaction, r) - percentage_bonding_interaction = np.round(percentage_bonding_interaction, r) - - bonding_interactions.append(bonding_interaction) - antibonding_interactions.append(antibonding_interaction) - difference_interactions.append(difference_interaction) - percentage_bonding_interactions.append(percentage_bonding_interaction) - - difference = bonding - antibonding - percentage_bonding = (bonding/(bonding+antibonding)) * 100 - - if scale: - bonding = bonding * scale - antibonding = antibonding * scale - difference = difference * scale - - if r: - bonding = np.round(bonding, r) - antibonding = np.round(antibonding, r) - difference = np.round(difference, r) - percentage_bonding = np.round(percentage_bonding, r) - - return [bonding, antibonding, difference, percentage_bonding], [bonding_interactions, antibonding_interactions, difference_interactions, percentage_bonding_interactions] - - - - -def plot_pdos(data: dict, options={}): - - default_options = { - 'xlabel': 'Energy', 'xunit': 'eV', 'xlim': None, 'x_tick_locators': None, - 'ylabel': 'Partial density of states', 'yunit': 'arb.u.', 'ylim': None, 'y_tick_locators': None, - 'mark_fermi_level': True, # Adds a dashed line to mark the Fermi-level - 'flip_axes': False, # Flips x- and y-axes - 'plot_indices': [], # List which indices to plot. If options["sum_atoms"] == True, this needs to be a list of lists, each specifying the index of a given atom - 'plot_atoms': [], # List of which atoms to plot. Only used if options["sum_atoms"] == True. - 'plot_orbitals': [], # List of which orbitals to plot. If options["sum_atoms"] == True, this needs to be a list of lists, each specifying the orbitals of a given atom - 'atom_colours': [], # Colours of each atom. Should be a colour for each atom, only in use if options["sum_atoms"] == True. - 'orbital_colours': [], # Colours of each orbital. The list should always correspond to the shape of options["plot_orbitals"]. - 'fill': False, - 'fig': None, # Matplotlib Figure object - 'ax': None, # Matplotlib Axes object - } - - options = aux.update_options(options=options, default_options=default_options) - - if 'axes_flipped' not in options.keys(): - options['axes_flipped'] = False - - data = dft.io.read_pdos(data=data, options=options) - - - - if not options['fig'] and not options['ax']: - fig, ax = btp.prepare_plot(options=options) - else: - fig, ax = options['fig'], options['ax'] - - - # If options['sum_atoms'] == True - if isinstance(data['pdos'], dict): - - # Populate the plot_atoms and plot_orbitals lists if they are not passed. Defaults to showing everything - if not options['plot_atoms']: - options['plot_atoms'] = data['atoms']['specie'] - - if not options['plot_orbitals']: - for atom in options['plot_atoms']: - options['plot_orbitals'].append([]) - - # This is to fill in each orbital list for each atom. This is in case options['plot_orbitals'] is passes, but one or more of the atoms lack colours - for i, atom in enumerate(options['plot_atoms']): - if not options['plot_orbitals'] or not options['plot_orbitals'][i]: - options['plot_orbitals'][i] = [orbital for orbital in data['pdos'][atom].columns if 'Energy' not in orbital] - - # Populate the atom_colours and orbital_colours. Defaults to same colour for all orbitals of one specie. - if not options['atom_colours']: - options['palettes'] = [('qualitative', 'Dark2_8')] - colour_cycle = generate_colours(options=options) - - for atom in options['plot_atoms']: - options['atom_colours'].append(next(colour_cycle)) - - if not options['orbital_colours']: - for i, atom in enumerate(options['plot_orbitals']): - options['orbital_colours'].append([]) # Make list for specific atom - for orbital in atom: - options['orbital_colours'][i].append(options['atom_colours'][i]) - - - for i, atom in enumerate(options['plot_atoms']): - - if not options['plot_orbitals'] or not options['plot_orbitals'][i]: - options['plot_orbitals'][i] = [orbital for orbital in data['pdos'][atom].columns if 'Energy' not in orbital] - - x = 'Energy' - y = options['plot_orbitals'][i] - - if options['flip_axes']: - for j, orbital in enumerate(options['plot_orbitals'][i]): - - if options['fill']: - ax.fill_betweenx(y=data['pdos'][atom]['Energy'], x1=data['pdos'][atom][orbital], x2=0, color=options['orbital_colours'][i][j], ec=(0,0,0,1)) - else: - ax.plot(data['pdos'][atom][orbital], data['pdos'][atom]['Energy'], color=options['orbital_colours'][i][j]) - - - - else: - data['pdos'][atom].plot(x=x, y=y, color=options['orbital_colours'][i], ax=ax) - - #print(options['plot_orbitals'], options['orbital_colours']) - - - if options['flip_axes']: - - if not options['axes_flipped']: - options = aux.swap_values(options=options, - key1=['xlim', 'xunit', 'xlabel', 'x_tick_locators'], - key2=['ylim', 'yunit', 'ylabel', 'y_tick_locators'] - ) - - options['axes_flipped'] = True # - - ax.axvline(x=0, c='black') - - if options['mark_fermi_level']: - ax.axhline(y=0, c='black', ls='--') - - - else: - ax.axhline(y=0, c='black') - ax.axvline(x=0, c='black', ls='--') - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - - - - #elif isinstance(data['pdos'], list): - # if not options['plot_atoms']: - # options['plot_atoms'] = data['atoms']['specie'] - # - # if not options['plot_indices']: - # for plot_specie in options['plot_atoms']: - # for i, doscar_specie in enumerate(data['atoms']['specie']): - # if plot_specie == doscar_specie: - # options['plot_indices'].append([k for k in range(data['atoms']['number'][i])]) - - - return None - - - - - -def plot_partial_dos_legacy(data: dict, options={}): - - - required_options = ['atoms', 'orbitals', 'up', 'down', 'sum_atoms', 'collapse_spin', 'sum_orbitals', 'palettes', 'colours', 'fig', 'ax'] - - default_options = { - 'atoms': None, - 'orbitals': None, - 'up': True, - 'down': True, - 'sum_atoms': True, - 'collapse_spin': False, - 'sum_orbitals': False, - 'palettes': [('qualitative', 'Dark2_8')], - 'colours': None, - 'fig': None, - 'ax': None - - - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - if not options['ax'] and not options['fig']: - fig, ax = btp.prepare_plot(options) - else: - fig, ax = options['fig'], options['ax'] - - species, *_ = dft.io.get_atoms(data['poscar']) - - pdos, options['dos_info'] = dft.io.read_pdos(data=data, options=options) # Extract projected DOS from DOSCAR, decomposed on individual atoms and orbitals Should yield list of N DataFrames where N is number of atoms in POSCAR - - - if not options['orbitals']: - options['orbitals'] = ['s', 'p1', 'p2', 'p3', 'd1', 'd2', 'd3', 'd4', 'd5'] if not options['sum_orbitals'] else ['s', 'p', 'd'] - - if not options['colours']: - colour_cycle = generate_colours(options=options) -# - #colours = [] - #for orbital in options['orbitals']: - # colours.append(next(colour_cycle)) -# - # else: - # colours = options['colours'] - - elif not isinstance(options['colours'], list): - new_colours = [] - for atom in options['atoms']: - new_colours.append([options['colours']]) - - options['colours'] = new_colours - - - print(options['colours']) - - - if not isinstance(options['orbitals'][0], list): - new_orbitals = [] - for atom in options['atoms']: - new_orbitals.append([options['orbitals']]) - - options['orbitals'] = new_orbitals - - - if options['atoms']: - for i, atom in enumerate(options['atoms']): - - if options['sum_atoms']: - for ind, specie in enumerate(species): - if specie == atom: - atom_index = ind - else: - atom_index = atom-1 - - - for j, orbital in enumerate(options['orbitals'][i]): - - colour = options['colours'][i][j] - - if options['dos_info']['spin_polarised']: - if options['up']: - pdos[atom_index].plot(x='Energy', y=orbital+'_u', ax=ax, c=colour) - - if options['down']: - pdos[atom_index].plot(x='Energy', y=orbital+'_d', ax=ax, c=colour) - else: - pdos[atom_index].plot(x='Energy', y=orbital, ax=ax, c=colour) - - - - btp.adjust_plot(fig=fig, ax=ax, options=options) - - return [pdos, ax, fig] - - - -def get_pdos_indices(poscar, atoms): - - species, atom_num, atoms_dict = dft.io.get_atoms(poscar) - - - - -def get_pdos(doscar='DOSCAR', nedos=301, headerlines=6, spin=True, adjust=True, manual_adjust=None): - - lines = dft.io.open_doscar(doscar) - - number_of_atoms = dft.io.get_number_of_atoms(doscar) - - if adjust: - e_fermi = dft.io.get_fermi_level(doscar) if not manual_adjust else manual_adjust - else: - e_fermi = 0 - - pdos = [] - - columns_non_spin = ["Energy", "s", "p_y", "p_z", "p_x", "d_xy", "d_yz", "d_z2-r2", "d_xz", "d_x2-y2"] - columns_spin = ["Energy", "s_up", "s_down", "p_y_up", "p_y_down", "p_z_up", "p_z_down", "p_x_up", "p_x_down", "d_xy_up", "d_xy_down", "d_yz_up", "d_yz_down", - "d_z2-r2_up", "d_z2-r2_down", "d_xz_up", "d_xz_down", "d_x2-y2_up", "d_x2-y2_down"] - - up = ['s_up', "p_y_up", "p_z_up", "p_x_up", "d_xy_up", "d_yz_up", "d_z2-r2_up", "d_xz_up", "d_x2-y2_up"] - down = ['s_down', "p_y_down", "p_z_down", "p_x_down", "d_xy_down", "d_yz_down", "d_z2-r2_down", "d_xz_down", "d_x2-y2_down"] - total = ["s", "p_y", "p_z", "p_x", "d_xy", "d_yz", "d_z2-r2", "d_xz", "d_x2-y2"] - - for i in range(1,number_of_atoms+1): - atom_dos = [] - - for j in range(headerlines+(nedos*i)+i,nedos+headerlines+(nedos*i)+i): - line = lines[j].strip() - values = line.split() - - for ind, value in enumerate(values): - values[ind] = float(value) - - values[0] = values[0] - e_fermi - atom_dos.append(values) - - - atom_df = pd.DataFrame(data=atom_dos, columns=columns_non_spin) if spin==False else pd.DataFrame(data=atom_dos, columns=columns_spin) - - if spin==True: - atom_df[["s_down"]] = -atom_df[["s_down"]] - atom_df[["p_y_down"]] = -atom_df[["p_y_down"]] - atom_df[["p_z_down"]] = -atom_df[["p_z_down"]] - atom_df[["p_x_down"]] = -atom_df[["p_x_down"]] - atom_df[["d_xy_down"]] = -atom_df[["d_xy_down"]] - atom_df[["d_yz_down"]] = -atom_df[["d_yz_down"]] - atom_df[["d_z2-r2_down"]] = -atom_df[["d_z2-r2_down"]] - atom_df[["d_xz_down"]] = -atom_df[["d_xz_down"]] - atom_df[["d_x2-y2_down"]] = -atom_df[["d_x2-y2_down"]] - - atom_df = atom_df.assign(total_up = atom_df[up].sum(axis=1)) - atom_df = atom_df.assign(total_down = atom_df[down].sum(axis=1)) - - elif spin==False: - atom_df = atom_df.assign(total = atom_df[total].sum(axis=1)) - - pdos.append(atom_df) - - return pdos - - - -def prepare_plot(options={}): - - rc_params = options['rc_params'] - format_params = options['format_params'] - - required_options = ['single_column_width', 'double_column_width', 'column_type', 'width_ratio', 'aspect_ratio', 'compress_width', 'compress_height', 'upscaling_factor', 'dpi'] - - default_options = { - 'single_column_width': 8.3, - 'double_column_width': 17.1, - 'column_type': 'single', - 'width_ratio': '1:1', - 'aspect_ratio': '1:1', - 'compress_width': 1, - 'compress_height': 1, - 'upscaling_factor': 1.0, - 'dpi': 600, - } - - options = update_options(format_params, required_options, default_options) - - - # Reset run commands - plt.rcdefaults() - - # Update run commands if any is passed (will pass an empty dictionary if not passed) - update_rc_params(rc_params) - - width = determine_width(options) - height = determine_height(options, width) - width, height = scale_figure(options=options, width=width, height=height) - - fig, ax = plt.subplots(figsize=(width, height), dpi=options['dpi']) - - return fig, ax - - -def prepare_dos_plot(width=None, height=None, square=True, dpi=None, colour_cycle=('qualitative', 'Dark2_8'), energyunit='eV', dosunit='arb. u.', scale=1, pdos=None): - - if not pdos: - linewidth = 3*scale - else: - linewidth = 3 - - axeswidth = 3*scale - - plt.rc('lines', linewidth=linewidth) - plt.rc('axes', linewidth=axeswidth) - - if square: - if not width: - width = 20 - - if not height: - height = width - - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(width, height), facecolor='w', dpi=dpi) - - - return fig, ax - -def prettify_dos_plot(fig, ax, options): - - - required_options = ['plot_kind', 'flip_xy', 'hide_x_labels', 'hide_y_labels', 'xlabel', 'ylabel', 'xunit', 'yunit', 'xlim', 'ylim', 'x_tick_locators', 'y_tick_locators', 'y_tick_format', 'x_tick_format', 'hide_x_ticks', 'hide_y_ticks', 'hide_x_ticklabels', 'hide_y_ticklabels', - 'colours', 'palettes', 'title', 'legend', 'labels', 'label_colours', 'legend_position', 'legend_ncol', 'subplots_adjust', 'text'] - - default_options = { - 'plot_kind': 'PDOS', # DOS/PDOS/COOP/COHP - 'flip_xy': False, - 'hide_x_labels': False, # Whether x labels should be hidden - 'hide_x_ticklabels': False, - 'hide_x_ticks': False, - 'hide_y_labels': False, # whether y labels should be hidden - 'hide_y_ticklabels': False, - 'hide_y_ticks': False, - 'xlabel': 'Energy', - 'ylabel': 'DOS', - 'xunit': r'eV', # The unit of the x-values in the curve plot - 'yunit': r'a.u.', # The unit of the y-values in the curve and bar plots - 'xlim': None, - 'ylim': None, - 'x_tick_locators': [1, .5], # Major and minor tick locators - 'y_tick_locators': [1, .5], - 'y_tick_format': None, - 'x_tick_format': None, - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'title': None, - 'legend': True, - 'labels': None, - 'label_colours': None, - 'legend_position': ['upper center', (0.20, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively - 'legend_ncol': 1, - 'subplots_adjust': [0.1, 0.1, 0.9, 0.9], - 'text': None - } - - - if 'plot_kind' in options.keys(): - if 'ylabel' not in options.keys(): - if options['plot_kind'] == 'DOS': - options['ylabel'] = 'DOS' - elif options['plot_kind'] == 'PDOS': - options['ylabel'] = 'PDOS' - elif options['plot_kind'] == 'COOP': - options['ylabel'] = 'COOP' - elif options['plot_kind'] == 'COHP': - options['ylabel'] = 'COHP' - - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - - if options['flip_xy']: - - # Switch all the x- and y-specific values - options = aux.swap_values(dict=options, key1='xlim', key2='ylim') - options = aux.swap_values(dict=options, key1='xunit', key2='yunit') - options = aux.swap_values(dict=options, key1='xlabel', key2='ylabel') - options = aux.swap_values(dict=options, key1='x_tick_locators', key2='y_tick_locators') - options = aux.swap_values(dict=options, key1='hide_x_labels', key2='hide_y_labels') - - # Set labels on x- and y-axes - if not options['hide_y_labels']: - ax.set_ylabel(f'{options["ylabel"]} [{options["yunit"]}]') - else: - ax.set_ylabel('') - - - - if not options['hide_x_labels']: - ax.set_xlabel(f'{options["xlabel"]} [{options["xunit"]}]') - else: - ax.set_xlabel('') - - - # Hide x- and y- ticklabels - if options['hide_y_ticklabels']: - ax.tick_params(axis='y', direction='in', which='both', labelleft=False, labelright=False) - if options['hide_x_ticklabels']: - ax.tick_params(axis='x', direction='in', which='both', labelbottom=False, labeltop=False) - - - # Hide x- and y-ticks: - if options['hide_y_ticks']: - ax.tick_params(axis='y', direction='in', which='both', left=False, right=False) - if options['hide_x_ticks']: - ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False) - - - - # Set multiple locators - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0])) - ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1])) - - # Change format of axis tick labels if specified: - - - - # Set title - if options['title']: - ax.set_title(options['title']) - - - if options['y_tick_format']: - ax.yaxis.set_major_formatter(FormatStrFormatter(options['y_tick_format'])) - if options['x_tick_format']: - ax.xaxis.set_major_formatter(FormatStrFormatter(options['x_tick_format'])) - - - # Create legend - - if ax.get_legend(): - ax.get_legend().remove() - - - if options['legend'] and options['labels']: - - - # Generate colours - if not options['colours']: - colour_cycle = generate_colours(palettes=options['palettes']) - - colours = [] - for label in options['labels']: - colours.append(next(colour_cycle)) - - - else: - colours = options['colours'] - - if options['label_colours']: - colours = options['label_colours'] - - # Create legend - patches = [] - for i, label in enumerate(options['labels']): - patches.append(mpatches.Patch(color=colours[i], label=label)) - - print(options['legend_ncol']) - - ax.legend(handles=patches, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False, ncol=options['legend_ncol']) - - - - # Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions) - plt.subplots_adjust(left=options['subplots_adjust'][0], bottom=options['subplots_adjust'][1], right=options['subplots_adjust'][2], top=options['subplots_adjust'][3]) - - - # If limits for x- and y-axes is passed, sets these. - if options['xlim']: - ax.set_xlim(options['xlim']) - - if options['ylim']: - ax.set_ylim(options['ylim']) - - - # Add custom text - if options['text']: - plt.text(x=options['text'][1][0], y=options['text'][1][1], s=options['text'][0]) - - - - if options['e_fermi']: - if options['flip_xy']: - ax.axhline(0, c='black', ls='dashed') - else: - ax.axvline(0, c='black', ls='dashed') - - if options['plot_kind'] == 'DOS' or options['plot_kind'] == 'PDOS': - if options['dos_info']['spin_polarised']: - if options['flip_xy']: - ax.axvline(0, c='black') - else: - ax.axhline(0, c='black') - elif options['plot_kind'] == 'COOP' or options['plot_kind'] == 'COHP': - if options['flip_xy']: - ax.axvline(0, c='black') - else: - ax.axhline(0, c='black') - - return fig, ax - - - -def plot_coop(data, options): - ''' interactions = list with number of interaction (index + 1 of interactions list from read_coop)''' - - - default_options = { - 'plot_kind': 'COOP', - 'mode': 'individual', - 'fill': False, - 'up': True, - 'down': True, - 'collapse': False, - 'interactions': None, - 'palettes': [('qualitative', 'Dark2_8')], - 'colours': None, - 'flip_xy': False - - } - - options = aux.update_options(options=options, default_options=default_options) - - - fig, ax = btp.prepare_plot(options=options) - - coopcar, coop_interactions = dft.io.read_coop(data=data, options=options) - - - - if not options['colours']: - colour_cycle = btp.generate_colours(palettes=options['palettes']) - - colours = [] - for interaction in range(len(coop_interactions)): - colours.append(next(colour_cycle)) - - else: - colours = options['colours'] - - - # If interactions has been specified - if options['interactions']: - - # Make interactions into a list of lists for correct looping below - if type(options['interactions'][0]) != list: - interactions_list = [options['interactions']] - else: - interactions_list = options['interactions'] - - for ind, interactions in enumerate(interactions_list): - - # Determine which columns to plot if collapse is enabled - if options['collapse']: - to_plot = [2*(i-1)+3 for i in interactions] - - # Make sum column for plotting if sum mode is enabled - if options['mode'] == 'sum': - coopcar["sum"] = coopcar.iloc[:, to_plot].sum(axis=1) - to_plot = ['sum'] - - - # Make mean column for plotting if mean mode is enabeld - elif options['mode'] == 'avg' or options['mode'] == 'average' or options['mode'] == 'mean': - coopcar["mean"] = coopcar.iloc[:, to_plot].mean(axis=1) - to_plot = ['mean'] - - # Determine which columns to plot if collapse is disabled and both up and down should be plotted - elif options['up'] and options['down']: - to_plot_up = [2*(i-1)+3 for i in interactions] - to_plot_down = [2*(i-1)+5 +2*len(coop_interactions) for i in interactions] - to_plot = to_plot_up + to_plot_down - - if options['mode'] == 'sum': - coopcar["sum_up"] = coopcar.iloc[:, to_plot_up].sum(axis=1) - coopcar["sum_down"] = coopcar.iloc[:, to_plot_down].sum(axis=1) - to_plot = ['sum_up', 'sum_down'] - - elif options['mode'] == 'avg' or options['mode'] == 'average' or options['mode'] == 'mean': - coopcar["mean_up"] = coopcar.iloc[:, to_plot_up].mean(axis=1) - coopcar["mean_down"] = coopcar.iloc[:, to_plot_down].mean(axis=1) - to_plot = ['mean_up', 'mean_down'] - - # Determine which columns to plot if collapse is disabled and only up should be plotted - elif options['up']: - to_plot = [2*(i-1)+3 for i in interactions] - - if options['mode'] == 'sum': - coopcar["sum_up"] = coopcar.iloc[:, to_plot].sum(axis=1) - to_plot = ['sum_up'] - - elif options['mode'] == 'avg' or options['mode'] == 'average' or options['mode'] == 'mean': - coopcar["mean_up"] = coopcar.iloc[:, to_plot].mean(axis=1) - to_plot = ['mean_up'] - - - # Determine which columns to plot if collapse is disabled and only down should be plotted - elif options['down']: - to_plot = [2*(i-1)+5 +2*len(coop_interactions) for i in interactions] - - if options['mode'] == 'sum': - coopcar["sum_down"] = coopcar.iloc[:, to_plot].sum(axis=1) - to_plot = ['sum_down'] - - elif options['mode'] == 'avg' or options['mode'] == 'average' or options['mode'] == 'mean': - coopcar["mean_down"] = coopcar.iloc[:, to_plot].mean(axis=1) - to_plot = ['mean_down'] - - - - - # Plot all columns as decided above - for j, column in enumerate(to_plot): - if options['fill']: - ax.fill_between(coopcar["Energy"], coopcar[column], 0, where=coopcar[column]>0, color=colours[ind]) - ax.fill_between(coopcar["Energy"], coopcar[column], 0, where=coopcar[column]<0, color=colours[ind+1]) - - else: - if options['mode'] == "individual": - colour = colours[j] - else: - colour = colours[ind] - - - if options['flip_xy']: - coopcar.plot(y='Energy', x=column, ax=ax, color=colour) - else: - coopcar.plot(x='Energy', y=column, ax=ax, color=colour) - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - return coopcar, fig, ax - - - -def prettify_coop_plot(fig, ax, energyunit='eV', dosunit='arb. u.', xlim=None, ylim=None, title=None, hide_ylabels=False, hide_xlabels=False, hide_yvals=False, hide_xvals=False, flip_xy=False, pad_bottom=None, scale=1, colours=None, atoms=None, pdos=False, width=None, height=None, e_fermi=False, adjust=False, legend=False, labels=None, label_colours=None, xpad=0, ypad=0): - - # Set sizes of ticks, labes etc. - ticksize = 30*scale - labelsize = 30*scale - legendsize = 30*scale - titlesize = 30*scale - - linewidth = 3*scale - axeswidth = 3*scale - majorticklength = 20*scale - minorticklength = 10*scale - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - if flip_xy: - - # Set labels on x- and y-axes - if not hide_ylabels: - if ypad: - ax.set_ylabel('Energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - else: - ax.set_ylabel('Energy [{}]'.format(energyunit), size=labelsize) - - if pdos: - if xpad: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize, labelpad=xpad) - else: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize) - - else: - if width >= 10: - if xpad: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize, labelpad=xpad) - else: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize) - - else: - if xpad: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize, labelpad=xpad) - else: - ax.set_xlabel('COOP [{}]'.format(dosunit), size=labelsize) - - ax.tick_params(axis='y', direction='in', which='major', right=True, length=majorticklength, width=linewidth) - ax.tick_params(axis='y', direction='in', which='minor', right=True, length=minorticklength, width=linewidth) - - if hide_yvals: - ax.tick_params(axis='y', labelleft=False) - - ax.tick_params(axis='x', direction='in', which='major', bottom=False, labelbottom=False) - - ax.yaxis.set_major_locator(MultipleLocator(1)) - ax.yaxis.set_minor_locator(MultipleLocator(.5)) - - - - else: - # Set labels on x- and y-axes - if adjust: - if xpad: - ax.set_xlabel('E - E$_F$ [{}]'.format(energyunit), size=labelsize, labelpad=xpad) - else: - ax.set_xlabel('E - E$_F$ [{}]'.format(energyunit), size=labelsize) - - - else: - if xpad: - ax.set_xlabel('Energy [{}]'.format(energyunit), size=labelsize, labelpad=xpad) - else: - ax.set_xlabel('Energy [{}]'.format(energyunit), size=labelsize) - - - if height < 10: - if ypad: - ax.set_ylabel('COOP [{}]'.format(dosunit), size=labelsize, labelpad=ypad) - else: - ax.set_ylabel('COOP [{}]'.format(dosunit), size=labelsize) - - else: - if ypad: - ax.set_ylabel('Crystal orbital overlap population [{}]'.format(dosunit), size=labelsize, labelpad=ypad) - else: - ax.set_ylabel('Crystal orbital overlap population [{}]'.format(dosunit), size=labelsize) - - - ax.tick_params(axis='x', direction='in', which='major', bottom=True, top=True, length=majorticklength, width=linewidth) - ax.tick_params(axis='x', direction='in', which='minor', bottom=True, top=True, length=minorticklength, width=linewidth) - - - ax.tick_params(axis='y', which='major', direction='in', right=True, left=True, labelleft=True, length=majorticklength, width=linewidth) - ax.tick_params(axis='y', which='minor', direction='in', right=True, left=True, length=minorticklength, width=linewidth) - - if hide_ylabels: - ax.set_ylabel('') - if hide_xlabels: - ax.set_xlabel('') - if hide_yvals: - ax.tick_params(axis='y', which='both', labelleft=False) - if hide_xvals: - ax.tick_params(axis='x', which='both', labelbottom=False) - - - if ylim: - yspan = ylim[1] - ylim[0] - yloc = np.round(yspan / 4, 2) - - ax.yaxis.set_major_locator(MultipleLocator(yloc)) - ax.yaxis.set_minor_locator(MultipleLocator(yloc/2)) - - - - ax.xaxis.set_major_locator(MultipleLocator(1)) - ax.xaxis.set_minor_locator(MultipleLocator(.5)) - - - - plt.xlim(xlim) - plt.ylim(ylim) - - - if title: - ax.set_title(title, size=40) - - - - if legend: - patches = [] - - if label_colours: - colours=label_colours - - for ind, label in enumerate(labels): - patches.append(mpatches.Patch(color=colours[ind], label=label)) - - fig.legend(handles=patches, loc='upper right', ncol=len(labels), bbox_to_anchor=(0.8, 0.45), fontsize=legendsize/1.25, frameon=False) - - #bbox_to_anchor=(1.20, 0.91) - - - - if pad_bottom is not None: - bigax = fig.add_subplot(111) - bigax.set_facecolor([1,1,1,0]) - bigax.spines['top'].set_visible(False) - bigax.spines['bottom'].set_visible(True) - bigax.spines['left'].set_visible(False) - bigax.spines['right'].set_visible(False) - bigax.tick_params(labelcolor='w', color='w', direction='in', top=False, bottom=True, left=False, right=False, labelleft=False, pad=pad_bottom) - - - - if xpad: - ax.tick_params(axis='x', pad=xpad) - - if ypad: - ax.tick_params(axis='y', pad=ypad) - - if e_fermi: - if flip_xy: - plt.axhline(0, lw=linewidth, c='black', ls='dashed') - else: - plt.axvline(0, lw=linewidth, c='black', ls='dashed') - - - plt.axhline(0, lw=linewidth, c='black') - - return fig, ax - - - - -def get_unique_atoms(interactions): - ''' Get all the unique atoms involved in the interactions from the COOP-calculation - - Input: - interactions: list of interactions that comes as output from read_coop() - - Outut: - unique_atoms: list of unique atoms in the interactions list''' - - unique_atoms = [] - - for interaction in interactions: - - atoms = interaction.split('->') - - for atom in atoms: - if atom not in unique_atoms: - unique_atoms.append(atom) - - - unique_atoms.sort() - - return unique_atoms - - -def get_interactions_involving(interactions, targets): - ''' Get the indicies (+1) of all the interactions involving target. This list can be used as input to plot_coop(), as it is - then formatted the way that function accepts these interactions. - - Input: - interactions: list of interactions as output from read_coop() - target: the particular atom that should be involved in the interactions contained in the output list - - Output: - target_interactions: Indices (+1) of all the interactions involving target atom.''' - - target_interactions = [] - appended_interactions = [] - - - if type(targets) == list: - for target in targets: - for ind, interaction in enumerate(interactions): - if target in interaction.split('->') and interaction not in appended_interactions: - target_interactions.append(ind+1) - appended_interactions.append(interaction) - - else: - for ind, interaction in enumerate(interactions): - if targets in interaction.split('->'): - target_interactions.append(ind+1) - - - return target_interactions - - - - -def update_rc_params(rc_params): - ''' Update all passed run commands in matplotlib''' - - if rc_params: - for key in rc_params.keys(): - plt.rcParams.update({key: rc_params[key]}) - - - -def update_options(options, required_options, default_options): - ''' Update all passed options''' - - - for option in required_options: - if option not in options.keys(): - options[option] = default_options[option] - - - - return options - - - -def determine_width(options): - - conversion_cm_inch = 0.3937008 # cm to inch - - if options['column_type'] == 'single': - column_width = options['single_column_width'] - elif options['column_type'] == 'double': - column_width = options['double_column_width'] - - column_width *= conversion_cm_inch - - - width_ratio = [float(num) for num in options['width_ratio'].split(':')] - - - width = column_width * width_ratio[0]/width_ratio[1] - - - return width - - -def determine_height(options, width): - - aspect_ratio = [float(num) for num in options['aspect_ratio'].split(':')] - - height = width/(aspect_ratio[0] / aspect_ratio[1]) - - return height - - -def scale_figure(options, width, height): - width = width * options['upscaling_factor'] * options['compress_width'] - height = height * options['upscaling_factor'] * options['compress_height'] - - return width, height - - - -def generate_colours(options: dict): - - if not isinstance(options['palettes'], list): - options['palettes'] = [options['palettes']] - - # Creates a list of all the colours that is passed in the colour_cycles argument. Then makes cyclic iterables of these. - colour_collection = [] - - for palette in options['palettes']: - mod = importlib.import_module("palettable.colorbrewer.%s" % palette[0]) - colour = getattr(mod, palette[1]).mpl_colors - colour_collection = colour_collection + colour - - colour_cycle = itertools.cycle(colour_collection) - - - return colour_cycle \ No newline at end of file diff --git a/nafuma/dft/io.py b/nafuma/dft/io.py deleted file mode 100644 index 23d7d41..0000000 --- a/nafuma/dft/io.py +++ /dev/null @@ -1,1021 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt -import numpy as np -from scipy.signal import savgol_filter - -import warnings - -import os - -import linecache - -import nafuma.auxillary as aux - - -def open_doscar(doscar='DOSCAR'): - with open(doscar, 'r') as dos: - lines = dos.readlines() - - return lines - -def open_poscar(poscar='POSCAR'): - with open(poscar, 'r') as pos: - lines = pos.readlines() - - return lines - -def open_outcar(outcar='OUTCAR'): - with open(outcar, 'r') as out: - lines = out.readlines() - - return lines - -def get_number_of_atoms(doscar='DOSCAR'): - lines = open_doscar(doscar) - - return int(lines[0].strip().split()[0]) - -def get_atoms(poscar): - - with open(poscar, 'r') as poscar: - lines = poscar.readlines() - - atoms = lines[5].split() - atom_num = lines[6].split() - - atom_num = [int(num) for num in atom_num] - - - atoms_list = make_atoms_list(atoms, atom_num) - - atoms_info = { - 'specie': atoms, # list of specie in sequential order - 'number': atom_num, # list of number of per specie in sequential order - 'list': atoms_list # list of every individual atom in sequential order - } - - return atoms_info - - -def get_fermi_level(doscar='DOSCAR', headerlines=6): - lines = open_doscar(doscar) - - return float(lines[headerlines-1].strip().split()[3]) - - -def get_valence_electron_count(outcar='OUTCAR', poscar='POSCAR'): - lines = open_outcar(outcar) - - atoms, atoms_dict = get_atoms(poscar) - n_atoms = len(atoms) - - for line in lines: - line = line.strip() - if line[0:4] == "ZVAL": - valence_electrons = line.split()[-n_atoms:] - break - - return valence_electrons - - - - -def read_coop(data={}, options={}): - ''' Reads a COOPCAR.lobster file and prepares the DataFrame for further data handling. - - Input: - path: The path to the COOPCAR.lobster-file - - Output: - coopcar: A DataFrame containing the COOP data - interactions: A list of interactions''' - - - required_options = ['collapse'] - - default_options = { - 'collapse': False, - 'adjust': None, - } - - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - interactions = determine_interactions(data['coopcar']) - - coopcar = pd.read_csv(data['coopcar'], skiprows=3+len(interactions), header=None, delim_whitespace=True) - - spin_polarised = determine_spin_polarisation(coopcar, interactions) - - - # Create list of column names - # If the run is spin polarised, add all up states first and then all down states - if spin_polarised: - columns = ['Energy', 'avg_up', 'avg_int_up'] - for i in range(1, len(interactions)+1): - columns += ['interaction{}_up'.format(i), 'interaction{}_int_up'.format(i)] - columns += ['avg_down', 'avg_int_down'] - for i in range(1, len(interactions)+1): - columns += ['interaction{}_down'.format(i), 'interaction{}_int_down'.format(i)] - - # Otherwise just - else: - columns = ['Energy', 'avg', 'avg_int'] - for i in range(1, len(interactions)+1): - columns += ['interaction_{}'.format(i), 'interaction_{}_int'.format(i)] - - - - - coopcar.columns = columns - - if options['adjust']: - coopcar['Energy'] = coopcar['Energy'] - options['adjust'] - - - if options['collapse']: - - columns_collapsed = ['Energy'] - - for column in columns: - if column.split('_')[0] not in columns_collapsed: - columns_collapsed.append(''.join(column.split('_')[:-1])) - columns_collapsed.append(''.join(column.split('_')[:-1])+'_int') - - - for column in columns_collapsed: - if column != 'Energy': - coopcar[column] = coopcar[column+'_up'] + coopcar[column+'_down'] - - columns.remove('Energy') - coopcar.drop(columns, axis=1, inplace=True) - coopcar.columns = columns_collapsed - - - - - - return coopcar, interactions - - -def read_cohp(path, flip_sign=False): - ''' Reads a COHPCAR.lobster file and prepares the DataFrame for further data handling. - - Input: - path: The path to the COOPCAR.lobster-file - flip_sign: Boolean value to determine whether all COHP-values should be flipped (that is, return -COHP) - - Output: - coopcar: A DataFrame containing the COOP data - interactions: A list of interactions''' - - interactions = determine_interactions(path) - - cohpcar = pd.read_csv(path, skiprows=3+len(interactions), header=None, delim_whitespace=True) - - spin_polarised = determine_spin_polarisation(cohpcar, interactions) - - - # Create list of column names - # If the run is spin polarised, add all up states first and then all down states - if spin_polarised: - columns = ['Energy', 'avg_up', 'avg_up_int'] - for i in range(1, len(interactions)+1): - columns += ['interaction_{}_up'.format(i), 'interaction_{}_up_int'.format(i)] - columns += ['avg_down', 'avg_down_int'] - for i in range(1, len(interactions)+1): - columns += ['interaction_{}_down'.format(i), 'interaction_{}_up_int'.format(i)] - - # Otherwise just - else: - columns = ['Energy', 'avg', 'avg_int'] - for i in range(1, len(interactions)+1): - columns += ['interaction_{}'.format(i), 'interaction_{}_int'.format(i)] - - - cohpcar.columns = columns - - if flip_sign: - columns = columns[1:] - - for column in columns: - cohpcar[column] = -cohpcar[column] - - return cohpcar, interactions - - -def determine_interactions(path): - ''' Determines the number of interactions present in the COOPCAR.lobster file. - - Input: - path: The path to the COOPCAR.lobster-file - - Output: - interactions: A list of strings with the interactions in the COOPCAR.lobster file''' - - - with open(path, 'r') as coop: - lines = coop.readlines() - - - interactions = [] - for line in lines: - if line[0:2] == 'No': - interactions.append(line.split(':')[-1].split('(')[0]) - - - return interactions - - - -def determine_spin_polarisation(coopcar, interactions): - ''' Determines whether a COOPCAR.lobster file is spin polarised or not. - - Input: - coopcar: A DataFrame containing the COOP data - interactions: A list of interactions obtained from the determine_interactions function - - Output: - spin_polarised: Boolean value to indicate whether or not the COOP data is spin polarised''' - - number_of_interactions = len(interactions) - - spin_polarised = True if coopcar.shape[1] == 4*number_of_interactions + 5 else False - - return spin_polarised - - - - -def read_dos(path, flip_down=True): - - dos_info = get_doscar_information(path) - - with open(path, 'r') as doscar: - count = 0 - raw_data = [] - - while count < dos_info["NEDOS"] + 6: - if count >= 6: - data_line = [float(x) for x in doscar.readline().split()] - raw_data.append(data_line) - - else: - doscar.readline() - - count += 1 - - - dos = pd.DataFrame(raw_data) - - if dos_info["spin_polarised"]: - header_names = ['energy', 'total_up', 'total_down', 'total_integrated_up', 'total_integrated_down'] - else: - header_names = ['energy', 'total', 'total_integrated'] - - - dos.columns = header_names - - if dos_info["spin_polarised"] and flip_down: - dos["total_down"] = -dos["total_down"] - dos["total_integrated_down"] = -dos["total_integrated_down"] - - return dos - - -def read_pdos(data: dict, options={}): - ''' data-dictionary should be structured like this: - data["path"] - dictionary with path to POSCAR/CONTCAR mapped to key 'poscar' and path to DOSCAR in key 'doscar'. ''' - - default_options = { - 'sum_atoms': False, - 'sum_orbitals': False, - 'adjust': False, # Manually adjust the energy scale if the automatic Fermi-level detection didn't work. - 'normalise': False, - } - - options = aux.update_options(options=options, default_options=default_options) - - # Grab some metadata - data['atoms'] = get_atoms(data['path']['poscar']) - data['info'] = get_doscar_information(data['path']['doscar']) - - - # Read the data - with open(data['path']['doscar'], 'r') as f: - doscar_raw = f.readlines() - - # Read basis functions - this will only yield a non-empty list if DOSCAR is generated by LOBSTER. It will only contain columns for the used basis set, so to be able to make proper column headers, this information is needed. - data['basis_functions'] = get_basis_functions(doscar_raw) - - # If DOSCAR is from VASP and not LOBSTER, the basis functions will not be listed in DOSCAR (but all columns will be present) - if not data['basis_functions']: - data['basis_functions'] = [None for _ in data['atoms']['list']] - - - data['pdos'] = [] - for i, (atom, basis_functions) in enumerate(zip(data['atoms']['list'], data['basis_functions'])): - - # Define line to start read-in: (1+i) to skip total DOS in the start, plus every previous PDOS, including the headerline. Then the initial 6 header lines in the start of the file. - start = int( (1 + i) * ( data['info']['NEDOS'] + 1 ) + 6 ) - - pdos_atom = [] - for j in range(0,data['info']['NEDOS']): - pdos_atom.append(doscar_raw[start+j].split()) - - # Convert the data to a DataFrame and convert to float - pdos_atom = pd.DataFrame(pdos_atom) - pdos_atom = pdos_atom.astype(float) - - - # If DOSCAR was written by VASP, set the standard columns to the basis_functions variable - # FIXME This does not allow for f-orbitals. Could do a check vs. shape of pdos_atom to determine - if not basis_functions: - basis_functions = ['s', 'p1', 'p2', 'p3', '2p1', '2p2', '2p3', 'd1', 'd2', 'd3', 'd4', 'd5'] - - - # Split the basis functions into up and down spin channels - if data['info']['spin_polarised']: - columns = ['Energy'] - for function in basis_functions: - columns.append(function+'_up') - columns.append(function+'_down') - - else: - columns = ['Energy'] + basis_functions - - # Set the new column names - pdos_atom.columns = columns - - if options['adjust']: - pdos_atom['Energy'] -= options['adjust'] - - if options['smooth']: - pdos_atom = smoothing(pdos=pdos_atom, options=options) - - if options['normalise']: - orbitals = [orbital for orbital in columns if 'Energy' not in orbital] - for k, specie in enumerate(data['atoms']['specie']): - if specie == atom: - index = k - - for orbital in orbitals: - pdos_atom[orbital] = pdos_atom[orbital] / data['atoms']['number'][index] - - # Make total columns - if data['info']['spin_polarised']: - up_functions = [orbital for orbital in columns if '_up' in orbital] - down_functions = [orbital for orbital in columns if '_down' in orbital] - - pdos_atom['total_up'] = pdos_atom[up_functions].sum(axis=1) - pdos_atom['total_down'] = pdos_atom[down_functions].sum(axis=1) - pdos_atom['total'] = pdos_atom[['total_up', 'total_down']].sum(axis=1) - - # Flip all the values for spin down - down_functions.append('total_down') - for orbital in down_functions: - pdos_atom[orbital] = (-1)*pdos_atom[orbital] - - data['pdos'].append(pdos_atom) - - if options['sum_atoms']: - data['pdos'] = sum_atoms(data, options) - if options['sum_orbitals']: - data['pdos'] = sum_orbitals(data, options) - - return data - - -def sum_atoms(data: dict, options={}): - ''' Needs to have read data through read_pdos() ''' - - default_options = { - - } - - options = aux.update_options(options=options, default_options=default_options) - - - total_atoms = 0 - - # Sort all the DataFrames into a dictionary - sorted_dfs = {} - for i, (specie, number) in enumerate(zip(data['atoms']['specie'], data['atoms']['number'])): - - sorted_dfs[specie] = [] - - for j in range(number): - index = j if i == 0 else j + total_atoms - - sorted_dfs[specie].append(data['pdos'][index]) - - total_atoms += number - - # Sum all the DataFrames for each specie - summed_dfs = {} - for i, (specie, dfs) in enumerate(sorted_dfs.items()): - for j, df in enumerate(dfs): - if j > 0: - dfs[0] += df - - dfs[0]['Energy'] = dfs[0]['Energy'] / data['atoms']['number'][i] - summed_dfs[specie] = dfs[0] - - return summed_dfs - - -def sum_orbitals(data: dict, options={}): - - shells = [ - '1s', '2s', '3s', '4s', '5s', '6s', - '2p', '3p', '4p', '5p', '6p', - '3d', '4d', '5d', '6d', - '4f', '5f', '6f' - ] - - summed_dfs = [] - - # If sum_atoms() is already called - if isinstance(data['pdos'], dict): - summed_dfs = {} - - for specie, specie_pdos in data['pdos'].items(): - orbitals = [orbital for orbital in specie_pdos.columns if 'Energy' not in orbital] - new_pdos = pd.DataFrame(specie_pdos['Energy']) - - for shell in shells: - if data['info']['spin_polarised']: - sub_columns_up = [orbital for orbital in orbitals if orbital.startswith(shell) and orbital.endswith('_up')] - sub_columns_down = [orbital for orbital in orbitals if orbital.startswith(shell) and orbital.endswith('_down')] - - new_pdos[shell+'_up'] = data['pdos'][specie][sub_columns_up].sum(axis=1) - new_pdos[shell+'_down'] = data['pdos'][specie][sub_columns_down].sum(axis=1) - - else: - sub_columns = [orbital for orbital in orbitals if orbital.startswith(shell)] - - new_pdos[shell] = data['pdos'][specie][sub_columns].sum(axis=1) - new_pdos = new_pdos.loc[:, (new_pdos != 0).any(axis=0)] - summed_dfs[specie] = new_pdos - - - else: - for pdos in data['pdos']: - orbitals = [orbital for orbital in pdos.columns if 'Energy' not in orbital] - new_pdos = pd.DataFrame(pdos['Energy']) - for shell in shells: - if data['info']['spin_polarised']: - sub_columns_up = [orbital for orbital in orbitals if orbital.startswith(shell) and orbital.endswith('_up')] - sub_columns_down = [orbital for orbital in orbitals if orbital.startswith(shell) and orbital.endswith('_down')] - new_pdos[shell+'_up'] = pdos[sub_columns_up].sum(axis=1) - new_pdos[shell+'_down'] = pdos[sub_columns_down].sum(axis=1) - else: - sub_columns = [orbital for orbital in orbitals if orbital.startswith(shell)] - new_pdos[shell] = pdos[sub_columns].sum(axis=1) - new_pdos = new_pdos.loc[:, (new_pdos != 0).any(axis=0)] - summed_dfs.append(new_pdos) - - - return summed_dfs - - -def smoothing(pdos: pd.DataFrame, options={}): - ' Smoothes the data using the Savitzky-Golay filter. This is the only algorithm at this moment. ' - - default_options = { - 'smooth_window_length': 3, # Determines the window length of smoothing that the savgol-filter uses for smoothing - 'smooth_polyorder': 2, # Determines the order of the polynomial used in the smoothing algorithm - 'smooth_algorithm': 'savgol', # At the present, only Savitzky-Golay filter is implemented. Add Gaussian and Boxcar later. - } - - options = aux.update_options(options=options, default_options=default_options) - - # Initialise new DataFrame with correct x-values - pdos_smooth = pd.DataFrame(pdos['Energy']) - - orbitals = [orbital for orbital in pdos.columns if 'Energy' not in orbital] - - if options['smooth_algorithm'] == 'savgol': - - for orbital in orbitals: - pdos_smooth.insert(1, orbital, savgol_filter(pdos[orbital], options['smooth_window_length'], options['smooth_polyorder'])) - - return pdos_smooth - - - -def get_basis_functions(doscar): - - basis_functions = [] - for line in doscar: - if 'Z=' in line: - basis_functions.append(line.split(';')[-1].split()) - - - return basis_functions - - - -def get_doscar_information(path): - ''' Reads information from the DOSCAR''' - - kind = 'LOBSTER' if 'LOBSTER' in linecache.getline(path, 5) else 'VASP' - dos_info_raw = linecache.getline(path, 6).split() - spin_polarised = True if len(linecache.getline(path, 7).split()) == 5 else False - - - dos_info = { - 'ENMIN': float(dos_info_raw[0]), - 'ENMAX': float(dos_info_raw[1]), - 'NEDOS': int(dos_info_raw[2]), - 'EFERMI': float(dos_info_raw[3]), - 'spin_polarised': spin_polarised, - 'kind': kind - } - - return dos_info - - -def make_atoms_list(atoms, number_of_atoms): - - atoms_list = [] - for atom, num in zip(atoms, number_of_atoms): - atoms_list = atoms_list + [atom for _ in range(num)] - - return atoms_list - - - -def read_pdos_legacy(data: dict, options={}): - - required_options = ['flip_down', 'sum_atoms', 'sum_orbitals', 'collapse_spin', 'adjust', 'manual_adjust', 'normalise', 'normalise_unit_cell', 'normalisation_factor'] - - default_options = { - 'flip_down': True, - 'sum_atoms': False, - 'sum_orbitals': False, - 'collapse_spin': False, - 'adjust': False, - 'manual_adjust': None, - 'normalise': False, - 'normalise_unit_cell': False, - 'normalisation_factor': None - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - # Get information about the DOSCAR-file (ENMIN, ENMAX, NEDOS, EFERMI and spin_polarised) - dos_info = get_doscar_information(data['doscar']) - - # Get information from the POSCAR-file (or CONTCAR) (which species, number of atoms per specie and a dictionary matching the two) - species, atom_num, atoms_dict = get_atoms(data['poscar']) - - - # Open DOSCAR and read all lines - with open(data['doscar'], 'r') as file: - doscar_data = file.readlines() - - # Make list of all individual atoms - atoms = [] - for specie in species: - for i in range(0,atoms_dict[specie]): - atoms.append(specie) - - - # Loop through all the atoms and make a DataFrame for each atom. - - pdos_full = [] - for ind, atom in enumerate(atoms): - - # Define line to start reading - start = int((1 + ind) * dos_info["NEDOS"] + 6 + (ind * 1)) - - add_d_orbitals = False - add_p_orbitals = False - - - - - # Check if d-orbitals are included (if DOSCAR comes from LOBSTER), otherwise they will have to be added to make the DataFrames the same size - if dos_info['kind'] == 'LOBSTER': - # Extract basis sets from each atom - basis_sets = doscar_data[start].split(';')[-1] - - if not 'd' in basis_sets: - add_d_orbitals = True - - if basis_sets.count('p') != 6: - add_p_orbitals = True - - - - # Start reading lines into a list and convert to DataFrame and cast all entries as float - pdos_atom = [] - for i in range(1,dos_info["NEDOS"]+1): - pdos_atom.append(doscar_data[start+i].split()) - - - pdos_atom = pd.DataFrame(pdos_atom) - pdos_atom = pdos_atom.astype(float) - - - - - # Give the columns names and add second set of p-orbitals, d-orbitals, or both if they don't exist in the data - if add_p_orbitals: - if dos_info['spin_polarised']: - pdos_atom.columns = ['Energy', 's_u', 's_d', 'p1_u', 'p1_d', 'p2_u', 'p2_d', 'p3_u', 'p3_d'] - pdos_atom[['2p1_u', '2p1_d', '2p2_u', '2p2_d', '2p3_u', '2p3_d']] = 0 - - if add_d_orbitals: - pdos_atom[['d1_u', 'd1_d', 'd2_u', 'd2_d', 'd3_u', 'd3_d', 'd4_u', 'd4_d', 'd5_u', 'd5_d']] = 0 - pdos_atom['tot_u'] = pdos_atom[['s_u', 'p1_u', 'p2_u', 'p3_u', '2p1_u', '2p2_u', '2p3_u', 'd1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - pdos_atom['tot_d'] = pdos_atom[['s_d', 'p1_d', 'p2_d', 'p3_d', '2p1_d', '2p2_d', '2p3_d', 'd1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - else: - pdos_atom['tot_u'] = pdos_atom[['s_u', 'p1_u', 'p2_u', 'p3_u', '2p1_u', '2p2_u', '2p3_u', 'd1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - pdos_atom['tot_d'] = pdos_atom[['s_d', 'p1_d', 'p2_d', 'p3_d', '2p1_d', '2p2_d', '2p3_d', 'd1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - - - elif add_d_orbitals: - if dos_info["spin_polarised"]: - pdos_atom.columns = ['Energy', 's_u', 's_d', 'p1_u', 'p1_d', 'p2_u', 'p2_d', 'p3_u', 'p3_d'] - pdos_atom[['d1_u', 'd1_d', 'd2_u', 'd2_d', 'd3_u', 'd3_d', 'd4_u', 'd4_d', 'd5_u', 'd5_d']] = 0 - pdos_atom['tot_u'] = pdos_atom[['s_u', 'p1_u', 'p2_u', 'p3_u', '2p1_u', '2p2_u', '2p3_u', 'd1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - pdos_atom['tot_d'] = pdos_atom[['s_d', 'p1_d', 'p2_d', 'p3_d', '2p1_d', '2p2_d', '2p3_d', 'd1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - - else: - pdos_atom.columns = ['Energy', 's', 'p1', 'p2', 'p3'] - pdos_atom[['d1', 'd2', 'd3', 'd4', 'd5']] = 0 - pdos_atom['tot'] = pdos_atom[['s', 'p1', 'p2', 'p3', '2p1', '2p2', '2p3', 'd1', 'd2', 'd3', 'd4', 'd5']].sum(axis=1) - - else: - if dos_info["spin_polarised"]: - if dos_info['kind'] == 'LOBSTER': - pdos_atom.columns = ['Energy', 's_u', 's_d', 'p1_u', 'p1_d', 'p2_u', 'p2_d', 'p3_u', 'p3_d', '2p1_u', '2p1_d', '2p2_u', '2p2_d', '2p3_u', '2p3_d', 'd1_u', 'd1_d', 'd2_u', 'd2_d', 'd3_u', 'd3_d', 'd4_u', 'd4_d', 'd5_u', 'd5_d'] - pdos_atom['tot_u'] = pdos_atom[['s_u', 'p1_u', 'p2_u', 'p3_u', '2p1_u', '2p2_u', '2p3_u','d1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - pdos_atom['tot_d'] = pdos_atom[['s_d', 'p1_d', 'p2_d', 'p3_d', '2p1_d', '2p2_d', '2p3_d', 'd1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - elif dos_info['kind'] == 'VASP': - pdos_atom.columns = ['Energy', 's_u', 's_d', 'p1_u', 'p1_d', 'p2_u', 'p2_d', 'p3_u', 'p3_d', 'd1_u', 'd1_d', 'd2_u', 'd2_d', 'd3_u', 'd3_d', 'd4_u', 'd4_d', 'd5_u', 'd5_d'] - pdos_atom['tot_u'] = pdos_atom[['s_u', 'p1_u', 'p2_u', 'p3_u', 'd1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - pdos_atom['tot_d'] = pdos_atom[['s_d', 'p1_d', 'p2_d', 'p3_d', 'd1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - else: - pdos_atom.columns = ['Energy', 's', 'p1', 'p2', 'p3', '2p1', '2p2', '2p3', 'd1', 'd2', 'd3', 'd4', 'd5'] - pdos_atom['tot'] = pdos_atom[['s', 'p1', 'p2', 'p3', '2p1', '2p2', '2p3', 'd1', 'd2', 'd3', 'd4', 'd5']].sum(axis=1) - - - - # Change the sign of the spin down columns if flip_down is True - if options['flip_down'] and dos_info['spin_polarised'] and not options['collapse_spin']: - down = [orbital for orbital in pdos_atom.columns if '_d' in orbital] - - for orbital in down: - pdos_atom[orbital] = -pdos_atom[orbital] - - - if options['manual_adjust']: - pdos_atom["Energy"] = pdos_atom["Energy"] - options['manual_adjust'] - - - - pdos_full.append(pdos_atom) - - - # If sum_atoms is True, all DataFrames of a given specie will be added together - if options['sum_atoms']: - - # Initalise a new emtpy list that will become our pdos_full - pdos_full_sum_atoms = [] - - - start = 0 - # Loop through each specie so that each specie will get exactly one DataFrame - for specie in species: - # Initialise with first DataFrame of the specie - atom_pdos_summed = pdos_full[start] - - # Loop over DOSes and add if there's a match for specie - for ind, pdos in enumerate(pdos_full): - if atoms[ind] == specie and ind != start: - atom_pdos_summed = atom_pdos_summed + pdos - - - # Divide the Energy by the number of DataFrames added to get back to the original value of the Energy - atom_pdos_summed["Energy"] = atom_pdos_summed["Energy"] / atoms_dict[specie] - - - if options['normalise']: - - if dos_info["spin_polarised"]: - columns = atom_pdos_summed.columns - #columns = ['s_u', 's_d', 'p1_u', 'p1_d', 'p2_u', 'p2_d', 'p3_u', 'p3_d', '2p1_u', '2p1_d', '2p2_u', '2p2_d', '2p3_u', '2p3_d', 'd1_u', 'd1_d', 'd2_u', 'd2_d', 'd3_u', 'd3_d', 'd4_u', 'd4_d', 'd5_u', 'd5_d', 'tot_u', 'tot_d'] - for column in columns: - atom_pdos_summed[column] = atom_pdos_summed[column] / atoms_dict[specie] - - - # Append the new DataFrame for a given specie to the list - pdos_full_sum_atoms.append(atom_pdos_summed) - - start += atoms_dict[specie] - - - # Rename the list - pdos_full = pdos_full_sum_atoms - - - # If collapse_spin is True for a spin polarised DOSCAR, the up and down channels of each orbital will be added together. - if options['collapse_spin'] and dos_info['spin_polarised']: - - pdos_full_spin_collapsed = [] - for pdos in pdos_full: - temp_pdos = pd.DataFrame() - - temp_pdos["Energy"] = pdos["Energy"] - - temp_pdos['s'] = pdos[['s_u', 's_d']].sum(axis=1) - - temp_pdos['p1'] = pdos[['p1_u', 'p1_d']].sum(axis=1) - temp_pdos['p2'] = pdos[['p2_u', 'p2_d']].sum(axis=1) - temp_pdos['p3'] = pdos[['p3_u', 'p3_d']].sum(axis=1) - - temp_pdos['2p1'] = pdos[['2p1_u', '2p1_d']].sum(axis=1) - temp_pdos['2p2'] = pdos[['2p2_u', '2p2_d']].sum(axis=1) - temp_pdos['2p3'] = pdos[['2p3_u', '2p3_d']].sum(axis=1) - - temp_pdos['d1'] = pdos[['d1_u', 'd1_d']].sum(axis=1) - temp_pdos['d2'] = pdos[['d2_u', 'd2_d']].sum(axis=1) - temp_pdos['d3'] = pdos[['d3_u', 'd3_d']].sum(axis=1) - temp_pdos['d4'] = pdos[['d4_u', 'd4_d']].sum(axis=1) - temp_pdos['d5'] = pdos[['d5_u', 'd5_d']].sum(axis=1) - - temp_pdos['tot'] = pdos[['tot_u', 'tot_d']].sum(axis=1) - - pdos_full_spin_collapsed.append(temp_pdos) - - - pdos_full = pdos_full_spin_collapsed - dos_info['spin_polarised'] = False - - - - # If sum_orbitals is True, all columns belonging to a particular set of orbitals will be added together. - if options['sum_orbitals']: - pdos_full_sum_orbitals = [] - - for pdos in pdos_full: - temp_pdos = pd.DataFrame() - - temp_pdos["Energy"] = pdos["Energy"] - - if dos_info['spin_polarised']: - temp_pdos['s_u'] = pdos['s_u'] - temp_pdos['s_d'] = pdos['s_d'] - - temp_pdos['p_u'] = pdos[['p1_u', 'p2_u', 'p3_u']].sum(axis=1) - temp_pdos['p_d'] = pdos[['p1_d', 'p2_d', 'p3_d']].sum(axis=1) - - if len(pdos_full[0].columns) == 25: - temp_pdos['2p_u'] = pdos[['2p1_u', '2p2_u', '2p3_u']].sum(axis=1) - temp_pdos['2p_d'] = pdos[['2p1_d', '2p2_d', '2p3_d']].sum(axis=1) - - temp_pdos['d_u'] = pdos[['d1_u', 'd2_u', 'd3_u', 'd4_u', 'd5_u']].sum(axis=1) - temp_pdos['d_d'] = pdos[['d1_d', 'd2_d', 'd3_d', 'd4_d', 'd5_d']].sum(axis=1) - - temp_pdos['tot_u'] = pdos['tot_u'] - temp_pdos['tot_d'] = pdos['tot_d'] - - else: - temp_pdos['s'] = pdos['s'] - temp_pdos['p'] = pdos[['p1', 'p2', 'p3']].sum(axis=1) - temp_pdos['2p'] = pdos[['2p1', '2p2', '2p3']].sum(axis=1) - temp_pdos['d'] = pdos[['d1', 'd2', 'd3', 'd4', 'd5']].sum(axis=1) - temp_pdos['tot'] = pdos['tot'] - - pdos_full_sum_orbitals.append(temp_pdos) - - pdos_full = pdos_full_sum_orbitals - - - - - return pdos_full, dos_info - - - - - -#def get_bader_charges(poscar='POSCAR', acf='ACF.dat'): - - - -def write_poscar(data: dict, options={}): - - required_options = ['path', 'overwrite', 'scale'] - - default_options = { - 'path': './POSCAR.vasp', - 'overwrite': False, - 'scale': 1.0 - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - if os.path.isfile(options['path']) and not options['overwrite']: - warnings.warn(f"File {options['path']} already exists, and overwrite disabled. Quitting.") - return None - - - atom_nums = count_atoms(data) - - with open(options['path'], 'w') as fout: - - # Write first line - for atom in data['atoms']: - if atom_nums[atom] > 1: - fout.write(f'{atom}{atom_nums[atom]}') - else: - fout.write(f'{atom}') - - fout.write('\t - Automatically generated by the NAFUMA Python package.\n') - - # Write second line - fout.write(str(options['scale'])+'\n') - - # Write lattice parameters - # FIXME Now only writes cells without any angles - fout.write('\t{:<09} \t {:<09} \t {:<09}\n'.format( - str(data['lattice_parameters'][0]), - str(0.0), - str(0.0), - ) - ) - - fout.write('\t{:<09} \t {:<09} \t {:<09}\n'.format( - str(0.0), - str(data['lattice_parameters'][1]), - str(0.0), - ) - ) - - fout.write('\t{:<09} \t {:<09} \t {:<09}\n'.format( - str(0.0), - str(0.0), - str(data['lattice_parameters'][2]), - ) - ) - - - # Write atoms - for atom in data['atoms']: - fout.write(f'\t{atom}') - fout.write('\n') - - for atom in data['atoms']: - fout.write(f'\t{atom_nums[atom]}') - fout.write('\n') - - fout.write('Direct\n') - - - - - for atom in data['atoms']: - for label, coords in data['coordinates'].items(): - if atom in label: - fout.write('\t{:<09} \t {:<09} \t {:<09}\n'.format( - coords[0], - coords[1], - coords[2] - ) - ) - - - - -def apply_transformation(data, rotation=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], translation=[0,0,0]): - - if not isinstance(rotation, np.ndarray): - rotation = np.array(rotation) - - if not rotation.shape == (3,3): - print("NOOOO!!!!") - - for label in data['coordinates'].keys(): - data['coordinates'][label] = rotation.dot(data['coordinates'][label]) - data['coordinates'][label] = translate_back(data['coordinates'][label]) - data['coordinates'][label] = data['coordinates'][label] + translation - - return data - - -def translate_back(coords): - - for i, coord in enumerate(coords): - if coord < 0: - while coords[i] < 0: - coords[i] = coords[i]+1 - - elif coord >= 1: - while coords[i] >= 1: - coords[i] = coords[i]-1 - - return coords - - -def count_atoms(data): - atom_nums = {} - for atom in data['atoms']: - atom_nums[atom] = 0 - - for label in data['coordinates'].keys(): - for atom in data['atoms']: - if atom in label: - atom_nums[atom] += 1 - - - return atom_nums - -def append_data(data, new_data): - - if not new_data: - return data - - if not data: - data = { - 'atoms': new_data['atoms'], - 'coordinates': {}, - 'lattice_parameters': new_data['lattice_parameters'] - } - - atom_num = count_atoms(data) - - new_coords = {} - - for label, coords in data['coordinates'].items(): - new_coords[label] = coords - - - extend_unit_cell = [0,0,0] - for label, coords in new_data['coordinates'].items(): - atom = ''.join(filter(str.isalpha, label)) - number = int(''.join(filter(str.isnumeric, label))) - new_number = number + atom_num[atom] - new_label = atom+str(new_number) - - new_coords[new_label] = coords - - for i, coord in enumerate(coords): - if coord > 1 and np.floor(coord) >= extend_unit_cell[i]: - extend_unit_cell[i] = np.floor(coord) - - data['coordinates'] = new_coords - - return data - - -def make_supercell(data, supercell): - - for i, param in enumerate(data['lattice_parameters']): - data['lattice_parameters'][i] = supercell[i] * param - - if supercell[i] > 0: - - for label, coords in data['coordinates'].items(): - data['coordinates'][label][i] = (1/supercell[i])*data['coordinates'][label][i] - - - - -def copy_data(data): - - new_data = {} - - new_data['atoms'] = list(data['atoms']) - new_data['coordinates'] = data['coordinates'].copy() - new_data['lattice_parameters'] = list(data['lattice_parameters']) - - return new_data - - -def unit_vector(vector): - """ Returns the unit vector of the vector. """ - return vector / np.linalg.norm(vector) - -def angle_between(v1, v2): - """ Returns the angle in radians between vectors 'v1' and 'v2':: - - >>> angle_between((1, 0, 0), (0, 1, 0)) - 1.5707963267948966 - >>> angle_between((1, 0, 0), (1, 0, 0)) - 0.0 - >>> angle_between((1, 0, 0), (-1, 0, 0)) - 3.141592653589793 - """ - v1_u = unit_vector(v1) - v2_u = unit_vector(v2) - return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)) \ No newline at end of file diff --git a/nafuma/dft/phonons.py b/nafuma/dft/phonons.py deleted file mode 100644 index 0f1454f..0000000 --- a/nafuma/dft/phonons.py +++ /dev/null @@ -1,1700 +0,0 @@ -import re -from this import d -import numpy as np -import matplotlib.pyplot as plt -import pandas as pd - -import subprocess -import os -import shutil - -from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) -from mpl_toolkits.axes_grid.inset_locator import (inset_axes, InsetPosition, - mark_inset) -import importlib -import matplotlib.patches as mpatches -from matplotlib.lines import Line2D -from cycler import cycler -import itertools - - -def get_atoms(path='.'): - - poscar = os.path.join(path, 'POSCAR') - - with open(poscar, 'r') as poscar: - lines = poscar.readlines() - - atoms = lines[5].split() - atom_num = lines[6].split() - - - atom_num = [int(num) for num in atom_num] - - return atoms, atom_num - -def get_dimensions(path='.'): - - poscar = os.path.join(path, 'POSCAR') - sposcar = os.path.join(path, 'SPOSCAR') - - - with open(poscar, 'r') as poscar: - - lines_pos = poscar.readlines() - - - with open(sposcar, 'r') as sposcar: - - lines_spos = sposcar.readlines() - - - - a_p, b_p, c_p = lines_pos[2].split(), lines_pos[3].split(), lines_pos[4].split() - a_s, b_s, c_s = lines_spos[2].split(), lines_spos[3].split(), lines_spos[4].split() - - lattice_params_poscar, lattice_params_sposcar = [a_p, b_p, c_p], [a_s, b_s, c_s] - - - - poscar_new = [] - sposcar_new = [] - - for lp_p, lp_s in zip(lattice_params_poscar, lattice_params_sposcar): - lp_p = np.sqrt(float(lp_p[0])**2 + float(lp_p[1])**2 + float(lp_p[2])**2) - lp_s = np.sqrt(float(lp_s[0])**2 + float(lp_s[1])**2 + float(lp_s[2])**2) - - poscar_new.append(lp_p) - sposcar_new.append(lp_s) - - - dim = [int(lp_s/lp_p) for lp_s, lp_p in zip(sposcar_new, poscar_new)] - - return dim - - -def read_band(band_dir): - ''' Reads a band file as written by the function write_phonon_bands() into a pandas DataFrame and returns this. Contains two columns: k-points (the "distance" output in the band.yaml-file by phonopy) and frequencies. - - Input: - band_dir: the path to the band-file. - - Output: - band: pandas DataFrame containing frequencies of the band along the k-point path specified in the phonopy calculation''' - - - # Read the band into a pandas DataFrame - band = pd.read_csv(band_dir, delim_whitespace=True, header=None, names=['kpt', 'frequency']) - - return band - - -def read_kpoints(kpoints_dir): - ''' Reads a VASP KPOINTS-file in line mode. Returns two lists: special_points_coords, containing the coordinates of the special points in k-space and special_points_labels, the names of these special points. - Requires a KPOINTS-file that is in line mode with special points indicated with a "!". - - Input: - kpoints_dir: the path to the KPOINTS-file - - Output: - special_points_coords: List of 3D coordinates of the k-space special points - special_points_labels: List of names of the k-space special points''' - - - # Open the KPOINTS-file and read it line by line, appending each line with a "!" to the special_points list. - special_points = [] - - with open(kpoints_dir) as kpoints: - lines = kpoints.readlines() - - for line in lines: - if '!' in line: - special_points.append(line) - - - # Go through the special points to separate them into the coordinate and the label for each special point into special_points_coords and special_points_labels respectively. - special_points_coords = [] - special_points_labels = [] - - for special_point in special_points: - if len(special_point.split()) == 5: - special_points_coords.append(special_point.split()[0:3]) - special_points_labels.append(special_point.split()[-1]) - - - return special_points_coords, special_points_labels - - - - - -def get_kpoints_ticks(band): - ''' Finds the coordinates for the special points in the 1D-projection given by phonopy (the parameter 'distance' in the band.yaml file). This is to determine the placement of labels and vertical lines in the bandstructure plot. - - Input: - band: the path to a band_XX.dat file. Should not matter which one is passed here. - - Output: - kpts_ticks: A list of coordinates corresponding to the special points.''' - - band = np.genfromtxt(band) - - kpts_ticks = [] - - # Append the first point - kpts_ticks.append(0.) - - # Go through all data points - where the x-value repeats, a k-point tick is appended to the list - for j in np.arange(np.shape(band)[0]-1): - if band[j,0]==band[j+1,0]: - kpts_ticks.append(band[j,0]) - - # Append the last point - kpts_ticks.append(max(band[:,0])) - - - return kpts_ticks - -def get_kpoints_labels(special_points_labels): - ''' Takes the raw special point labels from read_kpoints() and writes them in a way to be used in the bandstructure plots. - Where there is a discontinuity in the path, the label is separated with a |. - - Input: - special_points_labels: A list of special points as directly read from the KPOINTS-file by read_kpoints() - - Ouput: - labels: A list of labels suitable to pass as x-ticks during plotting of the bandstructure plots.''' - - - # Loop through the raw special points labels list following a set of rules, to extract the labels suitable for plotting - labels = [] - - for ind, label in enumerate(special_points_labels): - - # Add the first label as this will be a separate special point - if ind == 0: - label = '${}$'.format(label) if (label[0] == '\\') else label - labels.append(label) - - # Add the last label, as this will also be a separate special point (or will it? Must change this if that is not always the case) - elif ind == len(special_points_labels)-1: - label = '${}$'.format(label) if (label[0] == '\\') else label - labels.append(label) - - # Skip every second entry, as they will repeat due to the way the KPOINTS-file is constructed - elif ind%2 != 0: - continue - - # Add label if it's continuous (i.e. if the current and previous points are the same), add "previous|current" if discontinuous (i.e. if they are not the same) - else: - if label == special_points_labels[ind-1]: - label = '${}$'.format(label) if (label[0] == '\\') else label # If the special point has a greek letter, such as the gamma point, makes sure that the label is enclosed in $ to be rendered correctly. - labels.append(label) - - else: - label = '${}$'.format(label) if (label[0] == '\\') else label # If the special point has a greek letter, such as the gamma point, makes sure that the label is enclosed in $ to be rendered correctly. - previous_label = special_points_labels[ind-1] - previous_label = '${}$'.format(previous_label) if previous_label[0] == '\\' else previous_label # If the special point has a greek letter, such as the gamma point, makes sure that the label is enclosed in $ to be rendered correctly. - - labels.append("{}|{}".format(previous_label, label)) - - - return labels - - -def read_phonon_dos(dos_path): - ''' Reads the phonon density of states from a total_dos.dat file as written by phonopy. This file will be generated by the function calculate_phonon_dos() as well as this calls phonopy to calculate the density of states. - - Input: - dos_path: the path to the total_dos.dat file. Must include the filename - - Output: - df: pandas DataFrame containing the contents of the total_dos.dat file. Two columns, "Frequency" and "DOS". ''' - - df = pd.read_csv(dos_path, header=None, skiprows=1, delim_whitespace=True) - df.columns = ['Frequency', 'DOS'] - - return df - - -def read_phonon_pdos(path, normalise=False, poscar=None): - ''' Reads the phonon density of states from a total_dos.dat file as written by phonopy. This file will be generated by the function calculate_phonon_dos() as well as this calls phonopy to calculate the density of states. - - Input: - dos_path: the path to the total_dos.dat file. Must include the filename - - Output: - df: pandas DataFrame containing the contents of the total_dos.dat file. Two columns, "Frequency" and "DOS". ''' - - df = pd.read_csv(path, index_col=0) - - if normalise and poscar: - atoms, atom_num = get_atoms(poscar) - - - for atom, num in zip(atoms, atom_num): - df[atom] = df[atom] / num - - - return df - - -def write_phonopy_band_path(special_points_coords): - ''' Writes the band path used by phonopy to calculate the bandstructure from the raw information as extracted by read_kpoints(). - - Input: - special_points_coords: list of coordinates for the special points as read by read_kpoints(), that reads a VASP KPOINTS.bands file. - - Output: - phonopy_band_path: ''' - - coords = [] - - for ind, coord in enumerate(special_points_coords): - - # Add the first label - if ind == 0: - coord = "{} {} {} ".format(coord[0], coord[1], coord[2]) - coords.append(coord) - - # Add the last label - elif ind == len(special_points_coords)-1: - coord = "{} {} {}".format(coord[0], coord[1], coord[2]) - coords.append(coord) - - # Skip every second entry - elif ind%2 != 0: - continue - - # Add label if it's continuous, add "previous|current" if discontinuous - else: - if coord == special_points_coords[ind-1]: - coord = "{} {} {} ".format(coord[0], coord[1], coord[2]) - coords.append(coord) - - else: - first_coord = "{} {} {}".format(special_points_coords[ind-1][0], special_points_coords[ind-1][1], special_points_coords[ind-1][2]) - second_coord = "{} {} {} ".format(coord[0], coord[1], coord[2]) - coords.append("{}, {} ".format(first_coord, second_coord)) - - - phonopy_band_path = '' - - for coord in coords: - phonopy_band_path = phonopy_band_path + coord - - return phonopy_band_path - - -def write_mesh_conf(atoms, dim, mesh, dos_range=None, pdos=False, atom_num=None, tmax=None): - - atom_str = 'ATOM_NAME = ' - for atom in atoms: - atom_str += atom + " " - - dim_str = 'DIM = ' - for d in dim: - dim_str += str(d) + " " - - mesh_str = 'MP = ' - for m in mesh: - mesh_str += str(m) + " " - - dos_str = 'DOS_RANGE = ' - for d in dos_range: - dos_str += str(d) + " " - - if tmax: - tmax_str = f'TMAX = {tmax}' - - if pdos: - pdos_str = 'PDOS =' - - atoms_sum = 0 - for ind, atom in enumerate(atoms): - for i in range(1,atom_num[ind]+1): - pdos_str += " {}".format(i+atoms_sum) - - # Add comma after numbers unless it's the last entry - if ind != len(atom_num)-1: - pdos_str += ',' - - atoms_sum = atoms_sum + atom_num[ind] - - - with open('mesh.conf', 'w') as conf: - - conf.write(atom_str + '\n' + dim_str + '\n' + mesh_str) - - if tmax: - conf.write('\n' + tmax_str) - - if dos_range: - conf.write('\n' + dos_str) - - if pdos: - conf.write('\n' + pdos_str) - - conf.write('\n' + "WRITE_MESH = .FALSE.") - - -def write_band_conf(atoms, dim, mesh, band, band_points=None): - - atom_str = 'ATOM_NAME = ' - for atom in atoms: - atom_str += atom + " " - - dim_str = 'DIM = ' - for d in dim: - dim_str += str(d) + " " - - mesh_str = 'MP = ' - for m in mesh: - mesh_str += str(m) + " " - - band_str = 'BAND = ' + band - - if band_points: - band_points_str = "BAND_POINTS = " + band_points - - - with open('band.conf', 'w') as conf: - - if not band_points: - conf.write(atom_str + '\n' + dim_str + '\n' + mesh_str + '\n' + band_str) - - else: - conf.write(atom_str + '\n' + dim_str + '\n' + mesh_str + '\n' + band_str + '\n' + band_points_str) - - - -def calculate_phonon_dos(path, atoms, dim, mesh, dos_range=None): - - cwd = os.getcwd() - os.chdir(path) - - write_mesh_conf(atoms, dim, mesh, dos_range=dos_range) - - subprocess.call('phonopy -ps mesh.conf >> phonopy_output.dat', shell=True) - - - # Make folder and move output in there - os.mkdir('total_dos') - shutil.move('total_dos.pdf', 'total_dos/total_dos.pdf') - shutil.move('total_dos.dat', 'total_dos/total_dos.dat') - shutil.move('mesh.conf', 'total_dos/mesh.conf') - shutil.move('phonopy.yaml', 'total_dos/phonopy.yaml') - shutil.move('phonopy_output.dat', 'total_dos/phonopy_output.dat') - - os.chdir(cwd) - - - - - -def calculate_phonon_pdos(path, atoms, dim, mesh, dos_range=None, atom_num=None, order=None): - ''' Calculate the projected phonon DOS. Calls function to write mesh.conf file, and then cleans up the output by summing all the individual contributions per atom to the same species.''' - - cwd = os.getcwd() - os.chdir(path) - - write_mesh_conf(atoms, dim, mesh, dos_range=dos_range, pdos=True, atom_num=atom_num) - - subprocess.call('phonopy -ps mesh.conf >> phonopy_output.dat', shell=True) - - - df = pd.read_csv('projected_dos.dat', delim_whitespace=True, skiprows=1, header=None, dtype=float) - - - # Loop over the columns and add according to "atoms" and "atom_num" lists - - atoms_sum = 0 - for atom, num in zip(atoms, atom_num): - df[atom] = df[1+atoms_sum] - - for i in range(2+atoms_sum, atoms_sum+num+1): - df[atom] = df[atom] + df[i] - - - - atoms_sum += num - - - - # Remove all other columns, and rename the first column to "Frequency" - df.drop(df.iloc[:, 1:atoms_sum+1], inplace = True, axis = 1) - df.rename(columns = {0: "Frequency"}, inplace=True) - - - # If a list is passed to order, this will change the order of the atoms: - - if order: - df_temp = pd.DataFrame() - df_temp["Frequency"] = df["Frequency"] - - for atom in order: - df_temp[atom] = df[atom] - - df = df_temp - - - # Save the cleaned up DataFrame to file. - df.to_csv('projected_dos_clean.dat') - - - - # Make folder and move output in there - os.mkdir('projected_dos') - shutil.move('partial_dos.pdf', 'projected_dos/partial_dos.pdf') - shutil.move('projected_dos.dat', 'projected_dos/projected_dos.dat') - shutil.move('projected_dos_clean.dat', 'projected_dos/projected_dos_clean.dat') - shutil.move('mesh.conf', 'projected_dos/mesh.conf') - shutil.move('phonopy.yaml', 'projected_dos/phonopy.yaml') - shutil.move('phonopy_output.dat', 'projected_dos/phonopy_output.dat') - - - - os.chdir(cwd) - - - -def calculate_thermal_properties(path, atoms, dim, mesh, dos_range=None, tmax=None): - - cwd = os.getcwd() - os.chdir(path) - - write_mesh_conf(atoms, dim, mesh, dos_range=dos_range, tmax=tmax) - - subprocess.call('phonopy -t mesh.conf >> phonopy_output.dat', shell=True) - - with open('phonopy_output.dat', 'r') as f: - lines = f.readlines() - - - data = [] - for ind, line in enumerate(lines): - - if line.split(): - if "#" in line.split()[0]: - j = 1 - while lines[ind+j].split(): - data.append(lines[ind+j].split()) - j += 1 - - - - df = pd.DataFrame(data) - df.columns = ['T', 'F', 'S', 'C_v', 'E'] - - df.to_csv('thermal_properties.dat') - - - - - #Make folder and move output in there - os.mkdir('thermal_properties') - shutil.move('thermal_properties.yaml', 'thermal_properties/thermal_properties.yaml') - shutil.move('thermal_properties.dat', 'thermal_properties/thermal_properties.dat') - shutil.move('mesh.conf', 'thermal_properties/mesh.conf') - shutil.move('phonopy.yaml', 'thermal_properties/phonopy.yaml') - shutil.move('phonopy_output.dat', 'thermal_properties/phonopy_output.dat') - - - os.chdir(cwd) - -def calculate_phonon_bandstructure(path, atoms, dim, mesh, kpoints='KPOINTS.bands', band_points=None): - - cwd = os.getcwd() - os.chdir(path) - - - kpoints_coords, kpoints_labels = read_kpoints(kpoints) - - band = write_phonopy_band_path(kpoints_coords) - - write_band_conf(atoms, dim, mesh, band, band_points=band_points) - - subprocess.call('phonopy band.conf >> phonopy_output.dat', shell=True) - - write_phonon_bands() - - - os.mkdir('dispersion_relation') - - shutil.move('band.conf', 'dispersion_relation/band.conf') - shutil.move('band.yaml', 'dispersion_relation/band.yaml') - shutil.move('bands', 'dispersion_relation/bands') - shutil.move('mesh.yaml', 'dispersion_relation/mesh.yaml') - shutil.move('phonopy.yaml', 'dispersion_relation/phonopy.yaml') - shutil.move('phonopy_output.dat', 'dispersion_relation/phonopy_output.dat') - - os.chdir(cwd) - -def write_phonon_bands(band='band.yaml'): - - with open(band, 'r') as f: - lines = f.readlines() - - - kpoints = [] - frequencies = [] - - for line in lines: - if 'distance' in line: - kpoints.append(line.split()[-1]) - - if 'frequency' in line: - frequencies.append(line.split()[-1]) - - - number_of_kpoints = len(kpoints) - number_of_bands = len(frequencies) / number_of_kpoints - - if not os.path.isdir('bands'): - os.mkdir('bands') - - os.chdir('bands') - - for i in range(int(number_of_bands)): - - with open('band_{}.dat'.format(i+1), 'w') as b: - for ind, kpoint in enumerate(kpoints): - if ind == len(kpoints)-1: - b.write("{} {}".format(kpoint, frequencies[ind*int(number_of_bands)+i])) - else: - b.write("{} {}\n".format(kpoint, frequencies[ind*int(number_of_bands)+i])) - - - os.chdir('../') - - -def plot_phonon_dos(dos_path='total_dos.dat', options={}): - - - required_options = ['xlim', 'ylim', 'flip_xy', 'colours', 'palettes', 'rc_params', 'format_params'] - - - default_options = { - 'xlim': None, # x-limits - 'ylim': None, # y-limits - 'flip_xy': False, # Whether to flip what is plotted on the x- and y-axes respectively. Default is False and plots frequency along x-axis and density of states along y-axis. - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'format_params': {}, - 'rc_params': {} - } - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - fig, ax = prepare_plot(options=options) - - dos = read_phonon_dos(dos_path=dos_path) - - if not options['xlim']: - options['xlim'] = [dos["Frequency"].min(), dos["Frequency"].max()] - - if not options['ylim']: - options['ylim'] = [dos["DOS"].min(), dos["DOS"].max()*1.1] - - - if not options['colours']: - colours = generate_colours(palette=options['palette']) - else: - colours = itertools.cycle(options['colours']) - - if options['flip_xy']: - dos.plot(x='DOS', y='Frequency', ax=ax, color=colours[0]) - - else: - dos.plot(x='Frequency', y='DOS', ax=ax, color=colours[0]) - - - options['plot_kind'] = 'DOS' - fig, ax = prettify_dos_plot(fig=fig, ax=ax, options=options) - - ax.get_legend().remove() - - - return fig, ax - - - - -def prepare_plot(options={}): - - rc_params = options['rc_params'] - format_params = options['format_params'] - - required_options = ['single_column_width', 'double_column_width', 'column_type', 'width_ratio', 'aspect_ratio', 'compress_width', 'compress_height', 'upscaling_factor', 'dpi'] - - default_options = { - 'single_column_width': 8.3, - 'double_column_width': 17.1, - 'column_type': 'single', - 'width_ratio': '1:1', - 'aspect_ratio': '1:1', - 'compress_width': 1, - 'compress_height': 1, - 'upscaling_factor': 1.0, - 'dpi': 600, - } - - options = update_options(format_params, required_options, default_options) - - - # Reset run commands - plt.rcdefaults() - - # Update run commands if any is passed (will pass an empty dictionary if not passed) - update_rc_params(rc_params) - - width = determine_width(options) - height = determine_height(options, width) - width, height = scale_figure(options=options, width=width, height=height) - - fig, ax = plt.subplots(figsize=(width, height), dpi=options['dpi']) - - return fig, ax - - -def plot_phonon_pdos(path='projected_dos_clean.dat', options={}): - - - required_options = ['xlim', 'ylim', 'flip_xy', 'colours', 'palettes', 'normalise', 'poscar', 'atoms', 'rc_params', 'format_params'] - - - default_options = { - 'xlim': None, # x-limits - 'ylim': None, # y-limits - 'flip_xy': False, # Whether to flip what is plotted on the x- and y-axes respectively. Default is False and plots frequency along x-axis and density of states along y-axis. - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'normalise': False, - 'poscar': None, - 'atoms': [], - 'format_params': {}, - 'rc_params': {} - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - dos = read_phonon_pdos(path=path, normalise=options['normalise'], poscar=options['poscar']) - - fig, ax = prepare_plot(options=options) - - if not options['xlim']: - options['xlim'] = [dos["Frequency"].min(), dos["Frequency"].max()] - - if not options['ylim'] and options['atoms']: - ymin = 0 - ymax = 0 - - - for atom in options['atoms']: - if dos[atom].min() < ymin: - ymin = dos[atom].min() - - if dos[atom].max() > ymax: - ymax = dos[atom].max() - - options['ylim'] = [ymin, ymax*1.1] - - - if not options['colours']: - colours = generate_colours(palette=options['palette']) - else: - colours = itertools.cycle(options['colours']) - - for ind, atom in enumerate(options['atoms']): - - if options['flip_xy']: - dos.plot(x=atom, y='Frequency', ax=ax, color=next(colours)) - - else: - dos.plot(x='Frequency', y=atom, ax=ax, color=next(colours)) - - - options['plot_kind'] = 'PDOS' - prettify_dos_plot(fig=fig, ax=ax, options=options) - - return fig, ax - - - - -def plot_phonon_bandstructure(band_folder='bands', kpoints='KPOINTS.bands', options={}, title=None, xlim=None, ylim=None, pad_bottom=None, scale=1, square=True, width=None, height=None, dpi=None, rotation=None, xpad=None, ypad=None): - - - # Get the special points labels - kpoint_coords, kpoint_labels = read_kpoints(kpoints) - kpoint_labels = get_kpoints_labels(kpoint_labels) - - - - # Get current folder and change into the folder containing bands - cwd = os.getcwd() - os.chdir(band_folder) - - band_paths = [band for band in os.listdir() if os.path.isfile(band) and band[0:4] == 'band'] - - # Get the location of the special points along the x-axis - kpoint_ticks = get_kpoints_ticks(band_paths[0]) - - bands = [] - for band_path in band_paths: - bands.append(read_band(band_path)) - - - - fig, ax = prepare_plot(options=options) - - mod = importlib.import_module("palettable.colorbrewer.%s" % 'qualitative') - colour = getattr(mod, 'Dark2_3').mpl_colors[0] - - kpt_min = None - kpt_max = None - freq_min = None - freq_max = None - - for band in bands: - if kpt_min == None or band["kpt"].min() < kpt_min: - kpt_min = band["kpt"].min() - if kpt_max == None or band["kpt"].max() > kpt_max: - kpt_max = band["kpt"].max() - - if freq_min == None or band["frequency"].min() < freq_min: - freq_min = band["frequency"].min() - if freq_max == None or band["frequency"].max() > freq_max: - freq_max = band["frequency"].max() - - band.plot('kpt', 'frequency', ax=ax, color=colour) - - - if not xlim: - xlim = [kpt_min, kpt_max] - if not ylim: - ylim = [freq_min-freq_max*0.1, freq_max+freq_max*0.1] - - ax.get_legend().remove() - - prettify_plot(fig=fig, ax=ax, special_points_labels=kpoint_labels, special_points_coords=kpoint_ticks, xlim=xlim, ylim=ylim, title=title, pad_bottom=pad_bottom, scale=scale, rotation=rotation, xpad=xpad, ypad=ypad) - - os.chdir(cwd) - - -def prepare_plot_old(width=None, height=None, square=True, dpi=None, colour_cycle=('qualitative', 'Dark2_8'), temperatureunit='K', energyunit='eV f.u.$^{-1}$', scale=1): - - linewidth = 3*scale - axeswidth = 3*scale - - plt.rc('lines', linewidth=linewidth) - plt.rc('axes', linewidth=axeswidth) - - if square: - if not width: - width = 20 - - if not height: - height = width - - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(width, height), facecolor='w', dpi=dpi) - - return fig, ax - - -def prettify_plot(fig, ax, frequencyunit='THz', special_points_coords=None, special_points_labels=None, xlim=None, ylim=None, title=None, pad_bottom=None, scale=1, rotation=None, xpad=None, ypad=None): - - - # Set sizes of ticks, labes etc. - ticksize = 30*scale - labelsize = 30*scale - legendsize = 30*scale - titlesize = 30*scale - - linewidth = 3*scale - axeswidth = 3*scale - majorticklength = 20*scale - minorticklength = 10*scale - - - # Set labels on x- and y-axes - if ypad: - ax.set_ylabel('Frequency [{}]'.format(frequencyunit), size=labelsize, labelpad=ypad) - else: - ax.set_ylabel('Frequency [{}]'.format(frequencyunit), size=labelsize) - - - ax.set_xlabel('') - - - - - ax.tick_params(axis='y', direction='in', which='major', right=True, length=10, width=0.5) - ax.tick_params(axis='y', direction='in', which='minor', right=True, length=5, width=0.5) - - ax.tick_params(axis='x', direction='in', which='major', bottom=False) - - - - - ax.yaxis.set_major_locator(MultipleLocator(5)) - ax.yaxis.set_minor_locator(MultipleLocator(2.5)) - - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - - - # Set tick parameters - if special_points_coords: - for coord in special_points_coords: - plt.axvline(coord, color='black', linestyle='--', linewidth=0.5) - - - - plt.xticks(ticks=special_points_coords, labels=special_points_labels, rotation=rotation) - - - - if xlim: - plt.xlim(xlim) - - if ylim: - plt.ylim(ylim) - - - if title: - ax.set_title(title, size=40) - - if pad_bottom is not None: - bigax = fig.add_subplot(111) - bigax.set_facecolor([1,1,1,0]) - bigax.spines['top'].set_visible(False) - bigax.spines['bottom'].set_visible(True) - bigax.spines['left'].set_visible(False) - bigax.spines['right'].set_visible(False) - bigax.tick_params(labelcolor='w', color='w', direction='in', top=False, bottom=True, left=False, right=False, labelleft=False, pad=pad_bottom) - - if xpad: - ax.tick_params(axis='x', pad=xpad) - if ypad: - ax.tick_params(axis='y', pad=ypad) - - return fig, ax - - -def prettify_dos_plot(fig, ax, options, frequencyunit='THz', dosunit='a.u.', xlim=None, ylim=None, title=None, hide_ylabels=False, flip_xy=False, pad_bottom=None, scale=1, pdos=False, colours=None, atoms=None, xpad=None, ypad=None): - - - required_options = ['plot_kind', 'flip_xy', 'hide_x_labels', 'hide_y_labels', 'xlabel', 'ylabel', 'xunit', 'yunit', 'xlim', 'ylim', 'x_tick_locators', 'y_tick_locators', 'hide_x_ticks', 'hide_y_ticks', 'hide_x_ticklabels', 'hide_y_ticklabels', - 'colours', 'palettes', 'title', 'legend', 'legend_position', 'subplots_adjust', 'text'] - - default_options = { - 'plot_kind': 'DOS', # DOS or PDOS - 'flip_xy': False, - 'hide_x_labels': False, # Whether x labels should be hidden - 'hide_x_ticklabels': False, - 'hide_x_ticks': False, - 'hide_y_labels': False, # whether y labels should be hidden - 'hide_y_ticklabels': False, - 'hide_y_ticks': False, - 'xlabel': 'Frequency', - 'ylabel': 'DOS', - 'xunit': r'THz', # The unit of the x-values in the curve plot - 'yunit': r'a.u.', # The unit of the y-values in the curve and bar plots - 'xlim': None, - 'ylim': None, - 'x_tick_locators': [5, 2.5], # Major and minor tick locators - 'y_tick_locators': [10, 5], - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'title': None, - 'legend': True, - 'legend_position': ['upper center', (0.20, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively - 'subplots_adjust': [0.1, 0.1, 0.9, 0.9], - 'text': None - } - - - if 'plot_kind' in options.keys(): - if 'ylabel' not in options.keys(): - if options['plot_kind'] == 'DOS': - options['ylabel'] = 'Density of states' - elif options['plot_kind'] == 'PDOS': - options['ylabel'] = 'PDOS' - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - - if options['flip_xy']: - - # Switch all the x- and y-specific values - options = swap_values(dict=options, key1='xlim', key2='ylim') - options = swap_values(dict=options, key1='xunit', key2='yunit') - options = swap_values(dict=options, key1='xlabel', key2='ylabel') - options = swap_values(dict=options, key1='x_tick_locators', key2='y_tick_locators') - options = swap_values(dict=options, key1='hide_x_labels', key2='hide_y_labels') - - # Set labels on x- and y-axes - if not options['hide_y_labels']: - ax.set_ylabel(f'{options["ylabel"]} [{options["yunit"]}]') - else: - ax.set_ylabel('') - - - - if not options['hide_x_labels']: - ax.set_xlabel(f'{options["xlabel"]} [{options["xunit"]}]') - else: - ax.set_xlabel('') - - - # Hide x- and y- ticklabels - if options['hide_y_ticklabels']: - ax.tick_params(axis='y', direction='in', which='both', labelleft=False, labelright=False) - if options['hide_x_ticklabels']: - ax.tick_params(axis='x', direction='in', which='both', labelbottom=False, labeltop=False) - - - # Hide x- and y-ticks: - if options['hide_y_ticks']: - ax.tick_params(axis='y', direction='in', which='both', left=False, right=False) - if options['hide_x_ticks']: - ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False) - - - - # Set multiple locators - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0])) - ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1])) - - - # Set title - if options['title']: - ax.set_title(options['title']) - - - # Generate colours - if not options['colours']: - colours = generate_colours(palette=options['palette']) - else: - colours = itertools.cycle(options['colours']) - - - - # Create legend - - if ax.get_legend(): - ax.get_legend().remove() - - - if options['legend']: - if options['plot_kind'] == 'PDOS' and options['atoms']: - - # Create legend - patches = [] - for atom in options['atoms']: - patches.append(mpatches.Patch(color=next(colours), label=atom)) - - fig.legend(handles=patches, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False) - - - - # Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions) - plt.subplots_adjust(left=options['subplots_adjust'][0], bottom=options['subplots_adjust'][1], right=options['subplots_adjust'][2], top=options['subplots_adjust'][3]) - - - # If limits for x- and y-axes is passed, sets these. - if options['xlim'] is not None: - ax.set_xlim(options['xlim']) - - if options['ylim'] is not None: - ax.set_ylim(options['ylim']) - - - # Add custom text - if options['text']: - plt.text(x=options['text'][1][0], y=options['text'][1][1], s=options['text'][0]) - - return fig, ax - - - - - -def read_thermal_properties(path, number_of_formula_units=None, convert=True): - - kJ = 6.2415064799632E+21 - Na = 6.0221415E+23 - - thermal_properties = pd.read_csv(path, skiprows=1, index_col=0) - thermal_properties.columns = ['T', 'F', 'S', 'Cv', 'E'] - - - if convert: - thermal_properties.F = thermal_properties.F / Na * kJ - thermal_properties.S = thermal_properties.S / Na * kJ - thermal_properties.Cv = thermal_properties.Cv / Na * kJ - thermal_properties.E = thermal_properties.E / Na * kJ - - if number_of_formula_units: - thermal_properties.F = thermal_properties.F / number_of_formula_units - thermal_properties.S = thermal_properties.S / number_of_formula_units - thermal_properties.Cv = thermal_properties.Cv / number_of_formula_units - thermal_properties.E = thermal_properties.E / number_of_formula_units - - - - return thermal_properties - - - -def plot_thermal_properties(path, number_of_formula_units=None, convert=True): - - thermal_properties = read_thermal_properties(path=path, number_of_formula_units=number_of_formula_units, convert=convert) - - thermal_properties.plot(x='T', y=['F', 'S', 'Cv', 'E']) - - - - - -def get_adjusted_energies(paths, equilibrium_energies, options={}): - - - required_options = ['plot_kind', 'reference', 'number_of_formula_units', 'xlim', 'ylim', 'flip_xy', 'colours', 'palettes', 'normalise', 'poscar', 'atoms', 'rc_params', 'format_params'] - - - default_options = { - 'plot_kind': 'absolute', - 'reference': 0, - 'number_of_formula_units': None, - 'xlim': None, # x-limits - 'ylim': None, # y-limits - 'flip_xy': False, # Whether to flip what is plotted on the x- and y-axes respectively. Default is False and plots frequency along x-axis and density of states along y-axis. - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'normalise': False, - 'poscar': None, - 'atoms': [], - 'format_params': {}, - 'rc_params': {} - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - dfs = [] - - if not options['number_of_formula_units']: - options['number_of_formula_units'] = [None for i in range(len(paths))] - - for ind, path in enumerate(paths): - df = read_thermal_properties(path, options['number_of_formula_units'][ind]) - dfs.append(df) - - - for ind, df in enumerate(dfs): - df["adjusted_energy"] = equilibrium_energies[ind] + df["F"] - - - if options['plot_kind'] == 'difference': - for ind, df in enumerate(dfs): - df["reference_energy"] = dfs[options['reference']]["adjusted_energy"] - df["difference_energy"] = df["adjusted_energy"] - df["reference_energy"] - - - if options['plot_kind'] == 'relative': - for ind, df in enumerate(dfs): - df["reference_energy"] = dfs[options['reference']]["adjusted_energy"].iloc[0] - df["relative_energy"] = df["adjusted_energy"] - df["reference_energy"] - - - return dfs - - - -def find_low_energy_structures_at_extremas(dfs): - - energy_low_T = -1 - low_T_ind = -1 - energy_high_T = -1 - high_T_ind = -1 - - for ind, df in enumerate(dfs): - if low_T_ind == -1: - low_T_ind = ind - energy_low_T = df['adjusted_energy'].loc[df['T'] == df['T'].min()].values[0] - - elif df['adjusted_energy'].loc[df['T'] == df['T'].min()].values[0] < energy_low_T: - low_T_ind = ind - energy_low_T = df['adjusted_energy'].loc[df['T'] == df['T'].min()].values[0] - - if high_T_ind == -1: - high_T_ind = ind - energy_high_T = df['adjusted_energy'].loc[df['T'] == df['T'].max()].values[0] - - elif df['adjusted_energy'].loc[df['T'] == df['T'].max()].values[0] < energy_high_T: - high_T_ind = ind - energy_high_T = df['adjusted_energy'].loc[df['T'] == df['T'].max()].values[0] - - - - return [low_T_ind, high_T_ind] - -def find_intersection(dfs, ind1, ind2): - - intersection = -1 - - for T in dfs[0]['T']: - - if dfs[ind2]['adjusted_energy'].loc[dfs[ind2]['T'] == T].values[0] < dfs[ind1]['adjusted_energy'].loc[dfs[ind1]['T'] == T].values[0]: - intersection = T - break - - - return intersection - - - - - -def plot_adjusted_energies(paths, equilibrium_energies, options={}): - - - ''' This function plots the adjusted total energies of a set of structures given a set of thermal properties calculated using phonopy. - - paths: List of paths (strings) to the .csv-files with thermal properties. - equilibrium_energies: List of equilibrium energies (floats) of pristine calculations - labels: List of labels (strings) to be shown in the plot - mode: Whether to plot as a difference plot ("difference_plot") or absolute units ("absolute"). Defaults to absolute - difference_reference: Index of which structure should serve as the reference. Defaults to 0. - number_of_formula_units: List of number of formula units per unit cell (int, float) to scale the data properly. Defaults to None, meaning to scaling. - width: Width of the plot. Defaults to None, meaning standard width is used. - width: Height of the plot. Defaults to None, meaning standard height is used. - dpi: Dots per inch. Defaults to None, meaning standard dpi is used. - colour_cycle: Tuple with type of colour scheme from the colorbrewer: http://jiffyclub.github.io/palettable/colorbrewer/ - temperatureunit: The unit to plot the temperature in. Only K implemented so far. - energyunit: The unit to plot the energy in. Only eV per f.u. impleneted so far. - inset: Whether or not there should be an inset. This is not very well implemented, and may cause issues. Defaults to False. - inset_lims: The x-limits of the inset. Defaults to None, meaning it will just try to figure it out itself. - ''' - - required_options = ['plot_kind', 'reference', 'number_of_formula_units', 'labels', 'xlim', 'ylim', 'colours', 'palettes', 'linestyles', 'rc_params', 'format_params', 'inset_xlim', 'inset_ylim', 'draw_intersection_main', 'draw_intersection_inset', 'intersection_indices', 'intersection_lw'] - - - default_options = { - 'plot_kind': 'absolute', - 'reference': 0, - 'number_of_formula_units': None, - 'labels': None, - 'xlim': None, # x-limits - 'ylim': None, # y-limits - 'inset_xlim': None, - 'inset_ylim': None, - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'linestyles': ['solid', 'dotted', 'dashed'], - 'format_params': {}, - 'rc_params': {}, - 'draw_intersection_main': False, - 'draw_intersection_inset': False, - 'intersection_indices': None, - 'intersection_lw': None, - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - energy_dfs = get_adjusted_energies(paths=paths, equilibrium_energies=equilibrium_energies, options=options) - - fig, ax = prepare_plot(options=options) - - - if not options['labels']: - options['labels'] = ['_' for i in range(len(paths))] - - - if not options['colours']: - colours = generate_colours(palettes=options['palettes']) - else: - colours = itertools.cycle(options['colours']) - - - linestyles = itertools.cycle(options['linestyles']) - - for df in energy_dfs: - if options['plot_kind'] == 'difference': - df.plot(x='T', y='difference_energy', ax=ax, ls=next(linestyles), c=next(colours)) - elif options['plot_kind'] == 'relative': - df.plot(x='T', y='relative_energy', ax=ax, ls=next(linestyles), c=next(colours)) - elif options['plot_kind'] == 'absolute': - df.plot(x='T', y='adjusted_energy', ax=ax, ls=next(linestyles), c=next(colours)) - - ax.set_xlim([int(df["T"].min()), int(df["T"].max())]) - - - - - - fig, ax = prettify_thermal_plot(fig=fig, ax=ax, options=options) - - - - if options['inset_xlim']: - inset_ax = prepare_inset_axes(ax, options) - - if not options['colours']: - colours = generate_colours(palettes=options['palettes']) - else: - colours = itertools.cycle(options['colours']) - - - linestyles = itertools.cycle(options['linestyles']) - - - for df in energy_dfs: - if options['plot_kind'] =='absolute': - y = 'adjusted_energy' - elif options['plot_kind'] == 'relative': - y = 'relative_energy' - elif options['plot_kind'] == 'difference': - y = 'difference_energy' - - df.loc[(df["T"] >= options['inset_xlim'][0]) & (df["T"] <= options['inset_xlim'][1])].plot(x='T', y=y, ax=inset_ax, ls=next(linestyles), c=next(colours)) - inset_ax.set_xlim([options['inset_xlim'][0], options['inset_xlim'][1]]) - - if options['inset_ylim']: - inset_ax.set_ylim([options['inset_ylim'][0], options['inset_ylim'][1]]) - - inset_ax.get_legend().remove() - inset_ax.set_xlabel('') - - - if options['draw_intersection_main'] or options['draw_intersection_inset']: - - if not options['intersection_indices']: - options['intersection_indices'] = find_low_energy_structures_at_extremas(energy_dfs) - - if not options['intersection_lw']: - options['intersection_lw'] = plt.rcParams['lines.linewidth'] - - intersection = find_intersection(energy_dfs, options['intersection_indices'][0], options['intersection_indices'][1]) - - if options['draw_intersection_main']: - ax.axvline(x=intersection, ls='dashed', c='black', lw=options['intersection_lw']) - if options['draw_intersection_inset']: - inset_ax.axvline(x=intersection, ls='dashed', c='black', lw=options['intersection_lw']) - - - return fig, ax - - - - - -def prepare_thermal_plot(width=None, height=None, dpi=None, colour_cycle=('qualitative', 'Dark2_8'), temperatureunit='K', energyunit='eV f.u.$^{-1}$', scale=1): - - linewidth = 3*scale - axeswidth = 3*scale - - plt.rc('lines', linewidth=linewidth) - plt.rc('axes', linewidth=axeswidth) - - if not width: - width = 20 - - if not height: - height = width - - - fig = plt.figure(figsize=(width, height), facecolor='w', dpi=dpi) - ax = plt.gca() - - # Set colour cycle - mod = importlib.import_module("palettable.colorbrewer.%s" % colour_cycle[0]) - colors = getattr(mod, colour_cycle[1]).mpl_colors - ax.set_prop_cycle(cycler('color', colors)) - - return fig, ax - - - -def prettify_thermal_plot(fig, ax, options): - - required_options = ['plot_kind', 'hide_x_labels', 'hide_y_labels', 'rotation_x_ticks', 'rotation_y_ticks', 'xlabel', 'ylabel', 'xunit', 'yunit', 'xlim', 'ylim', 'x_tick_locators', 'y_tick_locators', 'hide_x_ticks', 'hide_y_ticks', 'hide_x_ticklabels', 'hide_y_ticklabels', - 'colours', 'palettes', 'title', 'legend', 'legend_position', 'subplots_adjust', 'text'] - - default_options = { - 'plot_kind': 'absolute', # absolute, relative, difference - 'hide_x_labels': False, # Whether x labels should be hidden - 'hide_x_ticklabels': False, - 'hide_x_ticks': False, - 'rotation_x_ticks': 0, - 'hide_y_labels': False, # whether y labels should be hidden - 'hide_y_ticklabels': False, - 'hide_y_ticks': False, - 'rotation_y_ticks': 0, - 'xlabel': 'Temperature', - 'ylabel': 'Energy', - 'xunit': r'K', # The unit of the x-values in the curve plot - 'yunit': r'eV f.u.$^{-1}$', # The unit of the y-values in the curve and bar plots - 'xlim': None, - 'ylim': None, - 'x_tick_locators': [100, 50], # Major and minor tick locators - 'y_tick_locators': [10, 5], - 'labels': None, - 'colours': None, - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], - 'title': None, - 'legend': True, - 'legend_position': ['upper center', (0.20, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively - 'subplots_adjust': [0.1, 0.1, 0.9, 0.9], - 'text': None - } - - - if 'plot_kind' in options.keys(): - if 'ylabel' not in options.keys(): - if options['plot_kind'] == 'absolute': - options['ylabel'] = 'Energy' - elif options['plot_kind'] == 'relative': - options['ylabel'] = 'Relative energy' - elif options['plot_kind'] == 'difference': - options['ylabel'] = 'Energy difference' - - if 'y_tick_locators' not in options.keys(): - if options['plot_kind'] == 'absolute' or options['plot_kind'] == 'relative': - options['y_tick_locators'] = [1, 0.5] - elif options['plot_kind'] == 'difference': - options['y_tick_locators'] = [0.1, 0.05] - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - # Set labels on x- and y-axes - if not options['hide_y_labels']: - ax.set_ylabel(f'{options["ylabel"]} [{options["yunit"]}]') - else: - ax.set_ylabel('') - - if not options['hide_x_labels']: - ax.set_xlabel(f'{options["xlabel"]} [{options["xunit"]}]') - else: - ax.set_xlabel('') - - - # Set multiple locators - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0])) - ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1])) - - # Hide x- and y- ticklabels - if options['hide_y_ticklabels']: - ax.tick_params(axis='y', direction='in', which='both', labelleft=False, labelright=False) - else: - plt.xticks(rotation=options['rotation_x_ticks']) - #ax.set_xticklabels(ax.get_xticks(), rotation = options['rotation_x_ticks']) - - if options['hide_x_ticklabels']: - ax.tick_params(axis='x', direction='in', which='both', labelbottom=False, labeltop=False) - else: - pass - #ax.set_yticklabels(ax.get_yticks(), rotation = options['rotation_y_ticks']) - - - # Hide x- and y-ticks: - if options['hide_y_ticks']: - ax.tick_params(axis='y', direction='in', which='both', left=False, right=False) - if options['hide_x_ticks']: - ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False) - - - - - - - # Set title - if options['title']: - ax.set_title(options['title']) - - - - - - # Create legend - - if ax.get_legend(): - ax.get_legend().remove() - - - if options['legend']: - - - # Make palette and linestyles from original parameters - if not options['colours']: - colours = generate_colours(palettes=options['palettes']) - else: - colours = itertools.cycle(options['colours']) - - - linestyles = itertools.cycle(options['linestyles']) - - # Create legend - custom_lines = [] - active_labels = [] - - for label in options['labels']: - - - # Discard next linestyle and colour if label is _ - if label == '_': - _ = next(colours) - _ = next(linestyles) - - else: - custom_lines.append(Line2D([0], [0], color=next(colours), ls=next(linestyles))) - active_labels.append(label) - - - - ax.legend(custom_lines, active_labels, frameon=False, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1]) - #fig.legend(handles=patches, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False) - - - - # Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions) - plt.subplots_adjust(left=options['subplots_adjust'][0], bottom=options['subplots_adjust'][1], right=options['subplots_adjust'][2], top=options['subplots_adjust'][3]) - - - # If limits for x- and y-axes is passed, sets these. - if options['xlim'] is not None: - ax.set_xlim(options['xlim']) - - if options['ylim'] is not None: - ax.set_ylim(options['ylim']) - - - # Add custom text - if options['text']: - plt.text(x=options['text'][1][0], y=options['text'][1][1], s=options['text'][0]) - - return fig, ax - - - -def prettify_thermal_plot_old(fig, ax, options, colour_cycle=('qualitative', 'Dark2_8'), temperatureunit='K', energyunit='eV f.u.$^{-1}$', mode='absolute', scale=1, linestyles=None, labels=None, xpad=None, ypad=None): - - # Set sizes of ticks, labes etc. - ticksize = 30*scale - labelsize = 30*scale - legendsize = 30*scale - titlesize = 30*scale - - linewidth = 3*scale - axeswidth = 3*scale - majorticklength = 20*scale - minorticklength = 10*scale - - xpad = 4 if not xpad else xpad - ypad = 4 if not ypad else ypad - - - # Set labels on x- and y-axes - ax.set_xlabel('Temperature [{}]'.format(temperatureunit), size=labelsize, labelpad=xpad) - if mode == 'absolute': - ax.set_ylabel('Total energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - elif mode == 'difference_plot': - ax.set_ylabel('Energy difference [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - - elif mode == 'relative': - ax.set_ylabel('Relative energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - - # Set tick parameters - ax.tick_params(axis='x', direction='in', which='major', top=True, length=majorticklength, width=axeswidth, pad=xpad) - ax.tick_params(axis='x', direction='in', which='minor', top=True, length=minorticklength, width=axeswidth) - - ax.tick_params(axis='y', direction='in', which='major', right=True, length=majorticklength, width=axeswidth, pad=ypad) - ax.tick_params(axis='y', direction='in', which='minor', right=True, length=minorticklength, width=axeswidth) - - - ax.xaxis.set_major_locator(MultipleLocator(100)) - ax.xaxis.set_minor_locator(MultipleLocator(50)) - - if mode == 'absolute': - ax.yaxis.set_major_locator(MultipleLocator(1)) - ax.yaxis.set_minor_locator(MultipleLocator(0.5)) - - elif mode == 'difference_plot': - ax.yaxis.set_major_locator(MultipleLocator(0.1)) - ax.yaxis.set_minor_locator(MultipleLocator(0.05)) - - - - if labels: - - custom_lines = [] - - if not linestyles: - linestyles = ['solid', 'dotted', 'dashed'] - - mod = importlib.import_module("palettable.colorbrewer.%s" % colour_cycle[0]) - palette = getattr(mod, colour_cycle[1]).mpl_colors - palette = itertools.cycle(palette) - - colours = [] - for label in labels: - colours.append(next(palette)) - - - patches = [] - for ind, label in enumerate(labels): - custom_lines.append(Line2D([0], [0], color=colours[ind], lw=linewidth, ls=linestyles[ind])) - - - ax.legend(custom_lines, labels, fontsize=labelsize, frameon=False) - - - plt.xticks(fontsize=ticksize, rotation=45) - plt.yticks(fontsize=ticksize) - - - return fig, ax - - -def prepare_inset_axes(parent_ax, options): - - default_options = { - 'hide_inset_x_labels': False, # Whether x labels should be hidden - 'hide_inset_x_ticklabels': False, - 'hide_inset_x_ticks': False, - 'rotation_inset_x_ticks': 0, - 'hide_inset_y_labels': False, # whether y labels should be hidden - 'hide_inset_y_ticklabels': False, - 'hide_inset_y_ticks': False, - 'rotation_inset_y_ticks': 0, - 'inset_x_tick_locators': [100, 50], # Major and minor tick locators - 'inset_y_tick_locators': [10, 5], - 'inset_position': [0.1,0.1,0.3,0.3], - 'legend_position': ['upper center', (0.20, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively - 'connecting_corners': [1,2] - } - - - options = update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - # Create a set of inset Axes: these should fill the bounding box allocated to - # them. - inset_ax = plt.axes([0, 0, 2, 2]) - # Manually set the position and relative size of the inset axes within ax1 - ip = InsetPosition(parent_ax, options['inset_position']) - inset_ax.set_axes_locator(ip) - - mark_inset(parent_ax, inset_ax, loc1a=2, loc2a=4, loc1b=1, loc2b=2, fc='none', ec='black') - - inset_ax.xaxis.set_major_locator(MultipleLocator(options['inset_x_tick_locators'][0])) - inset_ax.xaxis.set_minor_locator(MultipleLocator(options['inset_x_tick_locators'][1])) - - - inset_ax.yaxis.set_major_locator(MultipleLocator(options['inset_y_tick_locators'][0])) - inset_ax.yaxis.set_minor_locator(MultipleLocator(options['inset_y_tick_locators'][1])) - - - - - return inset_ax - - - -def update_rc_params(rc_params): - ''' Update all passed run commands in matplotlib''' - - if rc_params: - for key in rc_params.keys(): - plt.rcParams.update({key: rc_params[key]}) - - - -def update_options(options, required_options, default_options): - ''' Update all passed options''' - - - for option in required_options: - if option not in options.keys(): - options[option] = default_options[option] - - - - return options - - - -def determine_width(options): - - conversion_cm_inch = 0.3937008 # cm to inch - - if options['column_type'] == 'single': - column_width = options['single_column_width'] - elif options['column_type'] == 'double': - column_width = options['double_column_width'] - - column_width *= conversion_cm_inch - - - width_ratio = [float(num) for num in options['width_ratio'].split(':')] - - - width = column_width * width_ratio[0]/width_ratio[1] - - - return width - - -def determine_height(options, width): - - aspect_ratio = [float(num) for num in options['aspect_ratio'].split(':')] - - height = width/(aspect_ratio[0] / aspect_ratio[1]) - - return height - - -def scale_figure(options, width, height): - width = width * options['upscaling_factor'] * options['compress_width'] - height = height * options['upscaling_factor'] * options['compress_height'] - - return width, height - - - -def swap_values(dict, key1, key2): - - key1_val = dict[key1] - dict[key1] = dict[key2] - dict[key2] = key1_val - - return dict - - - -def generate_colours(palettes): - - # Creates a list of all the colours that is passed in the colour_cycles argument. Then makes cyclic iterables of these. - colour_collection = [] - for palette in palettes: - mod = importlib.import_module("palettable.colorbrewer.%s" % palette[0]) - colour = getattr(mod, palette[1]).mpl_colors - colour_collection = colour_collection + colour - - colour_cycle = itertools.cycle(colour_collection) - - - return colour_cycle \ No newline at end of file diff --git a/nafuma/dft/structure.py b/nafuma/dft/structure.py deleted file mode 100644 index 6558315..0000000 --- a/nafuma/dft/structure.py +++ /dev/null @@ -1,935 +0,0 @@ -import math -import re -import pandas as pd -import numpy as np -from scipy.optimize import curve_fit -import warnings - -import matplotlib.pyplot as plt -from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) -import importlib -import matplotlib.patches as mpatches -import matplotlib.lines as mlines -from mpl_toolkits.axisartist.axislines import Subplot -from cycler import cycler -import itertools - -from ase import Atoms -from ase.io.trajectory import Trajectory -from ase import io -from ase.units import kJ -from ase.eos import EquationOfState -import os -import os.path - -import nafuma.auxillary as aux -import nafuma.plotting as btp - - -def read_eos_data(path, options): - ''' Reads volume and energy data from a energy-volume run and fits the data to an equation of state. Outputs a list with one pandas DataFrame containing the data points from the DFT-calculations, - one DataFrame containing the fitted curve data points and one dictionary with equilibrium volume, equilibrium energy and bulk modulus in GPa - - path: Path to the folder containing the energ.dat and POSCAR files. energ.dat must have two columns with volumes in the first, energy in the second separated by whitespace. - atoms_per_fu: Number of atoms per formula unit. Used to scale the values to be comparable with other calculations that may have a different sized unit cell. - eos: Type of equation of state to fit to. Same keywords as the ones used in ASE, as it simply calls ASE to fit the equation of state. - ''' - - required_options = ['atoms_per_fu', 'reference', 'eos'] - - default_options = { - 'atoms_per_fu': -1, # Scaling factor to output energy per f.u. - 'reference': 0, # Whether the energy should be relative to some reference energy (typically lowest energy) - 'eos': 'birchmurnaghan', # what type of EoS curve to fit the data to. Options: murnaghan, birch, birchmurnaghan, vinet, pouriertarantola - } - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - - # Make paths for the energ.dat and POSCAR files. - energ_path = os.path.join(path, 'energ.dat') - poscar_path = os.path.join(path, 'POSCAR') - - # Read POSCAR and calculate the scale factor to give values per formula unit - at = io.read(poscar_path) - - if options['atoms_per_fu'] == -1: - scale_factor = 1 - else: - scale_factor = options['atoms_per_fu'] / len(at) - - # Get the label - label = os.path.basename(path) - - # Reads the energ.dat file and structures the data into a pandas DataFrame. Then scales the values according to the scale factor. - dft_df = pd.read_csv(energ_path, delim_whitespace=True, header=None) - dft_df.columns = ['Configuration', 'Volume', 'Energy'] - dft_df['Energy'] = dft_df['Energy'] * scale_factor - dft_df['Volume'] = dft_df['Volume'] * scale_factor - - - dft_df["Energy"] = dft_df["Energy"] - options['reference'] # subtracts a reference energy if provided. THis value defaults to 0, so will not do anything if not provided. - - # Fit data to Equation of State using ASEs EquationOfState object. Makes a DataFrame out of the data points of the fitted curve. Also makes a ditionary of the equilibrium constants, - #then packages everything in a list which is returned by the function. - eos = EquationOfState(dft_df['Volume'].values, dft_df['Energy'].values, eos=options['eos']) - - try: - v0, e0, B = eos.fit() - eos_df = pd.DataFrame(data={'Volume': eos.getplotdata()[4], 'Energy': eos.getplotdata()[5]}) - - equilibrium_constants = {'v0': v0, 'e0': e0,'B': B/kJ * 1.0e24} - - data = [dft_df, eos_df, equilibrium_constants, label] - - return data - - except: - warnings.warn(f'WARNING: Unable to fit EoS curve for {label}') - - return [None, None, None, label] - - -def read_eos_datas(path, options): - - - required_options = ['subset', 'sort_by'] - - default_options = { - 'subset': None, # list with directory names of what you want to include - 'sort_by': 'e0', # whether the data should be sorted or not - relevant for bar plots, but also for the order of the entries in the legend in the EoScruve plot - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - # If a subset of directories is not specified, will create a list of all directories in the path given. - if not options['subset']: - dirs = [dir for dir in os.listdir(path) if os.path.isdir(os.path.join(path, dir)) and dir[0] != '.'] - else: - dirs = options['subset'] - - - datas = [] - - - # Loop through all subdirectories and reads the data from these. Also appends the name of the directory to the list that is returned from the plot_eos_data() function - for dir in dirs: - subdir = os.path.join(path, dir) - data = read_eos_data(subdir, options) - - if isinstance(data[0], pd.DataFrame): - datas.append(data) - - - # Sorts the data if sort is enabled. - if options['sort_by']: - datas = sort_data(datas, options['sort_by']) - - - return datas - - -def get_summarised_data(path, options): - - datas = read_eos_datas(path=path, options=options) - - summary = [] - for data in datas: - summary.append([data[3], data[2]['e0'], data[2]['v0'], data[2]['B']]) - - df = pd.DataFrame(summary) - df.columns = ['Label', 'E0', 'V0', 'B'] - - emin = df["E0"].min() - - df["dE0"] = df["E0"] - emin - - # Rearranging the columns - df = df[['Label', 'E0', 'dE0', 'V0', 'B']] - - return df - - -def plot_eos_data(path, options): - ''' Plots the data from the energy-volume curve runs. Allows plotting of just the energy-volume curves, a bar plot showing the equilibrium energies or both. - - path: path to where the data is located. It should point to a directory with subdirectories for each structure to be plotted. Inside each of these subdirectories there should be an energ.dat and a POSCAR file. - atoms_per_fu: Number of atoms per formula unit. Used to scale the values to be comparable with other calculations that may have a different sized unit cell. - dirs: List of directory names if only a subset of all available datasets is to be plotted. Defaults to None, and will thus get data from all subdirectories. - eos: Type of equation of state to fit to. Same keywords as the ones used in ASE, as it simply calls ASE to fit the equation of state. - width: Width of the total figure. Defaults to None, which will again default to width=20. - height: Height of the total figure. Defaults to None, which will again will default to height= width / phi where phi is the golden ratio. - dpi: Dots per inch of the figure. Defaults to pyplot's default - colour_cycles: List of tuples with sets of colours for the palettable colour collection. Defaults to two sets of in total 20 colours. Used for giving different colours to energy-volume curves. - energyunit: The energy unit. Defaults to eV per formula unit. Only used on the axis labels. - volumeunit: The volume unit. Defaults to Å^3. Only used on the axis labels. - xlim: Limits of the x-axes. List of min and max. If mode = both is used, has to contain two lists for each of the plots. As the x-limits for a bar plot is nonesense, should just contain a list with a NoneType. - ylim: Limits of the y-axes. List of min and max. If mode = both is used, has to contain two lists for each of the plots. - sort: Whether or not to sort the data from lowest to highest equilibrium energy. Defaults to True. - sort_by: What to sort by if sort is enabled. Defaults to e0. Other options: v0 = equilibrium volumes, B = bulk moduli. Alphabetical order sorting is not implemented. - mode: Determines what to plot. Defaults to energy-volume curves ('curves'). Other options: 'bars', bar-plot of equilibrium energies. 'both', both energy-volume curves and bar plots are plotted side-by-side. - highlight: Takes a list, either of booleans to highlight certain bars (must be the same length as the number of data sets). Alternatively can contain only names of the datasets to highlight. Defaults to None.''' - - - - # FIXME A lot of refactoring required to tidy this up - - required_options = ['plot_kind', 'highlight', - 'reference', - 'eos', 'sort_by', - 'colours', - 'xlabel', 'ylabel', - 'xunit', 'yunit', - 'palettes', - 'markers', - 'ylim', - 'legend_map', - 'rc_params', - 'legend'] - - - default_options = { - 'plot_kind': 'EoScurve', # EoScurve or EoSbars - 'highlight': None, # list with directory names (or Boolean array) of which bars to highlight. Only relevant to EoSbars - 'reference': 0, # Whether the energy should be relative to some reference energy (typically lowest energy) - 'eos': 'birchmurnaghan', # what type of EoS curve to fit the data to. Options: murnaghan, birch, birchmurnaghan, vinet, pouriertarantola - 'sort_by': 'e0', # whether the data should be sorted or not - relevant for bar plots, but also for the order of the entries in the legend in the EoScruve plot - 'colours': None, - 'xlabel': 'Volume', 'ylabel': 'Energy', - 'xunit': 'Å$^3$', 'yunit': 'eV', - 'palettes': [('qualitative', 'Dark2_8'), ('qualitative', 'Paired_12')], # a set of two colour cycles from the palettable package. Requires many colours for the EoScurve plot - 'markers': ('o', '*', '^', 'v', 'd', 'H', '8', '>', 'P', 'X'), # marker styles for the EoScurve plot - 'ylim': None, # y-limits (ist) - 'legend': True, - 'legend_map': None, # a dictionary with mappings between the folder names and what should appear in the legend - 'rc_params': None # dictionary of run commands to update plot style - } - - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - # Create path to the data - datas = read_eos_datas(path=path, options=options) - - - ### PLOT THE ENERGY-VOLUME CURVES - if options['plot_kind'] == 'EoScurve': - - # Fetches a figure and axes object from the prepare_plot() function - fig, ax = btp.prepare_plot(options=options) - - # Make an cyclic iterable of markers to be used for the calculated data points. - marker_cycle = itertools.cycle(options['markers']) - - - # Creates a list of all the colours that is passed in the colour_cycles argument. Then makes cyclic iterables of these. - colour_collection = [] - for cycle in options['palettes']: - mod = importlib.import_module("palettable.colorbrewer.%s" % cycle[0]) - colour = getattr(mod, cycle[1]).mpl_colors - colour_collection = colour_collection + colour - - colour_cycle = itertools.cycle(colour_collection) - - labels = [] - colours = [] - markers = [] - - - # For each of the data sets, extracts the data and plots them. - for data in datas: - dft_df, eos_df, label = data[0], data[1], data[3] - - - # If ylim is passed, only plot those that have a minimum energy below the max ylim parameter - if options['ylim']: - plot = True if dft_df["Energy"].min() < options['ylim'][1] else False - else: - plot = True - - if plot: - if options['label_map']: - labels.append(options['label_map'][label]) - - colours.append(next(colour_cycle)) - markers.append(next(marker_cycle)) - - dft_df.plot.scatter(x=1, y=2, ax=ax, marker=markers[-1], color=colours[-1], s=20) - eos_df.plot(x=0, y=1, ax=ax, color=colours[-1], label='_', ls='--') - - options['labels'] = labels - - if options['legend']: - options['legend_content'] = [labels, colours, markers] - - - - ### PLOT THE BAR PLOTS - elif options['plot_kind'] == 'EoSbars': - - # Fetches a figure and axes object from the prepare_plot() function - fig, ax = btp.prepare_plot(options=options) - - e0 = [] - labels = [] - colours = [] - - # Pick out colour for highlighting (NB! These colours are not passed as arguments, but could be in future) - - bar_colours = [] - for cycle in options['palettes']: - mod = importlib.import_module("palettable.colorbrewer.%s" % cycle[0]) - bar_colours.append(getattr(mod, cycle[1]).mpl_colors[0]) - - - # Loops through the datasets, picks out equilibrium volume and labels and sets colours according to the whether the highlight option is used or not. - for data in datas: - - if options['ylim']: - plot = True if data[2]['e0'] < options['ylim'][1] else False - else: - plot = True - - if plot: - - # Adds 100 if plotting in relative mode. The bases of the bar plots are sunk by 100 during plotting - adjustment = 100 if options['reference'] != 0 else 100 - print(adjustment) - - e0.append(data[2]['e0']+adjustment) - print(e0[-1]) - labels.append(options['label_map'][data[3]]) - - if options['highlight'] is not None: - if data[3] in options['highlight']: - colours.append(bar_colours[0]) - else: - colours.append(bar_colours[1]) - - elif options['highlight'] is not None and type(options['highlight'][0] == str): - if labels[-1] in options['highlight']: - colours.append(bar_colours[0]) - else: - colours.append(bar_colours[1]) - - else: - colours.append(bar_colours[0]) - - # Makes the bar plot. - bottom = -100 if options['reference'] != 0 else 0 - plt.bar(range(len(e0)), e0, color=colours, bottom=bottom) - plt.xticks(range(len(e0)), labels, rotation=90) - - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - return datas, fig, ax - - - - -def sort_data(datas, sort_by='e0'): - ''' Bubble sort algorithm to sort the data sets''' - - l = len(datas) - - for i in range(0, l): - for j in range(0, l-i-1): - if datas[j][2]['{}'.format(sort_by)] > datas[j+1][2]['{}'.format(sort_by)]: - temp = datas[j] - datas[j] = datas[j+1] - datas[j+1] = temp - - return datas - - - -def prepare_plot(options={}): - - # Reset run commands - plt.rcdefaults() - - # Update run commands if any is passed - if 'rc_params' in options.keys(): - update_rc_params(options['rc_params']) - - - - required_options = ['single_column_width', 'double_column_width', 'column_type', 'width_ratio', 'aspect_ratio', 'compress_width', 'compress_height', 'upscaling_factor', 'dpi'] - default_options = { - 'single_column_width': 8.3, - 'double_column_width': 17.1, - 'column_type': 'single', - 'width_ratio': '1:1', - 'aspect_ratio': '1:1', - 'compress_width': 1, - 'compress_height': 1, - 'upscaling_factor': 1.0, - 'dpi': 600} - - options = update_options(options, required_options, default_options) - - width = determine_width(options) - height = determine_height(options, width) - width, height = scale_figure(options=options, width=width, height=height) - - fig, ax = plt.subplots(figsize=(width, height), dpi=options['dpi']) - - return fig, ax - - - - - - -def update_rc_params(rc_params): - ''' Update all passed run commands in matplotlib''' - - if rc_params: - for key in rc_params.keys(): - plt.rcParams.update({key: rc_params[key]}) - - - -def update_options(options, required_options, default_options): - ''' Update all passed options''' - - - for option in required_options: - if option not in options.keys(): - options[option] = default_options[option] - - - return options - - -def determine_width(options): - - conversion_cm_inch = 0.3937008 # cm to inch - - if options['column_type'] == 'single': - column_width = options['single_column_width'] - elif options['column_type'] == 'double': - column_width = options['double_column_width'] - - column_width *= conversion_cm_inch - - - width_ratio = [float(num) for num in options['width_ratio'].split(':')] - - - width = column_width * width_ratio[0]/width_ratio[1] - - - return width - - -def determine_height(options, width): - - aspect_ratio = [float(num) for num in options['aspect_ratio'].split(':')] - - height = width/(aspect_ratio[0] / aspect_ratio[1]) - - return height - -def scale_figure(options, width, height): - width = width * options['upscaling_factor'] * options['compress_width'] - height = height * options['upscaling_factor'] * options['compress_height'] - - return width, height - - - -def prepare_plot_old(width=None, height=None, dpi=None, energyunit='eV', volumeunit=r'Å$^3$', mode='curves', width_ratio=[1, 1], square=True, pad_bottom=None, scale=1, format_params=None): - '''Prepares pyplot figure and axes objects.''' - - - - linewidth = 3*scale - axeswidth = 3*scale - - plt.rc('lines', linewidth=linewidth) - plt.rc('axes', linewidth=axeswidth) - - - if square: - if not width: - width = 20 - - height = width - - - else: - if not width: - width = 20 - - - if not height: - golden_ratio = (math.sqrt(5) - 1) / 2 - height = width*golden_ratio - - - - if mode == 'curves': - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(width, height), facecolor='w', dpi=dpi) - - - if mode == 'bars': - - fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(width, height), facecolor='w', dpi=dpi) - - - if mode == 'both': - - fig, ax = plt.subplots(1, 2, figsize=(width, height), gridspec_kw={'width_ratios': width_ratio}) - - - return fig, ax - - -def prettify_plot(fig, ax, options): - '''Prepares pyplot figure and axes objects.''' - - required_options = ['plot_kind', 'hide_x_labels', 'hide_y_labels', 'xunit', 'yunit', 'legend_content', 'legend_position', 'x_tick_locators', 'y_tick_locators', 'tick_directions', 'subplots_adjust', 'xlim', 'ylim'] - - default_options = { - 'plot_kind': 'EoScurve', # EoScurve or EoSbars - 'hide_x_labels': False, # Whether x labels should be hidden - 'hide_y_labels': False, # whether y labels should be hidden - 'xunit': r'Å$^3$', # The unit of the x-values in the curve plot - 'yunit': r'eV f.u.$^{-1}$', # The unit of the y-values in the curve and bar plots - 'xlim': None, - 'ylim': None, - 'legend_content': None, - 'legend_position': ['upper center', (1.10, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively - 'x_tick_locators': [10, 5], # Major and minor tick locators - 'y_tick_locators': [.1, .05], # Major and minor tick locators - 'tick_directions': 'in', # in or out - 'subplots_adjust': [0.1, 0.1, 0.9, 0.9] - } - - options = update_options(options=options, required_options=required_options, default_options=default_options) - - - if options['plot_kind'] == 'EoScurve': - - # Set labels on x- and y-axes - ax.set_xlabel('Volume [{}]'.format(options['xunit'])) - - if not options['hide_y_labels']: - ax.set_ylabel('Energy [{}]'.format(options['yunit'])) - else: - ax.set_ylabel('') - ax.tick_params(labelleft=False) - - - ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0])) - ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1])) - - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - if ax.get_legend(): - ax.get_legend().remove() - - - if options['legend']: - labels = options['legend_content'][0] - colours = options['legend_content'][1] - markers = options['legend_content'][2] - - entries = [] - - for i in range(len(options['legend_content'][0])): - entries.append(mlines.Line2D([], [], label=labels[i], color=colours[i], marker=markers[i], linestyle='None')) - - - fig.legend(handles=entries, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False) - - - - - if options['plot_kind'] == 'EoSbars': - - if not options['hide_y_labels']: - ax.set_ylabel('Energy [{}]'.format(options['yunit'])) - - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - ax.tick_params(axis='x', which='minor', bottom=False, top=False) - - - - - # Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions) - plt.subplots_adjust(left=options['subplots_adjust'][0], bottom=options['subplots_adjust'][1], right=options['subplots_adjust'][2], top=options['subplots_adjust'][3]) - - - # If limits for x- and y-axes is passed, sets these. - if options['xlim'] is not None: - ax.set_xlim(options['xlim']) - - if options['ylim'] is not None: - ax.set_ylim(options['ylim']) - - - return fig, ax - - - -def prettify_plot_old(fig, ax, energyunit='eV', volumeunit=r'Å$^3$', mode='curves', legend_content=None, pad_bottom=None, scale=1, hide_ylabels=False, xpad=None, ypad=None): - '''Prepares pyplot figure and axes objects.''' - - # Set sizes of ticks, labes etc. - ticksize = 30*scale - labelsize = 30*scale - legendsize = 15*scale - titlesize = 30*scale - - linewidth = 3*scale - axeswidth = 3*scale - markersize = 15*scale - majorticklength = 20*scale - minorticklength = 10*scale - - xpad = 4 if not xpad else xpad - ypad = 4 if not ypad else ypad - - - if mode == 'curves': - - # Set labels on x- and y-axes - ax.set_xlabel('Volume [{}]'.format(volumeunit), size=labelsize, labelpad=xpad) - - if not hide_ylabels: - ax.set_ylabel('Energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - else: - ax.set_ylabel('') - - # Set tick parameters - ax.tick_params(axis='both', direction='in', which='major', length=majorticklength, width=axeswidth, right=True, top=True, labelsize=ticksize) - ax.tick_params(axis='both', direction='in', which='minor', length=minorticklength, width=axeswidth, right=True, top=True, labelsize=ticksize) - - ax.tick_params(axis='x', pad=xpad) - ax.tick_params(axis='y', pad=ypad) - - if hide_ylabels: - ax.tick_params(labelleft=False) - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - - ax.xaxis.set_major_locator(MultipleLocator(10)) - ax.xaxis.set_minor_locator(MultipleLocator(5)) - - ax.yaxis.set_major_locator(MultipleLocator(.1)) - ax.yaxis.set_minor_locator(MultipleLocator(.05)) - - - ax.get_legend().remove() - if legend_content: - patches = [] - labels = legend_content[0] - colours = legend_content[1] - markers = legend_content[2] - - entries = [] - - for ind, label in enumerate(legend_content[0]): - entries.append(mlines.Line2D([], [], color=colours[ind], marker=markers[ind], linestyle='None', - markersize=markersize, label=labels[ind])) - - #patches.append(mpatches.Patch(color=colours[ind], label=labels[ind])) - - - fig.legend(handles=entries, loc='upper center', bbox_to_anchor=(1.10, 0.90), fontsize=legendsize, frameon=False) - - if pad_bottom is not None: - bigax = fig.add_subplot(111) - bigax.set_facecolor([1,1,1,0]) - bigax.spines['top'].set_visible(False) - bigax.spines['bottom'].set_visible(True) - bigax.spines['left'].set_visible(False) - bigax.spines['right'].set_visible(False) - bigax.tick_params(labelcolor='w', color='w', direction='in', top=False, bottom=True, left=False, right=False, labelleft=False, pad=pad_bottom) - - if mode == 'bars': - - - ax.tick_params(axis='both', direction='in', which='major', length=majorticklength, width=axeswidth, right=True, top=True) - ax.tick_params(axis='both', direction='in', which='minor', length=minorticklength, width=axeswidth, right=True, top=True) - - if not hide_ylabels: - ax.set_ylabel('Energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - - ax.yaxis.set_major_locator(MultipleLocator(.1)) - ax.yaxis.set_minor_locator(MultipleLocator(.05)) - - ax.tick_params(axis='x', pad=xpad) - ax.tick_params(axis='y', pad=ypad) - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - if pad_bottom is not None: - bigax = fig.add_subplot(111) - bigax.set_facecolor([1,1,1,0]) - bigax.spines['top'].set_visible(False) - bigax.spines['bottom'].set_visible(True) - bigax.spines['left'].set_visible(False) - bigax.spines['right'].set_visible(False) - bigax.tick_params(labelcolor='w', color='w', direction='in', top=False, bottom=True, left=False, right=False, labelleft=False, pad=pad_bottom) - - if mode == 'both': - - # Set labels on x- and y-axes - ax[0].set_xlabel('Volume [{}]'.format(volumeunit), size=labelsize, labelpad=xpad) - ax[0].set_ylabel('Energy [{}]'.format(energyunit), size=labelsize, labelpad=ypad) - - # Set tick parameters - ax[0].tick_params(axis='both', direction='in', which='major', length=majorticklength, width=axeswidth, right=True, left=True, top=True, labelsize=ticksize) - ax[0].tick_params(axis='both', direction='in', which='minor', length=minorticklength, width=axeswidth, right=True, left=True, top=True, labelsize=ticksize) - - ax[0].tick_params(axis='x', pad=xpad) - ax[0].tick_params(axis='y', pad=ypad) - - ax[0].xaxis.set_major_locator(MultipleLocator(10)) - ax[0].xaxis.set_minor_locator(MultipleLocator(5)) - - ax[0].yaxis.set_major_locator(MultipleLocator(.1)) - ax[0].yaxis.set_minor_locator(MultipleLocator(.05)) - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - - ax[1].yaxis.set_major_locator(MultipleLocator(.2)) - ax[1].yaxis.set_minor_locator(MultipleLocator(.1)) - ax[1].yaxis.set_label_position('right') - ax[1].yaxis.tick_right() - ax[1].set_ylabel('Energy [{}]'.format(energyunit), size=labelsize, ypad=ypad) - ax[1].tick_params(axis='both', direction='in', which='major', length=majorticklength, width=axeswidth, left=True, right=True, top=True) - ax[1].tick_params(axis='both', direction='in', which='minor', length=minorticklength, width=axeswidth, left=True, right=True, top=True) - - ax[1].tick_params(axis='x', pad=xpad) - ax[1].tick_params(axis='y', pad=ypad) - - - plt.xticks(fontsize=ticksize) - plt.yticks(fontsize=ticksize) - - return fig, ax - - - -def parabola(V, a, b, c): - """parabola polynomial function - - this function is used to fit the data to get good guesses for - the equation of state fits - - a 4th order polynomial fit to get good guesses for - was not a good idea because for noisy data the fit is too wiggly - 2nd order seems to be sufficient, and guarantees a single minimum""" - - - E = (a * V**2) + (b * V) + c - - return E - - -def murnaghan(V, E0, V0, B0, BP): - 'From PRB 28,5480 (1983' - - E = E0 + ((B0 * V) / BP) * (((V0 / V)**BP) / (BP - 1) + 1) - ((V0 * B0) / (BP - 1)) - return E - - -def birch(V, E0, V0, B0, BP): - """ - From Intermetallic compounds: Principles and Practice, Vol. I: Principles - Chapter 9 pages 195-210 by M. Mehl. B. Klein, D. Papaconstantopoulos - paper downloaded from Web - - case where n=0 - """ - - E = (E0 + - 9 / 8 * B0 * V0 * ((V0 / V)**(2 / 3) - 1)**2 + - 9 / 16 * B0 * V0 * (BP - 4) * ((V0 / V)**(2 / 3) - 1)**3) - return E - - -def birchmurnaghan(V, E0, V0, B0, BP): - """ - BirchMurnaghan equation from PRB 70, 224107 - Eq. (3) in the paper. Note that there's a typo in the paper and it uses - inversed expression for eta. - """ - - eta = (V0 / V)**(1 / 3) - - E = E0 + 9 * B0 * V0 / 16 * (eta**2 - 1)**2 * (6 + BP * (eta**2 - 1) - 4 * eta**2) - - return E - - -def vinet(V, E0, V0, B0, BP): - 'Vinet equation from PRB 70, 224107' - - eta = (V / V0)**(1 / 3) - - E = (E0 + 2 * B0 * V0 / (BP - 1)**2 * - (2 - (5 + 3 * BP * (eta - 1) - 3 * eta) * - np.exp(-3 * (BP - 1) * (eta - 1) / 2))) - - return E - -def pouriertarantola(V, E0, V0, B0, BP): - 'Pourier-Tarantola equation from PRB 70, 224107' - - eta = (V / V0)**(1 / 3) - squiggle = -3 * np.log(eta) - - E = E0 + B0 * V0 * squiggle**2 / 6 * (3 + squiggle * (BP - 2)) - return E - - - -def get_initial_guesses(volume, energy): - - p = np.polyfit(volume, energy, deg=2) - - a, b, c = p[0], p[1], p[2] - - # Estimated from dE/dV = 2aV0 + b => V0 = -b / 2a - v0 = -b / (2*a) - - # Estimated by evaluating a parabola with a, b and c values at V = V0 - e0 = parabola(v0, a, b, c) - - # Estimated form B0 ~ V0 * d^2E / dV^2. d^2E / dV^2 = 2a. - b0 = 2 * a * v0 - - # Just a reasonable starting value - bp = 4 - - - return [e0, v0, b0, bp] - - - -def fit_eos_curve(volume, energy, p0, eos): - - eos_dict = {'murnaghan': murnaghan, 'birch': birch, 'birchmurnaghan': birchmurnaghan, 'vinet': vinet, 'pouriertarantola': pouriertarantola} - - func = eos_dict[eos] - - popt, pcov = curve_fit(func, volume, energy, p0) - - E0, V0, B0, BP = popt[0], popt[1], popt[2], popt[3] - - return [E0, V0, B0, BP] - - - - -def get_plotdata(volume, energy, equilibrium_values, eos): - - eos_dict = {'murnaghan': murnaghan, 'birch': birch, 'birchmurnaghan': birchmurnaghan, 'vinet': vinet, 'pouriertarantola': pouriertarantola} - - V = np.linspace(volume.min(), volume.max(), 100) - - E0, V0, B0, BP = equilibrium_values[0], equilibrium_values[1], equilibrium_values[2], equilibrium_values[3] - - print(E0, V0, B0, BP) - - func = eos_dict[eos] - - print(func) - - E = func(V, E0, V0, B0, BP) - - return E, V - - -def get_atoms(poscar): - - with open(poscar, 'r') as poscar: - lines = poscar.readlines() - - atoms = lines[5].split() - atom_num = lines[6].split() - - - atom_num = [int(num) for num in atom_num] - - atoms_dict = {} - - for ind, atom in enumerate(atoms): - atoms_dict[atom] = atom_num[ind] - - return atoms, atom_num, atoms_dict - - - -def get_equilibrium_data(path, atoms_per_formula_unit, eos=None): - - - if not eos: - eos = 'murnaghan' - - - dirs = [os.path.join(path, dir) for dir in os.listdir(path)] - - - - data = [] - - for dir in dirs: - atoms, atom_num, atoms_dict = get_atoms(os.path.join(dir, 'POSCAR')) - scaling_factor = sum(atom_num) / atoms_per_formula_unit - - label = os.path.basename(dir) - - dft_df = pd.read_csv(os.path.join(dir, 'energ.dat'), header=None, delim_whitespace=True, index_col=0) - dft_df.reset_index(drop=True, inplace=True) - dft_df.columns = ['Volume', 'Energy'] - - volume = dft_df["Volume"].to_numpy() / scaling_factor - energy = dft_df["Energy"].to_numpy() / scaling_factor - - p0 = get_initial_guesses(volume, energy) - - try: - equilibrium_constants = fit_eos_curve(volume, energy, p0, eos) - - e0, v0, b0, bp = equilibrium_constants[0], equilibrium_constants[1], equilibrium_constants[2], equilibrium_constants[3] - - data.append([label, e0, v0, b0/kJ*1e24, bp]) - - except: - data.append([label, None, None, None, None]) - - - df = pd.DataFrame(data) - df.columns = ['Label', 'E0', 'V0', 'B0', 'Bp'] - df.sort_values(by='E0', ascending=True, inplace=True) - df.reset_index(inplace=True) - - E_min = df['E0'].min() - - df['dE'] = df['E0'] - E_min - - df = df[['Label', 'E0', 'dE', 'V0', 'B0', 'Bp']] - - - return df - - - - diff --git a/nafuma/eds/__init__.py b/nafuma/eds/__init__.py deleted file mode 100644 index e0e052c..0000000 --- a/nafuma/eds/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import io, plot \ No newline at end of file diff --git a/nafuma/eds/io.py b/nafuma/eds/io.py deleted file mode 100644 index 91c8c51..0000000 --- a/nafuma/eds/io.py +++ /dev/null @@ -1,152 +0,0 @@ -from PIL import Image -import numpy as np -import cv2 -import pandas as pd - -def read_image(path, weight=None, colour=None, crop=None, resize=None, brightness=None): - - img = np.array(Image.open(path)) - - if colour is not None: - img = change_colour(img, colour) - - if brightness is not None: - img = increase_brightness(img, increase=brightness) - - if crop is not None: - img = crop_image(img, crop) - - if resize is not None: - img = resize_image(img, resize) - - if weight is not None: - img = scale_image(img, weight) - - return img - - -def scale_image(image, factor): - - for i in range(0,image.shape[0]): - for j in range(0, image.shape[1]): - image[i][j][0] = image[i][j][0]*factor - image[i][j][1] = image[i][j][1]*factor - image[i][j][2] = image[i][j][2]*factor - - return image - - -def crop_image(image, factor): - - y, x = image.shape[0:2] - - new_y, new_x = int(y*factor), int(x*factor) - - image = image[:new_y, :new_x] - - res = cv2.resize(image, dsize=(x, y), interpolation=cv2.INTER_CUBIC) - - return res - - -def resize_image(image, factor): - - y, x = image.shape[0:2] - - new_y, new_x = int(y*factor), int(x*factor) - - res = cv2.resize(image, dsize=(new_x, new_y), interpolation=cv2.INTER_CUBIC) - - return res - - -def increase_brightness(image, brightness): - - for i in range(0,image.shape[0]): - for j in range(0, image.shape[1]): - image[i][j][0] = image[i][j][0]+brightness - image[i][j][1] = image[i][j][1]+brightness - image[i][j][2] = image[i][j][2]+brightness - - - return image - - -def add_images(image1, image2): - - assert image1.shape == image2.shape - - compound_image = np.zeros((image1.shape[0], image1.shape[1], image1.shape[2])) - for i in range(image1.shape[0]): - for j in range(image1.shape[1]): - compound_image[i][j] = [0, 0, 0] - - compound_image[i][j][0] = int(int(image1[i][j][0]) + int(image2[i][j][0])) - compound_image[i][j][1] = int(int(image1[i][j][1]) + int(image2[i][j][1])) - compound_image[i][j][2] = int(int(image1[i][j][2]) + int(image2[i][j][2])) - - - - return compound_image - - - - -def get_colour(image): - - - colour = [0, 0, 0] - for i in range(image.shape[0]): - for j in range(image.shape[1]): - if image[i][j][0] > colour[0]: - colour[0] = image[i][j][0] - - if image[i][j][1] > colour[1]: - colour[1] = image[i][j][1] - - if image[i][j][2] > colour[2]: - colour[2] = image[i][j][2] - - colour = np.array(colour) - - return colour - - -def change_colour(image, new_colour): - - new_colour = np.array(new_colour) - - old_colour = get_colour(image) - - - for i in range(image.shape[0]): - for j in range(image.shape[1]): - factor = max(image[i][j]) / max(old_colour) - image[i][j] = new_colour.astype(float) * factor - - - return image - - -def read_spectrum(path): - - headers = find_start(path) - - spectrum = pd.read_csv(path, skiprows=headers, delim_whitespace=True) - - - return spectrum - - - -def find_start(path): - - with open(path, 'r') as f: - line = f.readline() - i = 0 - while not line.startswith('Energy'): - line = f.readline() - i += 1 - - return i - diff --git a/nafuma/eds/plot.py b/nafuma/eds/plot.py deleted file mode 100644 index be8ee39..0000000 --- a/nafuma/eds/plot.py +++ /dev/null @@ -1,135 +0,0 @@ -import nafuma.auxillary as aux -import nafuma.plotting as btp -import nafuma.eds.io as io - -import numpy as np - -def show_image(data, options={}): - - - default_options = { - 'hide_x_labels': True, - 'hide_y_labels': True, - 'hide_x_ticklabels': True, - 'hide_y_ticklabels': True, - 'hide_x_ticks': True, - 'hide_y_ticks': True, - 'colours': None, - 'brightness': None, - 'show_image': True, - 'resize': None, - 'crop': None, - 'ax': None, - 'fig': None, - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - - if not 'image' in data.keys(): - - data['image'] = [None for _ in range(len(data['path']))] - - if not 'weights' in data.keys(): - data['weights'] = [1.0 for _ in range(len(data['path']))] - - if not options['colours']: - options['colours'] = [None for _ in range(len(data['path']))] - - for i, (path, weight, colour) in enumerate(zip(data['path'], data['weights'], options['colours'])): - data['image'][i] = io.read_image(path=path, weight=weight, colour=colour, resize=options['resize'], crop=options['crop']) - - - images = [] - for i, image in enumerate(data['image']): - images.append(image) -# - final_image = np.mean(images, axis=0) / 255 - if options['brightness']: - final_image = io.increase_brightness(final_image, brightness=options['brightness']) - - if len(data['path']) > 1: - data['image'].append(final_image) - - - if options['show_image']: - if not options['fig'] and not options['ax']: - fig, ax = btp.prepare_plot(options) - else: - fig, ax = options['fig'], options['ax'] - - ax.imshow(final_image) - btp.adjust_plot(fig=fig, ax=ax, options=options) - - return data['image'], fig, ax - - else: - return data['image'], None, None - - - -def plot_spectrum(data: dict, options={}): - - default_options = { - 'deconvolutions': None, - 'lines': None, - 'colours': None, - 'xlabel': 'Energy', 'xunit': 'keV', 'xlim': None, - 'ylabel': 'Counts', 'yunit': 'arb. u.', 'ylim': None, 'hide_y_ticklabels': True, 'hide_y_ticks': True, - } - - options = aux.update_options(options=options, default_options=default_options) - - fig, ax = btp.prepare_plot(options=options) - - - spectrum = io.read_spectrum(data['path']) - - if options['deconvolutions']: - - deconvolutions = [] - if not isinstance(options['deconvolutions'], list): - options['deconvolutions'] = [options['deconvolutions']] - - if options['colours'] and (len(options['colours']) != len(options['deconvolutions'])): - options['colours'] = None - - for deconv in options['deconvolutions']: - df = io.read_spectrum(deconv) - deconvolutions.append(df) - - - - spectrum.plot(x='Energy', y='Counts', ax=ax, color='black') - - if options['deconvolutions']: - if options['colours']: - for deconv, colour in zip(deconvolutions, options['colours']): - ax.fill_between(x=deconv['Energy'], y1=deconv['Counts'], y2=0, color=colour, alpha=0.4) - else: - for deconv in deconvolutions: - ax.fill_between(x=deconv['Energy'], y1=deconv['Counts'], y2=0, alpha=0.4) - - - if not options['xlim']: - options['xlim'] = [spectrum['Energy'].min(), spectrum['Energy'].max()] - - if not options['ylim']: - options['ylim'] = [0, 1.1*spectrum['Counts'].max()] - - if options['lines']: - for i, (line, energy) in enumerate(options['lines'].items()): - ax.axvline(x=energy, ls='--', lw=0.5, c='black') - ax.text(s=line, x=energy, y=(0.9-0.1*i)*options['ylim'][1], fontsize=8) - - - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - - return spectrum, fig, ax \ No newline at end of file diff --git a/nafuma/electrochemistry/__init__.py b/nafuma/electrochemistry/__init__.py deleted file mode 100644 index 0270f1d..0000000 --- a/nafuma/electrochemistry/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import io, plot, unit_tables diff --git a/nafuma/electrochemistry/io.py b/nafuma/electrochemistry/io.py deleted file mode 100644 index 884cc39..0000000 --- a/nafuma/electrochemistry/io.py +++ /dev/null @@ -1,871 +0,0 @@ -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import os - -import nafuma.auxillary as aux -from sympy import re - - -# FIXME This is not good practice, but a temporary fix as I don't have time to understand what causes the SettingWithCopyWarning. -# Read this: https://www.dataquest.io/blog/settingwithcopywarning/ -pd.set_option('mode.chained_assignment', None) - - -def read_data(data, options={}): - - if data['kind'] == 'neware': - df = read_neware(data['path'], options=options) - cycles = process_neware_data(df=df, options=options) - - elif data['kind'] == 'batsmall': - df = read_batsmall(data['path']) - cycles = process_batsmall_data(df=df, options=options) - - elif data['kind'] == 'biologic': - df = read_biologic(data['path']) - cycles = process_biologic_data(df=df, options=options) - - return cycles - - - -def read_neware(path, options={}): - ''' Reads electrochemistry data, currently only from the Neware battery cycler. Will convert to .csv if the filetype is .xlsx, - which is the file format the Neware provides for the backup data. In this case it matters if summary is False or not. If file - type is .csv, it will just open the datafile and it does not matter if summary is False or not.''' - from xlsx2csv import Xlsx2csv - - # FIXME Do a check if a .csv-file already exists even if the .xlsx is passed - - # Convert from .xlsx to .csv to make readtime faster - if path.endswith('xlsx'): - csv_details = ''.join(path[:-5]) + '_details.csv' - csv_summary = os.path.abspath(''.join(path[:-5]) + '_summary.csv') - - if not os.path.isfile(csv_summary): - Xlsx2csv(path, outputencoding="utf-8").convert(os.path.abspath(csv_summary), sheetid=3) - - if not os.path.isfile(csv_details): - Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4) - - if options['summary']: - df = pd.read_csv(csv_summary) - else: - df = pd.read_csv(csv_details) - - elif path.endswith('csv'): - df = pd.read_csv(path) - - return df - - -def read_batsmall(path): - ''' Reads BATSMALL-data into a DataFrame. - - Input: - path (required): string with path to datafile - - Output: - df: pandas DataFrame containing the data as-is, but without additional NaN-columns.''' - - - # Determine if decimal point is . or , - with open(path, 'r') as f: - for i, line in enumerate(f): - if i == 10: - values = line.split() - if len(values[1].split('.')) == 2: - decimal_point = '.' - elif len(values[1].split(',')) == 2: - decimal_point = ',' - - - - df = pd.read_csv(path, skiprows=2, sep='\t', decimal=decimal_point) - df = df.loc[:, ~df.columns.str.contains('^Unnamed')] - - return df - - -def read_biologic(path): - ''' Reads Bio-Logic-data into a DataFrame. - - Input: - path (required): string with path to datafile - - Output: - df: pandas DataFrame containing the data as-is, but without additional NaN-columns.''' - - with open(path, 'rb') as f: - lines = f.readlines() - - header_lines = int(lines[1].split()[-1]) - 1 - - - df = pd.read_csv(path, sep='\t', skiprows=header_lines, encoding='cp1252') - df.dropna(inplace=True, axis=1) - - return df - - - -def process_batsmall_data(df, options=None): - ''' Takes BATSMALL-data in the form of a DataFrame and cleans the data up and converts units into desired units. - Splits up into individual charge and discharge DataFrames per cycle, and outputs a list where each element is a tuple with the Chg and DChg-data. E.g. cycles[10][0] gives the charge data for the 11th cycle. - - For this to work, the cycling program must be set to use the counter. - - Input: - df (required): A pandas DataFrame containing BATSMALL-data, as obtained from read_batsmall(). - t (optional): Unit for time data. Defaults to ms. - C (optional): Unit for specific capacity. Defaults to mAh/g. - I (optional): Unit for current. Defaults mA. - U (optional): Unit for voltage. Defaults to V. - - Output: - cycles: A list with - ''' - - default_options = { - 'splice_cycles': False, - 'append': False, # Add max of ions and specific_capacity of previous run #TODO Generalise - 'append_gap': 0, # Add a gap between cyclces - only used if append == True. - 'molecular_weight': None, - 'reverse_discharge': False, - 'units': None, - } - - - aux.update_options(options=options, default_options=default_options) - options['kind'] = 'batsmall' - - # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. - set_units(options) - options['old_units'] = get_old_units(df, options) - - - df = add_columns(df=df, options=options) # adds columns to the DataFrame if active material weight and/or molecular weight has been passed in options - - df = unit_conversion(df=df, options=options) - - - if options['splice_cycles']: - df = splice_cycles(df=df, options=options) - - - # Replace NaN with empty string in the Comment-column and then remove all steps where the program changes - this is due to inconsistent values for current - df[["comment"]] = df[["comment"]].fillna(value={'comment': ''}) - df = df[df["comment"].str.contains("program")==False] - - # Creates masks for charge and discharge curves - chg_mask = df['current'] >= 0 - dchg_mask = df['current'] < 0 - - # Initiate cycles list - cycles = [] - - # Loop through all the cycling steps, change the current and capacities in the - for i in range(df["count"].max()): - - sub_df = df.loc[df['count'] == i+1].copy() - - sub_df.loc[dchg_mask, 'current'] *= -1 - sub_df.loc[dchg_mask, 'specific_capacity'] *= -1 - sub_df.loc[dchg_mask, 'ions'] *= -1 - - - - chg_df = sub_df.loc[chg_mask] - dchg_df = sub_df.loc[dchg_mask] - - # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) - if chg_df.empty and dchg_df.empty: - continue - - if options['append']: - if cycles: - chg_df.loc[chg_mask, 'ions'] += cycles[-1][1]['ions'].max() + options['append_gap'] - - dchg_df.loc[dchg_mask, 'ions'] += chg_df['ions'].max() + options['append_gap'] - - if options['reverse_discharge']: - max_capacity = dchg_df['capacity'].max() - dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) - - if 'specific_capacity' in df.columns: - max_capacity = dchg_df['specific_capacity'].max() - dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) - - if 'ions' in df.columns: - max_capacity = dchg_df['ions'].max() - dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) - - cycles.append((chg_df, dchg_df)) - - - return cycles - - -def splice_cycles(df, options: dict) -> pd.DataFrame: - ''' Splices two cycles together - if e.g. one charge cycle are split into several cycles due to change in parameters. - - Incomplete, only accomodates BatSmall so far, and only for charge.''' - - if options['kind'] == 'batsmall': - - # Creates masks for charge and discharge curves - chg_mask = df['current'] >= 0 - - # Loop through all the cycling steps, change the current and capacities in the - for i in range(df["count"].max()): - sub_df = df.loc[df['count'] == i+1] - sub_df_chg = sub_df.loc[chg_mask] - - # get indices where the program changed - chg_indices = sub_df_chg[sub_df_chg["comment"].str.contains("program")==True].index.to_list() - - # Delete first item if first cycle after rest (this will just be the start of the cycling) - if i+1 == 1: - del chg_indices[0] - - - if chg_indices: - last_chg = chg_indices.pop() - - if chg_indices: - for i in chg_indices: - add = df['specific_capacity'].iloc[i-1] - df['specific_capacity'].iloc[i:last_chg] = df['specific_capacity'].iloc[i:last_chg] + add - - - if options['kind'] == 'neware': - - if options['summary']: - for i in range(df['cycle'].max()): - sub_df = df.loc[df['cycle'] == i+1].copy() - - if sub_df['status'].loc[sub_df['status'] == 'CC Chg'].count() > 1: - indices = sub_df.index[sub_df['status'] == 'CC Chg'] - - add_columns = ['capacity', 'specific_capacity', 'ions', 'energy', 'cycle_time'] - - for column in add_columns: - if column in df.columns: - df[column].iloc[indices[-1]] = df[column].iloc[indices[-1]] + df[column].iloc[indices[0]] - - df.drop(index=indices[0], inplace=True) - df.reset_index(inplace=True, drop=True) - - else: - for i in range(df['cycle'].max()): - sub_df = df.loc[df['cycle'] == i+1].copy() - sub_chg_df = sub_df.loc[sub_df['status'] == 'CC Chg'].copy() - - steps_indices = sub_chg_df['steps'].unique() - - if len(steps_indices) > 1: - - add_columns = ['capacity', 'specific_capacity', 'ions', 'energy', 'cycle_time'] - - for column in add_columns: - if column in df.columns: - # Extract the maximum value from the first of the two cycles by accessing the column value of the highest index of the first cycle - add = df[column].iloc[df.loc[df['steps'] == steps_indices[0]].index.max()] - - df[column].loc[df['steps'] == steps_indices[1]] += add - - return df - - - - -def process_neware_data(df, options={}): - - """ Takes data from NEWARE in a DataFrame as read by read_neware() and converts units, adds columns and splits into cycles. - - Input: - df: pandas DataFrame containing NEWARE data as read by read_neware() - units: dictionary containing the desired units. keywords: capacity, current, voltage, mass, energy, time - splice_cycles: tuple containing index of cycles that should be spliced. Specifically designed to add two charge steps during the formation cycle with two different max voltages - active_materiale_weight: weight of the active material (in mg) used in the cell. - molecular_weight: the molar mass (in g mol^-1) of the active material, to calculate the number of ions extracted. Assumes one electron per Li+/Na+-ion """ - - required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles', 'increment_cycles_from', 'delete_datapoints'] - - default_options = { - 'units': None, - 'active_material_weight': None, - 'molecular_weight': None, - 'reverse_discharge': False, - 'splice_cycles': None, - 'increment_cycles_from': None,# index - 'delete_datapoints': None, # list of indices - } - - - aux.update_options(options=options, required_options=required_options, default_options=default_options) - options['kind'] = 'neware' - - - if not options['summary']: - # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. - set_units(options=options) # sets options['units'] - options['old_units'] = get_old_units(df=df, options=options) - - df = add_columns(df=df, options=options) # adds columns to the DataFrame if active material weight and/or molecular weight has been passed in options - - df = unit_conversion(df=df, options=options) # converts all units from the old units to the desired units - - if options['increment_cycles_from']: - df['cycle'].iloc[options['increment_cycles_from']:] += 1 - - if options['delete_datapoints']: - for datapoint in options['delete_datapoints']: - df.drop(index=datapoint, inplace=True) - - if options['splice_cycles']: - df = splice_cycles(df=df, options=options) - - - # Creates masks for charge and discharge curves - chg_mask = df['status'] == 'CC Chg' - dchg_mask = df['status'] == 'CC DChg' - - # Initiate cycles list - cycles = [] - - - - # Loop through all the cycling steps, change the current and capacities in the - for i in range(df["cycle"].max()): - - sub_df = df.loc[df['cycle'] == i+1].copy() - - chg_df = sub_df.loc[chg_mask] - dchg_df = sub_df.loc[dchg_mask] - - # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) - if chg_df.empty and dchg_df.empty: - continue - - - # Reverses the discharge curve if specified - if options['reverse_discharge']: - max_capacity = dchg_df['capacity'].max() - dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) - - if 'specific_capacity' in df.columns: - max_capacity = dchg_df['specific_capacity'].max() - dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) - - if 'ions' in df.columns: - max_capacity = dchg_df['ions'].max() - dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) - - - if not chg_df.empty: - chg_df.reset_index(inplace=True) - if not dchg_df.empty: - dchg_df.reset_index(inplace=True) - - cycles.append((chg_df, dchg_df)) - - - return cycles - - - elif options['summary']: - set_units(options=options) - options['old_units'] = get_old_units(df=df, options=options) - - df = add_columns(df=df, options=options) - df = unit_conversion(df=df, options=options) - - - if options['splice_cycles']: - df = splice_cycles(df=df, options=options) - - - chg_df = df.loc[df['status'] == 'CC Chg'] - chg_df.reset_index(inplace=True) - dchg_df = df.loc[df['status'] == 'CC DChg'] - dchg_df.reset_index(inplace=True) - - # Construct new DataFrame - new_df = pd.DataFrame(chg_df["cycle"]) - new_df.insert(1,'charge_capacity',chg_df['capacity']) - new_df.insert(1,'charge_specific_capacity',chg_df['specific_capacity']) - new_df.insert(1,'discharge_capacity',dchg_df['capacity']) - new_df.insert(1,'discharge_specific_capacity',dchg_df['specific_capacity']) - new_df.insert(1,'charge_energy',chg_df['energy']) - new_df.insert(1,'charge_specific_energy',chg_df['specific_energy']) - new_df.insert(1,'discharge_energy',dchg_df['energy']) - new_df.insert(1,'discharge_specific_energy',dchg_df['specific_energy']) - - new_df = calculate_efficiency(df=new_df, options=options) - - return new_df - - - -def process_biologic_data(df, options=None): - - required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles'] - - default_options = { - 'units': None, - 'active_material_weight': None, - 'molecular_weight': None, - 'reverse_discharge': False, - 'splice_cycles': None} - - - # Check if the DataFrame contains GC or CV data. - # FIXME This might not be a very rigorous method of checking. E.g. Rest has mode == 3, so if loading a short GC with many Rest-datapoints, the mean will be 2 and it will be treated as CV. For now manual override is sufficient - if not 'mode' in options.keys(): - options['mode'] = 'GC' if int(df['mode'].mean()) == 1 else 'CV' - - aux.update_options(options=options, required_options=required_options, default_options=default_options) - options['kind'] = 'biologic' - - # Pick out necessary columns - headers = [ - 'Ns changes', 'Ns', 'time/s', 'Ewe/V', 'Energy charge/W.h', 'Energy discharge/W.h', '/mA', 'Capacity/mA.h', 'cycle number' ] if options['mode'] == 'GC' else [ - 'ox/red', 'time/s', 'control/V', 'Ewe/V', '/mA', 'cycle number', '(Q-Qo)/C', 'P/W' - ] - - - df = df[headers].copy() - - - # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. - set_units(options) - options['old_units'] = get_old_units(df=df, options=options) - - df = add_columns(df=df, options=options) - - - - df = unit_conversion(df=df, options=options) - - # Creates masks for charge and discharge curves - if options['mode'] == 'GC': - chg_mask = (df['status'] == 1) & (df['status_change'] != 1) - dchg_mask = (df['status'] == 2) & (df['status_change'] != 1) - - elif options['mode'] == 'CV': - chg_mask = (df['status'] == 1) # oxidation - dchg_mask = (df['status'] == 0) # reduction - - # Initiate cycles list - cycles = [] - - if df['cycle'].max() == 0: - no_cycles = 1 - else: - no_cycles = int(df['cycle'].max()) - - # Loop through all the cycling steps, change the current and capacities in the - for i in range(no_cycles): - - sub_df = df.loc[df['cycle'] == i].copy() - - #sub_df.loc[dchg_mask, 'current'] *= -1 - #sub_df.loc[dchg_mask, 'capacity'] *= -1 - - chg_df = sub_df.loc[chg_mask] - dchg_df = sub_df.loc[dchg_mask] - - # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) - if chg_df.empty and dchg_df.empty: - continue - - if options['mode'] == 'GC' and options['reverse_discharge']: - max_capacity = dchg_df['capacity'].max() - dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) - - if 'specific_capacity' in df.columns: - max_capacity = dchg_df['specific_capacity'].max() - dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) - - if 'ions' in df.columns: - max_capacity = dchg_df['ions'].max() - dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) - - - if options['mode'] == 'CV': - chg_df = chg_df.sort_values(by='voltage').reset_index(drop=True) - dchg_df = dchg_df.sort_values(by='voltage', ascending=False).reset_index(drop=True) - - cycles.append((chg_df, dchg_df)) - - - return cycles - - -def add_columns(df, options): - from . import unit_tables - - if options['kind'] == 'neware': - - if options['summary']: - df[f'Energy({options["old_units"]["energy"]})'] = np.abs(df[f'Net discharge energy({options["old_units"]["energy"]})']) - - if options['active_material_weight']: - df[f"SpecificCapacity({options['old_units']['capacity']}/mg)"] = df["Capacity({})".format(options['old_units']['capacity'])] / (options['active_material_weight']) - df[f"SpecificEnergy({options['old_units']['energy']}/mg)"] = df["Energy({})".format(options['old_units']['energy'])] / (options['active_material_weight']) - - if options['molecular_weight']: - faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 - seconds_per_hour = 3600 # s h^-1 - f = faradays_constant / seconds_per_hour * 1000.0 # [f] = mAh mol^-1 - - df["IonsExtracted"] = (df["SpecificCapacity({}/mg)".format(options['old_units']['capacity'])]*options['molecular_weight'])*1000/f - - - if options['kind'] == 'biologic': - if options['active_material_weight']: - - capacity = options['old_units']['capacity'].split('h')[0] + '.h' - - - df["SpecificCapacity({}/mg)".format(options['old_units']["capacity"])] = df["Capacity/{}".format(capacity)] / (options['active_material_weight']) - - if options['molecular_weight']: - faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 - seconds_per_hour = 3600 # s h^-1 - f = faradays_constant / seconds_per_hour * 1000.0 # [f] = mAh mol^-1 - - df["IonsExtracted"] = (df["SpecificCapacity({}/mg)".format(options['old_units']['capacity'])]*options['molecular_weight'])*1000/f - - - if options['kind'] == 'batsmall': - if options['active_material_weight']: - - - active_material_weight = options['active_material_weight'] * unit_tables.mass()['mg'].loc[options['units']['mass']] - capacity = options['old_units']['capacity'] - - df[f'Capacity [{options["old_units"]["capacity"]}]'] = df[f'C [{options["old_units"]["capacity"]}/{options["old_units"]["mass"]}]'] * active_material_weight - - if options['molecular_weight']: - faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 - seconds_per_hour = 3600 # s h^-1 - f = faradays_constant / seconds_per_hour * 1000.0 # [f] = mAh mol^-1 - - molecular_weight = options['molecular_weight'] * unit_tables.mass()['g'].loc[options['old_units']['mass']] - - df["IonsExtracted"] = (df[f'C [{options["old_units"]["capacity"]}/{options["old_units"]["mass"]}]'] * molecular_weight)/f - - - #df['reaction_coordinate'] = (df[f'TT [{options["old_units"]["time"]}]'] * unit_tables.time()[options['old_units']["time"]].loc["h"]) / np.abs(df[f'I [{options["old_units"]["current"]}]'] * unit_tables.current()[options['old_units']["current"]].loc['A']) - - - return df - - -def calculate_efficiency(df: pd.DataFrame, options: dict) -> pd.DataFrame: - - - default_options = { - 'reference_index': 0 - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - df['charge_capacity_fade'] = (df['charge_capacity'] / df['charge_capacity'].iloc[options['reference_index']])*100 - df['discharge_capacity_fade'] = (df['discharge_capacity'] / df['discharge_capacity'].iloc[options['reference_index']])*100 - - df['coulombic_efficiency'] = (df['discharge_capacity'] / df['charge_capacity'])*100 - df['energy_efficiency'] = (df['discharge_energy'] / df['charge_energy'])*100 - - - return df - - -def unit_conversion(df, options): - from . import unit_tables - - if options['kind'] == 'batsmall': - - df["TT [{}]".format(options['old_units']["time"])] = df["TT [{}]".format(options['old_units']["time"])] * unit_tables.time()[options['old_units']["time"]].loc[options['units']['time']] - df["U [{}]".format(options['old_units']["voltage"])] = df["U [{}]".format(options['old_units']["voltage"])] * unit_tables.voltage()[options['old_units']["voltage"]].loc[options['units']['voltage']] - df["I [{}]".format(options['old_units']["current"])] = df["I [{}]".format(options['old_units']["current"])] * unit_tables.current()[options['old_units']["current"]].loc[options['units']['current']] - df["C [{}/{}]".format(options['old_units']["capacity"], options['old_units']["mass"])] = df["C [{}/{}]".format(options['old_units']["capacity"], options['old_units']["mass"])] * (unit_tables.capacity()[options['old_units']["capacity"]].loc[options['units']["capacity"]] / unit_tables.mass()[options['old_units']["mass"]].loc[options['units']["mass"]]) - - columns = ['time', 'voltage', 'current', 'count', 'specific_capacity', 'comment'] - - # Add column labels for specific capacity and ions if they exist - if f'Capacity [{options["old_units"]["capacity"]}]' in df.columns: - df[f'Capacity [{options["old_units"]["capacity"]}]'] = df[f'Capacity [{options["old_units"]["capacity"]}]'] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] - columns.append('capacity') - - if 'IonsExtracted' in df.columns: - columns.append('ions') - - #columns.append('reaction_coordinate') - - df.columns = columns - - - - - - if options['kind'] == 'neware': - - - record_number = 'Data serial number' if 'Data serial number' in df.columns else 'Record number' - relative_time = 'Relative Time(h:min:s.ms)' if 'Relative Time(h:min:s.ms)' in df.columns else 'Relative Time' - continuous_time = 'Continuous Time(h:min:s.ms)' if 'Continuous Time(h:min:s.ms)' in df.columns else 'Continuous Time' - real_time = 'Real Time(h:min:s.ms)' if 'Real Time(h:min:s.ms)' in df.columns else 'Real Time' - - - if options['summary']: - df[f'Energy({options["old_units"]["energy"]})'] = df[f'Energy({options["old_units"]["energy"]})'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df[f'Starting current({options["old_units"]["current"]})'] = df[f'Starting current({options["old_units"]["current"]})'] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] - df[f'Start Volt({options["old_units"]["voltage"]})'] = df[f'Start Volt({options["old_units"]["voltage"]})'] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] - df[f'Capacity({options["old_units"]["capacity"]})'] = df[f'Capacity({options["old_units"]["capacity"]})'] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] - df[f'Energy({options["old_units"]["energy"]})'] = df[f'Energy({options["old_units"]["energy"]})'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df[f'CycleTime({options["units"]["time"]})'] = df.apply(lambda row : convert_time_string(row[relative_time], unit=options['units']['time']), axis=1) - df[f'RunTime({options["units"]["time"]})'] = df.apply(lambda row : convert_datetime_string(row[real_time], reference=df[real_time].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0],unit=options['units']['time']), axis=1) - - - - droplist = [ - 'Chnl', - 'Original step', - f'End Volt({options["old_units"]["voltage"]})', - f'Termination current({options["old_units"]["current"]})', - relative_time, - real_time, - continuous_time, - f'Net discharge capacity({options["old_units"]["capacity"]})', - f'Chg Cap({options["old_units"]["capacity"]})', - f'DChg Cap({options["old_units"]["capacity"]})', - f'Net discharge energy({options["old_units"]["energy"]})', - f'Chg Eng({options["old_units"]["energy"]})', - f'DChg Eng({options["old_units"]["energy"]})' - ] - - # Drop all undesireable columns - for drop in droplist: - if drop in df.columns: - df.drop(drop, axis=1, inplace=True) - - columns = ['cycle', 'steps', 'status', 'voltage', 'current', 'capacity', 'energy'] - - - # Add column labels for specific capacity and ions if they exist - if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: - df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_capacity') - - if f'SpecificEnergy({options["old_units"]["energy"]}/mg)' in df.columns: - df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] = df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_energy') - - if 'IonsExtracted' in df.columns: - columns.append('ions') - - # Append energy column label here as it was the last column to be generated - columns.append('cycle_time') - columns.append('runtime') - - # Apply new column labels - df.columns = columns - - - else: - df['Current({})'.format(options['old_units']['current'])] = df['Current({})'.format(options['old_units']['current'])] * unit_tables.current()[options['old_units']['current']].loc[options['units']['current']] - df['Voltage({})'.format(options['old_units']['voltage'])] = df['Voltage({})'.format(options['old_units']['voltage'])] * unit_tables.voltage()[options['old_units']['voltage']].loc[options['units']['voltage']] - df['Capacity({})'.format(options['old_units']['capacity'])] = df['Capacity({})'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] - df['Energy({})'.format(options['old_units']['energy'])] = df['Energy({})'.format(options['old_units']['energy'])] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] - df['CycleTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_time_string(row[relative_time], unit=options['units']['time']), axis=1) - df['RunTime({})'.format(options['units']['time'])] = df.apply(lambda row : convert_datetime_string(row[real_time], reference=df[real_time].iloc[0], ref_time=df[f'CycleTime({options["units"]["time"]})'].iloc[0], unit=options['units']['time']), axis=1) - columns = ['status', 'jump', 'cycle', 'steps', 'current', 'voltage', 'capacity', 'energy'] - - if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: - df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_capacity') - - if f'SpecificEnergy({options["old_units"]["energy"]}/mg)' in df.columns: - df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] = df[f'SpecificEnergy({options["old_units"]["energy"]}/mg)'] * unit_tables.energy()[options['old_units']['energy']].loc[options['units']['energy']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_energy') - - - if 'IonsExtracted' in df.columns: - columns.append('ions') - - columns.append('cycle_time') - columns.append('time') - - - droplist = [record_number, relative_time, real_time] - - for drop in droplist: - if drop in df.columns: - df.drop(drop, axis=1, inplace=True) - - df.columns = columns - - if options['kind'] == 'biologic': - for column in df.columns: - if 'time' in column: - df['time/{}'.format(options['old_units']['time'])] = df["time/{}".format(options['old_units']["time"])] * unit_tables.time()[options['old_units']["time"]].loc[options['units']['time']] - - if 'Ewe' in column: - df["Ewe/{}".format(options['old_units']["voltage"])] = df["Ewe/{}".format(options['old_units']["voltage"])] * unit_tables.voltage()[options['old_units']["voltage"]].loc[options['units']['voltage']] - - if '' in column: - df["/{}".format(options['old_units']["current"])] = df["/{}".format(options['old_units']["current"])] * unit_tables.current()[options['old_units']["current"]].loc[options['units']['current']] - - if 'Capacity' in column: - capacity = options['old_units']['capacity'].split('h')[0] + '.h' - df["Capacity/{}".format(capacity)] = df["Capacity/{}".format(capacity)] * (unit_tables.capacity()[options['old_units']["capacity"]].loc[options['units']["capacity"]]) - - - - columns = [ - 'status_change', 'status', 'time', 'voltage', 'energy_charge', 'energy_discharge', 'current', 'capacity', 'cycle'] if options['mode'] == 'GC' else [ # GC headers - 'status', 'time', 'control_voltage', 'voltage', 'current', 'cycle', 'charge', 'power' # CV headers - ] - - if options['mode'] == 'GC': - if 'SpecificCapacity({}/mg)'.format(options['old_units']['capacity']) in df.columns: - df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] = df['SpecificCapacity({}/mg)'.format(options['old_units']['capacity'])] * unit_tables.capacity()[options['old_units']['capacity']].loc[options['units']['capacity']] / unit_tables.mass()['mg'].loc[options['units']["mass"]] - columns.append('specific_capacity') - - if 'IonsExtracted' in df.columns: - columns.append('ions') - - df.columns = columns - - return df - - -def set_units(options: dict) -> None: - - # Complete the list of units - if not all are passed, then default value will be used - required_units = ['time', 'current', 'voltage', 'capacity', 'mass', 'energy', 'specific_capacity'] - - default_units = { - 'time': 'h', - 'current': 'mA', - 'voltage': 'V', - 'capacity': 'mAh', - 'mass': 'g', - 'energy': 'mWh', - 'specific_capacity': None} - - if not options['units']: - options['units'] = default_units - - - aux.update_options(options=options['units'], required_options=required_units, default_options=default_units) - - options['units']['specific_capacity'] = r'{} {}'.format(options['units']['capacity'], options['units']['mass']) + '$^{-1}$' - - - -def get_old_units(df: pd.DataFrame, options: dict) -> dict: - ''' Reads a DataFrame with cycling data and determines which units have been used and returns these in a dictionary''' - - if options['kind'] == 'batsmall': - - old_units = {} - - for column in df.columns: - if 'TT [' in column: - old_units['time'] = column.split()[-1].strip('[]') - elif 'U [' in column: - old_units['voltage'] = column.split()[-1].strip('[]') - elif 'I [' in column: - old_units['current'] = column.split()[-1].strip('[]') - elif 'C [' in column: - old_units['capacity'], old_units['mass'] = column.split()[-1].strip('[]').split('/') - - # time = df.columns[0].split()[-1].strip('[]') - # voltage = df.columns[1].split()[-1].strip('[]') - # current = df.columns[2].split()[-1].strip('[]') - # capacity, mass = df.columns[4].split()[-1].strip('[]').split('/') - # old_units = {'time': time, 'current': current, 'voltage': voltage, 'capacity': capacity, 'mass': mass} - - if options['kind']=='neware': - - for column in df.columns: - if 'Voltage' in column or 'Start Volt' in column: - voltage = column.split('(')[-1].strip(')') - elif 'Current' in column or 'Starting current' in column: - current = column.split('(')[-1].strip(')') - elif 'Capacity' in column: - capacity = column.split('(')[-1].strip(')') - elif 'Energy' in column or 'Eng' in column: - energy = column.split('(')[-1].strip(')') - - old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy} - - - if options['kind'] == 'biologic': - - old_units = {} - for column in df.columns: - if 'time' in column: - old_units['time'] = column.split('/')[-1] - elif 'Ewe' in column: - old_units['voltage'] = column.split('/')[-1] - elif 'Capacity' in column: - old_units['capacity'] = column.split('/')[-1].replace('.', '') - elif 'Energy' in column: - old_units['energy'] = column.split('/')[-1].replace('.', '') - elif '' in column: - old_units['current'] = column.split('/')[-1] - - return old_units - -def convert_time_string(time_string, unit='ms'): - ''' Convert time string from Neware-data with the format hh:mm:ss.xx to any given unit''' - - h, m, s = time_string.split(':') - ms = float(s)*1000 + int(m)*1000*60 + int(h)*1000*60*60 - - factors = {'ms': 1, 's': 1/1000, 'min': 1/(1000*60), 'h': 1/(1000*60*60)} - - t = ms*factors[unit] - - return t - - - -def convert_datetime_string(datetime_string, reference, ref_time, unit='s'): - ''' Convert time string from Neware-data with the format yyy-mm-dd hh:mm:ss to any given unit''' - - from datetime import datetime - - # Parse the - current_date, current_time = datetime_string.split() - current_year, current_month, current_day = current_date.split('-') - current_hour, current_minute, current_second = current_time.split(':') - current_date = datetime(int(current_year), int(current_month), int(current_day), int(current_hour), int(current_minute), int(current_second)) - - reference_date, reference_time = reference.split() - reference_year, reference_month, reference_day = reference_date.split('-') - reference_hour, reference_minute, reference_second = reference_time.split(':') - reference_date = datetime(int(reference_year), int(reference_month), int(reference_day), int(reference_hour), int(reference_minute), int(reference_second)) - - days = current_date - reference_date - - - s = days.days*24*60*60 + days.seconds - - factors = {'ms': 1000, 's': 1, 'min': 1/(60), 'h': 1/(60*60)} - - time = s * factors[unit] + ref_time - - return time - - - - - - diff --git a/nafuma/electrochemistry/plot.py b/nafuma/electrochemistry/plot.py deleted file mode 100644 index cb4c045..0000000 --- a/nafuma/electrochemistry/plot.py +++ /dev/null @@ -1,747 +0,0 @@ -from pickle import MARK -import matplotlib.pyplot as plt -from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) - -import pandas as pd -import numpy as np -import math -import os -import shutil -from PIL import Image - -import ipywidgets as widgets -from IPython.display import display - -import nafuma.electrochemistry as ec -import nafuma.plotting as btp -import nafuma.auxillary as aux - - - - -def plot_gc(data, options=None): - - - # Update options - default_options = { - 'force_reload': False, - 'x_vals': 'capacity', 'y_vals': 'voltage', - 'which_cycles': 'all', - 'limit': None, # Limit line to be drawn - 'exclude_cycles': [], - 'show_plot': True, - 'summary': False, - 'charge': True, 'discharge': True, - 'colours': None, - 'markers': None, - 'differentiate_charge_discharge': True, - 'gradient': False, - 'interactive': False, - 'interactive_session_active': False, - 'rc_params': {}, - 'format_params': {}, - 'save_gif': False, - 'save_path': 'animation.gif', - 'fps': 1, - 'fig': None, 'ax': None, - 'edgecolor': plt.rcParams['lines.markeredgecolor'], - 'plot_every': 1, - } - - options = aux.update_options(options=options, default_options=default_options) - - - # Read data if not already loaded - if not 'cycles' in data.keys() or options['force_reload']: - data['cycles'] = ec.io.read_data(data=data, options=options) - - # Update list of cycles to correct indices - update_cycles_list(data=data, options=options) - - if options['interactive']: - options['interactive'], options['interactive_session_active'] = False, True - plot_gc_interactive(data=data, options=options) - return - - - - colours = generate_colours(options=options) - markers = generate_markers(options=options) - - if not options['summary']: - - if options['show_plot']: - # Prepare plot - - if not options['fig'] and not options['ax']: - fig, ax = btp.prepare_plot(options=options) - else: - fig, ax = options['fig'], options['ax'] - - - for i, cycle in enumerate(options['which_cycles']): - if options['charge']: - data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) - - if options['discharge']: - data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) - - - if options['interactive_session_active']: - update_labels(options, force=True) - else: - update_labels(options) - - - - if options['save_gif'] and not options['interactive_session_active']: - if not os.path.isdir('tmp'): - os.makedirs('tmp') - - # Scale image to make GIF smaller - options['format_params']['width'] = 7.5 - options['format_params']['height'] = 3 - - options['format_params']['dpi'] = 200 - - for i, cycle in enumerate(data['cycles']): - if i in options['which_cycles']: - - giffig, gifax = btp.prepare_plot(options=options) - - if options['charge']: - cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0]) - if options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1]) - - gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}') - update_labels(options) - - giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options) - - plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png')) - plt.close() - - - img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')] - frames = [] - for path in img_paths: - frame = Image.open(path) - frames.append(frame) - - frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) - - shutil.rmtree('tmp') - - - - - elif options['summary'] and options['show_plot']: - # Prepare plot - if not options['fig'] and not options['ax']: - fig, ax = btp.prepare_plot(options=options) - else: - fig, ax = options['fig'], options['ax'] - - mask = [] - for i in range(data['cycles'].shape[0]): - if i+1 in options['which_cycles']: - mask.append(True) - else: - mask.append(False) - - - # Drop the last row if it is midway through a charge in order to avoid mismatch of length of mask and dataset. - if len(mask) > data['cycles'].shape[0]: - del mask[-1] - data['cycles'].drop(data['cycles'].tail(1).index, inplace=True) - - - # FIXME To begin, the default is that y-values correspond to x-values. This should probably be implemented in more logical and consistent manner in the future. - if options['x_vals'] in ['coulombic_efficiency', 'energy_efficiency']: - data['cycles'].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor']) - if options['limit']: - ax.axhline(y=options['limit'], ls='--', c='black') - - else: - if options['charge']: - yval = 'charge_' + options['x_vals'] - data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][0], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor']) - - if options['discharge']: - yval = 'discharge_' + options['x_vals'] - data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[1], edgecolor=plt.rcParams['lines.markeredgecolor']) - - - if options['limit']: - ax.axhline(y=options['limit'], ls='--', c='black') - - - if options['interactive_session_active']: - update_labels(options, force=True) - else: - update_labels(options) - - - - if options['show_plot']: - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - return data['cycles'], fig, ax - else: - return data['cycles'], None, None - - -def plot_gc_interactive(data, options): - - w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_gc), data=widgets.fixed(data), options=widgets.fixed(options), - charge=widgets.ToggleButton(value=True), - discharge=widgets.ToggleButton(value=True), - x_vals=widgets.Dropdown(options=['specific_capacity', 'capacity', 'ions', 'voltage', 'time', 'energy'], value='specific_capacity', description='X-values') - ) - - options['widget'] = w - - display(w) - - - - -def plot_cv(data, options): - - # Update options - default_options = { - 'force_reload': False, - 'x_vals': 'voltage', 'y_vals': 'current', - 'which_cycles': 'all', - 'limit': None, # Limit line to be drawn - 'exclude_cycles': [], - 'show_plot': True, - 'charge': True, 'discharge': True, - 'colours': None, - 'differentiate_charge_discharge': True, - 'gradient': False, - 'interactive': False, - 'interactive_session_active': False, - 'rc_params': {}, - 'format_params': {}, - 'save_gif': False, - 'save_path': 'animation.gif', - 'fps': 1, - 'plot_every': 1, - 'fig': None, - 'ax': None - } - - options = aux.update_options(options=options, default_options=default_options) - - - # Read data if not already loaded - if not 'cycles' in data.keys() or options['force_reload']: - data['cycles'] = ec.io.read_data(data=data, options=options) - - - # Update list of cycles to correct indices - update_cycles_list(data=data, options=options) - - colours = generate_colours(options=options) - - if options['show_plot']: - # Prepare plot - if not options['fig'] and not options['ax']: - fig, ax = btp.prepare_plot(options=options) - else: - fig, ax = options['fig'], options['ax'] - - for i, cycle in enumerate(options['which_cycles']): - if options['charge']: - data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) - - if options['discharge']: - data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) - - # for i, cycle in enumerate(data['cycles']): - # if i in options['which_cycles']: - # if options['charge']: - # cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) - - # if options['discharge']: - # cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) - - update_labels(options) - - - - if options['save_gif'] and not options['interactive_session_active']: - if not os.path.isdir('tmp'): - os.makedirs('tmp') - - # Scale image to make GIF smaller - options['format_params']['width'] = 7.5 - options['format_params']['height'] = 3 - - options['format_params']['dpi'] = 200 - - - for i, cycle in enumerate(data['cycles']): - if i in options['which_cycles']: - - giffig, gifax = btp.prepare_plot(options=options) - - if options['charge']: - cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0]) - if options['discharge']: - cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1]) - - gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}') - update_labels(options) - - giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options) - - plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png')) - plt.close() - - - img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')] - frames = [] - for path in img_paths: - frame = Image.open(path) - frames.append(frame) - - frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) - - shutil.rmtree('tmp') - - - - if options['show_plot']: - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - return data['cycles'], fig, ax - else: - return data['cycles'], None, None - -def update_labels(options, force=False): - - if 'xlabel' not in options.keys() or force: - options['xlabel'] = options['x_vals'].capitalize().replace('_', ' ') - - if 'ylabel' not in options.keys() or force: - options['ylabel'] = options['y_vals'].capitalize().replace('_', ' ') - - - if 'xunit' not in options.keys() or force: - if options['x_vals'] == 'capacity': - options['xunit'] = options['units']['capacity'] - elif options['x_vals'] == 'specific_capacity': - options['xunit'] = f"{options['units']['capacity']} {options['units']['mass']}$^{{-1}}$" - elif options['x_vals'] == 'time': - options['xunit'] = options['units']['time'] - elif options['x_vals'] == 'ions': - options['xunit'] = None - - - if 'yunit' not in options.keys() or force: - if options['y_vals'] == 'voltage': - options['yunit'] = options['units']['voltage'] - - - - - -def update_cycles_list(data, options: dict) -> None: - - if options['which_cycles'] == 'all': - options['which_cycles'] = [i for i in range(len(data['cycles']))] - - - elif isinstance(options['which_cycles'], list): - - cycles =[] - - for cycle in options['which_cycles']: - if isinstance(cycle, int): - cycles.append(cycle-1) - - elif isinstance(cycle, tuple): - interval = [i-1 for i in range(cycle[0], cycle[1]+1)] - cycles.extend(interval) - - - options['which_cycles'] = cycles - - - # Tuple is used to define an interval - as elements tuples can't be assigned, I convert it to a list here. - elif isinstance(options['which_cycles'], tuple): - which_cycles = list(options['which_cycles']) - - if which_cycles[0] <= 0: - which_cycles[0] = 1 - - elif which_cycles[1] < 0: - which_cycles[1] = len(options['which_cycles']) - - - options['which_cycles'] = [i-1 for i in range(which_cycles[0], which_cycles[1]+1)] - - for i, cycle in enumerate(options['which_cycles']): - if cycle in options['exclude_cycles']: - del options['which_cycles'][i] - - - options['which_cycles'] = options['which_cycles'][::options['plot_every']] - - - - -def prettify_gc_plot(fig, ax, options=None): - - - ################################################################## - ######################### UPDATE OPTIONS ######################### - ################################################################## - - # Define the required options - required_options = [ - 'columns', - 'xticks', 'yticks', - 'show_major_ticks', 'show_minor_ticks', - 'xlim', 'ylim', - 'hide_x_axis', 'hide_y_axis', - 'positions', - 'x_vals', 'y_vals', - 'xlabel', 'ylabel', - 'units', 'sizes', - 'title' - ] - - - # Define the default options - default_options = { - 'columns': 1, - 'xticks': None, 'yticks': None, - 'show_major_ticks': [True, True, True, True], 'show_minor_ticks': [True, True, True, True], - 'xlim': None,'ylim': None, - 'hide_x_axis': False, 'hide_y_axis': False, - 'positions': {'xaxis': 'bottom', 'yaxis': 'left'}, - 'x_vals': 'specific_capacity', 'y_vals': 'voltage', - 'xlabel': None, 'ylabel': None, - 'units': {'capacity': 'mAh', 'specific_capacity': r'mAh g$^{-1}$', 'time': 's', 'current': 'mA', 'energy': 'mWh', 'mass': 'g', 'voltage': 'V'}, - 'sizes': None, - 'title': None - } - - aux.update_options(options, required_options, default_options) - - - ################################################################## - ########################## DEFINE SIZES ########################## - ################################################################## - - # Define the required sizes - required_sizes = [ - 'labels', - 'legend', - 'title', - 'line', 'axes', - 'tick_labels', - 'major_ticks', 'minor_ticks'] - - - - # Define default sizes - default_sizes = { - 'labels': 30*options['columns'], - 'legend': 30*options['columns'], - 'title': 30*options['columns'], - 'line': 3*options['columns'], - 'axes': 3*options['columns'], - 'tick_labels': 30*options['columns'], - 'major_ticks': 20*options['columns'], - 'minor_ticks': 10*options['columns'] - } - - # Initialise dictionary if it doesn't exist - if not options['sizes']: - options['sizes'] = {} - - - # Update dictionary with default values where none is supplied - for size in required_sizes: - if size not in options['sizes']: - options['sizes'][size] = default_sizes[size] - - - ################################################################## - ########################## AXIS LABELS ########################### - ################################################################## - - - if not options['xlabel']: - options['xlabel'] = prettify_labels(options['x_vals']) + ' [{}]'.format(options['units'][options['x_vals']]) - - else: - options['xlabel'] = options['xlabel'] + ' [{}]'.format(options['units'][options['x_vals']]) - - - if not options['ylabel']: - options['ylabel'] = prettify_labels(options['y_vals']) + ' [{}]'.format(options['units'][options['y_vals']]) - - else: - options['ylabel'] = options['ylabel'] + ' [{}]'.format(options['units'][options['y_vals']]) - - ax.set_xlabel(options['xlabel'], size=options['sizes']['labels']) - ax.set_ylabel(options['ylabel'], size=options['sizes']['labels']) - - ################################################################## - ###################### TICK MARKS & LABELS ####################### - ################################################################## - - ax.tick_params(direction='in', which='major', bottom=options['show_major_ticks'][0], left=options['show_major_ticks'][1], top=options['show_major_ticks'][2], right=options['show_major_ticks'][0], length=options['sizes']['major_ticks'], width=options['sizes']['axes']) - ax.tick_params(direction='in', which='minor', bottom=options['show_minor_ticks'][0], left=options['show_minor_ticks'][1], top=options['show_minor_ticks'][2], right=options['show_minor_ticks'][0], length=options['sizes']['minor_ticks'], width=options['sizes']['axes']) - - - - # DEFINE AND SET TICK DISTANCES - - from . import unit_tables - - # Define default ticks and scale to desired units - default_ticks = { - 'specific_capacity': [100 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']]), 50 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']])], - 'capacity': [0.1 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']]), 0.05 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']])], - 'voltage': [0.5 * (unit_tables.voltage()['V'].loc[options['units']['voltage']]), 0.25 * (unit_tables.voltage()['V'].loc[options['units']['voltage']])], - 'time': [10 * (unit_tables.time()['h'].loc[options['units']['time']]), 5 * (unit_tables.time()['h'].loc[options['units']['time']])] - } - - - if options['positions']['yaxis'] == 'right': - ax.yaxis.set_label_position("right") - ax.yaxis.tick_right() - - - # Set default tick distances for x-axis if not specified - if not options['xticks']: - - major_xtick = default_ticks[options['x_vals']][0] - minor_xtick = default_ticks[options['x_vals']][1] - - # Otherwise apply user input - else: - major_xtick = options['xticks'][0] - minor_xtick = options['xticks'][1] - - - # Set default tick distances for x-axis if not specified - if not options['yticks']: - - major_ytick = default_ticks[options['y_vals']][0] - minor_ytick = default_ticks[options['y_vals']][1] - - # Otherwise apply user input - else: - major_ytick = options['yticks'][0] - minor_ytick = options['yticks'][1] - - - # Apply values - ax.xaxis.set_major_locator(MultipleLocator(major_xtick)) - ax.xaxis.set_minor_locator(MultipleLocator(minor_xtick)) - - ax.yaxis.set_major_locator(MultipleLocator(major_ytick)) - ax.yaxis.set_minor_locator(MultipleLocator(minor_ytick)) - - - - - # SET FONTSIZE OF TICK LABELS - - plt.xticks(fontsize=options['sizes']['tick_labels']) - plt.yticks(fontsize=options['sizes']['tick_labels']) - - ################################################################## - ########################## AXES LIMITS ########################### - ################################################################## - - if options['xlim']: - plt.xlim(options['xlim']) - - if options['ylim']: - plt.ylim(options['ylim']) - - ################################################################## - ############################# TITLE ############################## - ################################################################## - - if options['title']: - ax.set_title(options['title'], size=options['sizes']['title']) - - ################################################################## - ############################# LEGEND ############################# - ################################################################## - - if ax.get_legend(): - ax.get_legend().remove() - - return fig, ax - - -def prettify_labels(label): - - labels_dict = { - 'capacity': 'Capacity', - 'specific_capacity': 'Specific capacity', - 'voltage': 'Voltage', - 'current': 'Current', - 'energy': 'Energy', - 'time': 'Time' - } - - return labels_dict[label] - - - -def generate_colours(options): - - default_options = { - 'gradient_colours': None, - } - - aux.update_options(options=options, default_options=default_options) - - # Assign colours from the options dictionary if it is defined, otherwise use standard colours. - if options['colours']: - charge_colour = options['colours'][0] - discharge_colour = options['colours'][1] - - if isinstance(charge_colour, tuple): - charge_colour = list(charge_colour) - if isinstance(discharge_colour, tuple): - discharge_colour = list(discharge_colour) - - else: - charge_colour = [(40/255, 70/255, 75/255)] # Dark Slate Gray #28464B, coolors.co - discharge_colour = [(239/255, 160/255, 11/255)] # Marigold #EFA00B, coolors.co - - if not options['differentiate_charge_discharge']: - discharge_colour = charge_colour - - - - # If gradient is enabled, find start and end points for each colour - if options['gradient']: - - if not options['gradient_colours']: - - options['gradient_colours'] = [[None, None], [None, None]] - - add_charge = min([(1-x)*0.75 for x in charge_colour]) - add_discharge = min([(1-x)*0.75 for x in discharge_colour]) - - options['gradient_colours'][0][0] = charge_colour - options['gradient_colours'][0][1] = [x+add_charge for x in charge_colour] - - options['gradient_colours'][1][0] = discharge_colour - options['gradient_colours'][1][1] = [x+add_discharge for x in discharge_colour] - - - - - - # Generate lists of colours - colours = [] - - if len(charge_colour) != len(options['which_cycles']): - if options['gradient']: - options['number_of_colours'] = len(options['which_cycles']) - - charge_colours = btp.mix_colours(colour1=options['gradient_colours'][0][0], colour2=options['gradient_colours'][0][1], options=options) - discharge_colours = btp.mix_colours(colour1=options['gradient_colours'][1][0], colour2=options['gradient_colours'][1][1], options=options) - - for chg, dchg in zip(charge_colours, discharge_colours): - colours.append([chg, dchg]) - - else: - for i in options['which_cycles']: - colours.append([charge_colour, discharge_colour]) - - - else: - for chg, dchg in zip(charge_colour, discharge_colour): - colours.append([chg, dchg]) - - - return colours - - - -def generate_markers(options): - - if not options['markers']: - markers = ['o', 'v'] - - else: - markers = [options['markers'][0], options['markers'][1]] - - return markers - - - -def get_tickmarks(df: pd.DataFrame, ticks: list, value: str, exclude=None): - - - min_val = df[value].min() - max_val = df[value].max() - - - # Get major ticks - major_ticks = [np.round((min_val + ticks[0]*i),2) for i in range(int(np.floor((max_val-min_val)/ticks[0]))+1)] - major_ticks.append(np.round(max_val, 2)) - - major_ticks = aux.get_unique(major_ticks) - - major_ticklabels = [i*ticks[0] for i in range(len(major_ticks)-1)] - major_ticklabels.append(np.round((max_val-min_val),1)) - - if exclude: - for i, tick in enumerate(major_ticklabels): - if tick in exclude: - del major_ticks[i] - del major_ticklabels[i] - - - # Get minor ticks - minor_ticks = [np.round((min_val + ticks[1]*i),2) for i in range(int(np.floor((max_val-min_val)/ticks[1]))+1) if np.round((min_val + ticks[1]*i),2) not in major_ticks] - minor_ticklabels = [np.round(tick - min_val, 2) for tick in minor_ticks] - - return major_ticks, major_ticklabels, minor_ticks, minor_ticklabels - - - -def assign_tickmarks(dfs: list, options, fig, ax, exclude=None): - - major_ticks, major_ticklabels, minor_ticks = [], [], [] - - if not exclude: - exclude = [[None, None] for i in range(len(options['which_cycles']))] - - for i, cycle in enumerate(options['which_cycles']): - #Get ticks from charge cycle - major_tick, major_ticklabel, minor_tick, minor_ticklabel = ec.plot.get_tickmarks(dfs[cycle][0], ticks=options['x_tick_locators'], value=options['x_vals'], exclude=exclude[i][0]) - major_ticks += major_tick - major_ticklabels += major_ticklabel - minor_ticks += minor_tick - - # Get ticks from discharge cycle - major_tick, major_ticklabel, minor_tick, minor_ticklabel = ec.plot.get_tickmarks(dfs[cycle][1], ticks=[1, 0.25], value='ions', exclude=exclude[i][1]) - major_ticks += major_tick - major_ticklabels += major_ticklabel - minor_ticks += minor_tick - - - ax.set_xticks(major_ticks, minor=False) - ax.set_xticklabels(major_ticklabels) - ax.set_xticks(minor_ticks, minor=True) - - - - - return fig, ax \ No newline at end of file diff --git a/nafuma/electrochemistry/unit_tables.py b/nafuma/electrochemistry/unit_tables.py deleted file mode 100644 index c839b9b..0000000 --- a/nafuma/electrochemistry/unit_tables.py +++ /dev/null @@ -1,53 +0,0 @@ -import pandas as pd - -def time(): - # Define matrix for unit conversion for time - time = {'h': [1, 60, 3600, 3600000], 'min': [1/60, 1, 60, 60000], 's': [1/3600, 1/60, 1, 1000], 'ms': [1/3600000, 1/60000, 1/1000, 1]} - time = pd.DataFrame(time) - time.index = ['h', 'min', 's', 'ms'] - - return time - -def current(): - # Define matrix for unit conversion for current - current = {'A': [1, 1000, 1000000], 'mA': [1/1000, 1, 1000], 'uA': [1/1000000, 1/1000, 1]} - current = pd.DataFrame(current) - current.index = ['A', 'mA', 'uA'] - - return current - -def voltage(): - # Define matrix for unit conversion for voltage - voltage = {'V': [1, 1000, 1000000], 'mV': [1/1000, 1, 1000], 'uV': [1/1000000, 1/1000, 1]} - voltage = pd.DataFrame(voltage) - voltage.index = ['V', 'mV', 'uV'] - - return voltage - -def capacity(): - # Define matrix for unit conversion for capacity - capacity = {'Ah': [1, 1000, 1000000], 'mAh': [1/1000, 1, 1000], 'uAh': [1/1000000, 1/1000, 1]} - capacity = pd.DataFrame(capacity) - capacity.index = ['Ah', 'mAh', 'uAh'] - - return capacity - -def mass(): - # Define matrix for unit conversion for capacity - mass = {'kg': [1, 1000, 1000000, 1000000000], 'g': [1/1000, 1, 1000, 1000000], 'mg': [1/1000000, 1/1000, 1, 1000], 'ug': [1/1000000000, 1/1000000, 1/1000, 1]} - mass = pd.DataFrame(mass) - mass.index = ['kg', 'g', 'mg', 'ug'] - - return mass - - -def energy(): - - energy = {'kWh': [1, 1000, 1000000], 'Wh': [1/1000, 1, 1000], 'mWh': [1/100000, 1/1000, 1]} - energy = pd.DataFrame(energy) - energy.index = ['kWh', 'Wh', 'mWh'] - - return energy - - - diff --git a/nafuma/pdf/__init__.py b/nafuma/pdf/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nafuma/plotting.py b/nafuma/plotting.py deleted file mode 100644 index 0231ad9..0000000 --- a/nafuma/plotting.py +++ /dev/null @@ -1,520 +0,0 @@ -import nafuma.auxillary as aux - -import matplotlib.pyplot as plt -from matplotlib.ticker import (MultipleLocator) -from mpl_toolkits.axes_grid.inset_locator import (inset_axes, InsetPosition, BboxPatch, BboxConnector) - -from matplotlib.transforms import TransformedBbox -from matplotlib.patches import Rectangle - -import importlib -import matplotlib.patches as mpatches -from matplotlib.lines import Line2D -import matplotlib.lines as mlines -import matplotlib.markers as mmarkers -import itertools - -from PIL import Image -import os - -import numpy as np - - - -def prepare_plot(options={}): - ''' A general function to prepare a plot based on contents of options['rc_params'] and options['format_params']. - - rc_params is a dictionary with keyval-pairs corresponding to rcParams in matplotlib, to give the user full control over this. Please consult the matplotlib-documentation - - format_params will determine the size, aspect ratio, resolution etc. of the figure. Should be modified to conform with any requirements from a journal.''' - - if 'rc_params' in options.keys(): - rc_params = options['rc_params'] - else: - rc_params = {} - - - if 'format_params' in options.keys(): - format_params = options['format_params'] - else: - format_params = {} - - - - default_format_params = { - 'single_column_width': 8.3, - 'double_column_width': 17.1, - 'column_type': 'single', - 'width_ratio': '1:1', - 'aspect_ratio': '1:1', - 'width': None, - 'height': None, - 'compress_width': 1, - 'compress_height': 1, - 'upscaling_factor': 1.0, - 'dpi': 600, - 'nrows': 1, - 'ncols': 1, - 'grid_ratio_height': None, - 'grid_ratio_width': None - } - - format_params = aux.update_options(options=format_params, default_options=default_format_params) - - - - - # Reset run commands - plt.rcdefaults() - - # Update run commands if any is passed (will pass an empty dictionary if not passed) - update_rc_params(rc_params) - - if not format_params['width']: - format_params['width'] = determine_width(format_params=format_params) - - if not format_params['height']: - format_params['height'] = determine_height(format_params=format_params, width=format_params['width']) - - format_params['width'], format_params['height'] = scale_figure(format_params=format_params, width=format_params['width'], height=format_params['height']) - - if format_params['nrows'] == 1 and format_params['ncols'] == 1: - fig, ax = plt.subplots(figsize=(format_params['width'], format_params['height']), dpi=format_params['dpi']) - - return fig, ax - - else: - if not format_params['grid_ratio_height']: - format_params['grid_ratio_height'] = [1 for i in range(format_params['nrows'])] - - if not format_params['grid_ratio_width']: - format_params['grid-ratio_width'] = [1 for i in range(format_params['ncols'])] - - fig, axes = plt.subplots(nrows=format_params['nrows'], ncols=format_params['ncols'], figsize=(format_params['width'],format_params['height']), - gridspec_kw={'height_ratios': format_params['grid_ratio_height'], 'width_ratios': format_params['grid_ratio_width']}, - facecolor='w', dpi=format_params['dpi']) - - return fig, axes - - -def adjust_plot(fig, ax, options): - ''' A general function to adjust plot according to contents of the options-dictionary ''' - - - default_options = { - 'plot_kind': None, # defaults to None, but should be utilised when requiring special formatting for a particular plot - 'xlabel': None, 'ylabel': None, - 'xunit': None, 'yunit': None, - 'xlabel_pad': 4.0, 'ylabel_pad': 4.0, - 'hide_x_labels': False, 'hide_y_labels': False, # Whether the main labels on the x- and/or y-axes should be hidden - 'hide_x_ticklabels': False, 'hide_y_ticklabels': False, # Whether ticklabels on the x- and/or y-axes should be hidden - 'hide_x_ticks': False, 'hide_y_ticks': False, # Whether the ticks on the x- and/or y-axes should be hidden - 'x_tick_locators': None, 'y_tick_locators': None, # The major and minor tick locators for the x- and y-axes - 'rotation_x_ticks': 0, 'rotation_y_ticks': 0, # Degrees the x- and/or y-ticklabels should be rotated - 'xticks': None, 'yticks': None, # Custom definition of the xticks and yticks. This is not properly implemented now. - 'xlim': None, 'ylim': None, # Limits to the x- and y-axes - 'xlim_reset': False, 'ylim_reset': False, # For use in setting limits of backgrounds - forcing reset of xlim and ylim, useful when more axes - 'title': None, # Title of the plot - 'backgrounds': [], - 'legend': False, 'legend_position': ['lower center', (0.5, -0.1)], 'legend_ncol': 1, # Toggles on/off legend. Specifices legend position and the number of columns the legend should appear as. - 'subplots_adjust': {'left': None, 'right': None, 'top': None, 'bottom': None, 'wspace': None, 'hspace': None}, # Adjustment of the Axes-object within the Figure-object. Fraction of the Figure-object the left, bottom, right and top edges of the Axes-object will start. - 'marker_edges': None, - 'text': None # Text to show in the plot. Should be a list where the first element is the string, and the second is a tuple with x- and y-coordinates. Could also be a list of lists to show more strings of text. - } - - - options = aux.update_options(options=options, default_options=default_options) - - # Set labels on x- and y-axes - if not options['hide_y_labels']: - if not options['yunit']: - ax.set_ylabel(f'{options["ylabel"]}', labelpad=options['ylabel_pad']) - else: - ax.set_ylabel(f'{options["ylabel"]} [{options["yunit"]}]', labelpad=options['ylabel_pad']) - - else: - ax.set_ylabel('') - - if not options['hide_x_labels']: - if not options['xunit']: - ax.set_xlabel(f'{options["xlabel"]}', labelpad=options['xlabel_pad']) - else: - ax.set_xlabel(f'{options["xlabel"]} [{options["xunit"]}]', labelpad=options['xlabel_pad']) - else: - ax.set_xlabel('') - - # Set multiple locators - if options['y_tick_locators']: - ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0])) - ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1])) - - if options['x_tick_locators']: - ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0])) - ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1])) - - - # FIXME THIS NEEDS REWORK FOR IT TO FUNCTION PROPERLY! - #if options['xticks']: - # ax.set_xticks(np.arange(plot_data['start'], plot_data['end']+1)) - # ax.set_xticklabels(options['xticks']) - # else: - # ax.set_xticks(np.arange(plot_data['start'], plot_data['end']+1)) - # ax.set_xticklabels([x/2 for x in np.arange(plot_data['start'], plot_data['end']+1)]) - - # Hide x- and y- ticklabels - if options['hide_y_ticklabels']: - ax.tick_params(axis='y', direction='in', which='both', labelleft=False, labelright=False) - else: - plt.xticks(rotation=options['rotation_x_ticks']) - #ax.set_xticklabels(ax.get_xticks(), rotation = options['rotation_x_ticks']) - - if options['hide_x_ticklabels']: - ax.tick_params(axis='x', direction='in', which='both', labelbottom=False, labeltop=False) - else: - pass - #ax.set_yticklabels(ax.get_yticks(), rotation = options['rotation_y_ticks']) - - - # Hide x- and y-ticks: - if options['hide_y_ticks']: - ax.tick_params(axis='y', direction='in', which='both', left=False, right=False) - else: - ax.tick_params(axis='y', direction='in', which='both', left=True, right=True) - - if options['hide_x_ticks']: - ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False) - else: - ax.tick_params(axis='x', direction='in', which='both', bottom=True, top=True) - - - - # Set title - if options['title']: - ax.set_title(options['title'], fontsize=plt.rcParams['font.size']) - - - - #### DRAW/REMOVE LEGEND #### - # Options: - # 'legend_position': (default ['lower center', (0.5, -0.1)]) - Follows matplotlib's way of specifying legend position - # 'legend_ncol': (default 1) # Number of columns to write the legend in - # Also requires options to contain values in colours, markers and labels. (No defaults) - - if ax.get_legend(): - ax.get_legend().remove() - - - if options['legend']: - # Make palette and linestyles from original parameters - if not options['colours']: - colours = generate_colours(palettes=options['palettes']) - else: - colours = itertools.cycle(options['colours']) - - - markers = itertools.cycle(options['markers']) - - # Create legend - active_markers = [] - active_labels = [] - - for label in options['labels']: - - - # Discard next linestyle and colour if label is _ - if label == '_': - _ = next(colours) - _ = next(markers) - - else: - marker = next(markers) - if not marker: - active_markers.append(mlines.Line2D([], [], color=next(colours))) - else: - active_markers.append(mlines.Line2D([], [], markerfacecolor=next(colours), markeredgecolor=options['marker_edges'], markersize=10, color=(1,1,1,0), marker=marker)) - - active_labels.append(label) - - - - ax.legend(active_markers, active_labels, frameon=False, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], ncol=options['legend_ncol']) - #fig.legend(handles=patches, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False) - - - - # Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions) - plt.subplots_adjust(**options['subplots_adjust']) - - - # If limits for x- and y-axes is passed, sets these. - if options['xlim'] is not None: - ax.set_xlim(options['xlim']) - - if options['ylim'] is not None: - ax.set_ylim(options['ylim']) - - - #### DRAW BACKGROUNDS #### - # options['backgrounds'] should contain a dictionary or a list of dictionaries. Options to be specified are listed below. - - if options['backgrounds']: - - if not isinstance(options['backgrounds'], list): - options['backgrounds'] = [options['backgrounds']] - - - for background in options['backgrounds']: - default_background_options = { - 'colour': (0,0,0), - 'alpha': 0.2, - 'xlim': list(ax.get_xlim()), - 'ylim': list(ax.get_ylim()), - 'zorder': 0, - 'edgecolour': None, - 'linewidth': None - } - - - background = aux.update_options(options=background, default_options=default_background_options) - - if options['xlim_reset']: - background['xlim'] = list(ax.get_xlim()) - if options['ylim_reset']: - background['ylim'] = list(ax.get_ylim()) - - if not background['xlim'][0]: - background['xlim'][0] = ax.get_xlim()[0] - if not background['xlim'][1]: - background['xlim'][1] = ax.get_xlim()[1] - if not background['ylim'][0]: - background['ylim'][0] = ax.get_ylim()[0] - if not background['ylim'][1]: - background['ylim'][1] = ax.get_ylim()[1] - - ax.add_patch(Rectangle( - xy=(background['xlim'][0], background['ylim'][0]), # Anchor point - width=background['xlim'][1]-background['xlim'][0], # Width of background - height=background['ylim'][1]-background['ylim'][0], # Height of background - zorder=background['zorder'], # Placement in stack - facecolor=(background['colour'][0], background['colour'][1], background['colour'][2], background['alpha']), # Colour - edgecolor=background['edgecolour'], # Edgecolour - linewidth=background['linewidth']) # Linewidth - ) - - - # Add custom text - if options['text']: - - # If only a single element, put it into a list so the below for-loop works. - if isinstance(options['text'][0], str): - options['text'] = [options['text']] - - # Plot all passed texts - for text in options['text']: - ax.text(x=text[1][0], y=text[1][1], s=text[0]) - - return fig, ax - - - - -def ipywidgets_update(func, data, options={}, **kwargs): - ''' A general ipywidgets update function that can be passed to ipywidgets.interactive. To use this, you can run: - - import ipywidgets as widgets - import beamtime.plotting as btp - - w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(my_func), plot_data=widgets.fixed(plot_data), options=widgets.fixed(options), key1=widget1, key2=widget2, key3=widget3) - - where key1, key2, key3 etc. are the values in the options-dictionary you want widget control of, and widget1, widget2, widget3 etc. are widgets to control these values, e.g. widgets.IntSlider(value=1, min=0, max=10) - ''' - - # Update the options-dictionary with the values from the widgets - for key in kwargs: - options[key] = kwargs[key] - - # Call the function with the plot_data and options-dictionaries - func(data=data, options=options) - - - -def determine_width(format_params): - ''' ''' - - conversion_cm_inch = 0.3937008 # cm to inch - - if format_params['column_type'] == 'single': - column_width = format_params['single_column_width'] - elif format_params['column_type'] == 'double': - column_width = format_params['double_column_width'] - - column_width *= conversion_cm_inch - - - width_ratio = [float(num) for num in format_params['width_ratio'].split(':')] - - - width = column_width * width_ratio[0]/width_ratio[1] - - - return width - - -def determine_height(format_params, width): - - aspect_ratio = [float(num) for num in format_params['aspect_ratio'].split(':')] - - height = width/(aspect_ratio[0] / aspect_ratio[1]) - - return height - - -def scale_figure(format_params, width, height): - width = width * format_params['upscaling_factor'] * format_params['compress_width'] - height = height * format_params['upscaling_factor'] * format_params['compress_height'] - - return width, height - - - -def update_rc_params(rc_params): - ''' Update all passed run commands in matplotlib''' - - if rc_params: - for key in rc_params.keys(): - plt.rcParams.update({key: rc_params[key]}) - - -def generate_colours(palettes, kind=None): - - if kind == 'single': - colour_cycle = itertools.cycle(palettes) - - else: - # Creates a list of all the colours that is passed in the colour_cycles argument. Then makes cyclic iterables of these. - colour_collection = [] - for palette in palettes: - mod = importlib.import_module("palettable.colorbrewer.%s" % palette[0]) - colour = getattr(mod, palette[1]).mpl_colors - colour_collection = colour_collection + colour - - colour_cycle = itertools.cycle(colour_collection) - - - return colour_cycle - - - -def prepare_inset_axes(parent_ax, options): - - default_options = { - 'hide_inset_x_labels': False, # Whether x labels should be hidden - 'hide_inset_x_ticklabels': False, - 'hide_inset_x_ticks': False, - 'rotation_inset_x_ticks': 0, - 'hide_inset_y_labels': False, # whether y labels should be hidden - 'hide_inset_y_ticklabels': False, - 'hide_inset_y_ticks': False, - 'rotation_inset_y_ticks': 0, - 'inset_x_tick_locators': [100, 50], # Major and minor tick locators - 'inset_y_tick_locators': [10, 5], - 'inset_position': [0.1,0.1,0.3,0.3], - 'inset_bounding_box': [0,0,0.1, 0.1], - 'inset_marks': [None, None], - 'legend_position': ['upper center', (0.20, 0.90)], # the position of the legend passed as arguments to loc and bbox_to_anchor respectively, - 'connecting_corners': [1,2] - } - - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - # Create a set of inset Axes: these should fill the bounding box allocated to - # them. - inset_ax = plt.axes(options["inset_bounding_box"]) - # Manually set the position and relative size of the inset axes within ax1 - ip = InsetPosition(parent_ax, options['inset_position']) - inset_ax.set_axes_locator(ip) - - if options['connecting_corners'] and len(options["connecting_corners"]) == 2: - connect_inset(parent_ax, inset_ax, loc1a=options['connecting_corners'][0], loc2a=options['connecting_corners'][1], loc1b=options['connecting_corners'][0], loc2b=options['connecting_corners'][1], fc='none', ec='black') - elif options['connecting_corners'] and len(options['connecting_corners']) == 4: - connect_inset(parent_ax, inset_ax, loc1a=options['connecting_corners'][0], loc2a=options['connecting_corners'][1], loc1b=options['connecting_corners'][2], loc2b=options['connecting_corners'][3], fc='none', ec='black', ls='--') - - inset_ax.xaxis.set_major_locator(MultipleLocator(options['inset_x_tick_locators'][0])) - inset_ax.xaxis.set_minor_locator(MultipleLocator(options['inset_x_tick_locators'][1])) - - - inset_ax.yaxis.set_major_locator(MultipleLocator(options['inset_y_tick_locators'][0])) - inset_ax.yaxis.set_minor_locator(MultipleLocator(options['inset_y_tick_locators'][1])) - - - - - return inset_ax - - - - -def connect_inset(parent_axes, inset_axes, loc1a=1, loc1b=1, loc2a=2, loc2b=2, **kwargs): - rect = TransformedBbox(inset_axes.viewLim, parent_axes.transData) - - pp = BboxPatch(rect, fill=False, **kwargs) - parent_axes.add_patch(pp) - - p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1a, loc2=loc1b, **kwargs) - inset_axes.add_patch(p1) - p1.set_clip_on(False) - p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2a, loc2=loc2b, **kwargs) - inset_axes.add_patch(p2) - p2.set_clip_on(False) - - return pp, p1, p2 - - - -def make_animation(paths, options={}): - - default_options = { - 'save_folder': '.', - 'save_filename': 'animation.gif', - 'fps': 5 - } - - options = aux.update_options(options=options, default_options=default_options) - - - frames = [] - for path in paths: - frame = Image.open(path) - frames.append(frame) - - frames[0].save(os.path.join(options['save_folder'], options['save_filename']), format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) - - - - -def mix_colours(colour1, colour2, options): - - default_options = { - 'number_of_colours': 10, - 'weights': None - } - - options = aux.update_options(options=options, default_options=default_options) - - if not options['weights']: - options['weights'] = [x/options['number_of_colours'] for x in range(options['number_of_colours'])] - - colours = [] - for weight in options['weights']: - colour = [] - - for c1, c2 in zip(colour1, colour2): - colour.append(np.round(((1-weight)*c1 + weight*c2), 5)) - - colours.append(colour) - - - return colours - \ No newline at end of file diff --git a/nafuma/ppms/__init__.py b/nafuma/ppms/__init__.py deleted file mode 100644 index e0e052c..0000000 --- a/nafuma/ppms/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import io, plot \ No newline at end of file diff --git a/nafuma/ppms/io.py b/nafuma/ppms/io.py deleted file mode 100644 index 8691163..0000000 --- a/nafuma/ppms/io.py +++ /dev/null @@ -1,129 +0,0 @@ -import pandas as pd -import numpy as np - -import nafuma.auxillary as aux - -def read_data(path, options={}): - - default_options = { - 'split': False, - } - - options = aux.update_options(options=options, default_options=default_options) - - index = find_start(path) - - df = pd.read_csv(path, skiprows=index+1) - - df = df[['Comment', 'Time Stamp (sec)', 'Temperature (K)', 'Magnetic Field (Oe)', - 'DC Moment (emu)', 'DC Std. Err. (emu)', 'DC Quad. Moment (emu)', - 'AC=1 DC=2 Locate=3', 'Max. Field (Oe)', 'Pressure (Torr)', 'Temp. Status (code)', - ]] - - new_columns = ['Comment', 'Time', 'Temperature', 'Magnetic_Field', - 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', - 'Status', 'Max_Field', 'Pressure', 'Temperature_Status'] - - df.columns = new_columns - - - df[['Temperature', 'Magnetic_Field', 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', 'Max_Field', 'Pressure']] = df[['Temperature', 'Magnetic_Field', 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', 'Max_Field', 'Pressure']].astype(float) - - df = df.loc[df['DC_Std_Err'] < 0.001] - - if all([option in options.keys() for option in ['molar_mass', 'sample_mass']]): - df = calculate_emu_per_mol_oe(df, options) - df = calculate_bohr_magnetons(df, options) - df = calculate_chi_inverse(df, options) - - if options['split']: - mask = df.loc[df['Comment'].notna()] - dfs = [] - for i in range(1,len(mask.index)): - dfs.append(df.iloc[mask.index[i-1]:mask.index[i]]) - - return dfs - - return df - - -def read_hysteresis(path): - - index = find_start(path) - - df = pd.read_csv(path, skiprows=index+1) - - df = df[['Comment', 'Time Stamp (sec)', 'Temperature (K)', 'Magnetic Field (Oe)', - 'DC Moment (emu)', 'DC Std. Err. (emu)', 'DC Quad. Moment (emu)', - 'AC=1 DC=2 Locate=3', 'Max. Field (Oe)', 'Pressure (Torr)', 'Temp. Status (code)', - ]] - - new_columns = ['Comment', 'Time', 'Temperature', 'Magnetic_Field', - 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', - 'Status', 'Max_Field', 'Pressure', 'Temperature_Status'] - - df.columns = new_columns - - df[['Temperature', 'Magnetic_Field', 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', 'Max_Field', 'Pressure']] = df[['Temperature', 'Magnetic_Field', 'DC_Moment', 'DC_Std_Err', 'DC_Quad_Moment', 'Max_Field', 'Pressure']].astype(float) - - df = df.loc[df['DC_Std_Err'] < 0.001] - - return df - - -def find_start(path): - - with open(path, 'r') as f: - - i = 0 - line = f.readline() - - while '[Data]' not in line: - line = f.readline() - i += 1 - - - if i > 1000: - break - - - return i - - - -def calculate_emu_per_mol_oe(df, options={}): - - m = options['sample_mass'] / 1000 # convert from mg to g - n = m / options['molar_mass'] - - - df['DC_Moment_emu_per_mol'] = df['DC_Moment'] / n - df['DC_Moment_emu_per_mol_oe'] = df['DC_Moment'] / (n * df['Magnetic_Field']) - - - return df - - - - -def calculate_bohr_magnetons(df, options={}): - - - default_options = { - 'units': 'cgs', - } - - options = aux.update_options(options=options, default_options=default_options) - - if options['units'] == 'cgs': - df['bohr_magnetons'] = df['DC_Moment_emu_per_mol'] * 1.07828E20 / 6.023E23 ## mu_B per emu divided by Avogadro's number - - return df - - - -def calculate_chi_inverse(df, options={}): - - df['chi_inverse'] = 1/ df['DC_Moment_emu_per_mol'] - - return df \ No newline at end of file diff --git a/nafuma/ppms/plot.py b/nafuma/ppms/plot.py deleted file mode 100644 index e69de29..0000000 diff --git a/nafuma/test/__init__.py b/nafuma/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nafuma/test/pytest.ini b/nafuma/test/pytest.ini deleted file mode 100644 index c317621..0000000 --- a/nafuma/test/pytest.ini +++ /dev/null @@ -1,9 +0,0 @@ -# pytest.ini - -[pytest] -minversion = 6.0 -testpaths = - . - -filterwarnings = - ignore::DeprecationWarning diff --git a/nafuma/test/test_auxillary.py b/nafuma/test/test_auxillary.py deleted file mode 100644 index 3425814..0000000 --- a/nafuma/test/test_auxillary.py +++ /dev/null @@ -1,76 +0,0 @@ -import nafuma.auxillary as aux -import os - -def test_swap_values(): - - - test_dict = {'test1': 1, 'test2': 2} - key1 = 'test1' - key2 = 'test2' - - oldval1 = test_dict[key1] - oldval2 = test_dict[key2] - - new_dict = aux.swap_values(options=test_dict, key1=key1, key2=key2) - - assert (test_dict[key1] == oldval2) and (test_dict[key2] == oldval1) - - -def test_ceil() -> None: - - assert aux.ceil(1.05, 0.5) == 1.5 - assert aux.ceil(1.05, 1) == 2.0 - assert aux.ceil(1.1, 0.2) == 1.2 - - -def test_floor() -> None: - - assert aux.floor(2.02, 1) == 2.0 - assert aux.floor(2.02, 0.01) == 2.02 - assert aux.floor(2.013, 0.01) == 2.01 - - - -def test_options() -> None: - - - options = {} - - default_options = { - 'test1': 1, - 'test2': 2, - 'test3': 3, - 'test4': 4, - 'test5': 5, - } - - - options = aux.update_options(options=options, default_options=default_options) - - assert options['test1'] == default_options['test1'] - - -def test_save_options() -> None: - - options = {'test1': 1, 'test2': 2} - path = 'tmp.dat' - - aux.save_options(options, path) - - assert os.path.isfile(path) - - os.remove(path) - - -def test_load_options() -> None: - - options = {'test1': 1, 'test2': 2} - path = 'tmp.dat' - - aux.save_options(options, path) - - loaded_options = aux.load_options(path) - - assert (loaded_options['test1'] == 1) and (loaded_options['test2'] == 2) - - os.remove(path) diff --git a/nafuma/test/test_plotting.py b/nafuma/test/test_plotting.py deleted file mode 100644 index e1af0b8..0000000 --- a/nafuma/test/test_plotting.py +++ /dev/null @@ -1,181 +0,0 @@ -import nafuma.plotting as btp -from cycler import cycler -import itertools -import numpy as np - -import matplotlib.pyplot as plt -import matplotlib as mpl - - -def test_generate_colours() -> None: - - assert type(btp.generate_colours('black', kind='single')) == itertools.cycle - - palettes = [('qualitative', 'Dark2_8')] - colour_cycle = btp.generate_colours(palettes) - - assert type(colour_cycle) == itertools.cycle - - - # Test that it actually loaded 8 colours when given a set of 8 colours to - - same_colour = None - for i in range(10): - colour = next(colour_cycle) - if i == 0: - first_colour = colour - - if colour == first_colour: - repeat_colour_index = i - - - assert repeat_colour_index == 8 - - - - -def test_update_rc_params() -> None: - - rc_params = { - 'lines.linewidth': 100 - } - - prev_params = plt.rcParams['lines.linewidth'] - - # Update run commands if any is passed (will pass an empty dictionary if not passed) - btp.update_rc_params(rc_params) - - new_params = plt.rcParams['lines.linewidth'] - - assert new_params == 100 - assert prev_params != new_params - - - # Reset run commands - plt.rcdefaults() - - - -def test_scale_figure() -> None: - - width, height = 1, 1 - - format_params = { - 'upscaling_factor': 2, - 'compress_width': 1, - 'compress_height': 1 - } - - width1, height1 = btp.scale_figure(format_params=format_params, width=width, height=height) - - assert width1 == 2 and height1 == 2 - - format_params = { - 'upscaling_factor': 1, - 'compress_width': 0.5, - 'compress_height': 1 - } - - width2, height2 = btp.scale_figure(format_params=format_params, width=width, height=height) - - assert width2 == 0.5 and height2 == 1 - - format_params = { - 'upscaling_factor': 2, - 'compress_width': 0.5, - 'compress_height': 0.2 - } - - width2, height2 = btp.scale_figure(format_params=format_params, width=width, height=height) - - assert width2 == 1 and height2 == 0.4 - - -def test_determine_width() -> None: - - conversion_cm_inch = 0.3937008 # cm to inch - - format_params = { - 'column_type': 'single', - 'single_column_width': 5, - 'double_column_width': 10, - 'width_ratio': '1:1' - } - - assert np.round(btp.determine_width(format_params),6) == np.round(5*conversion_cm_inch,6) - - format_params['column_type'] = 'double' - - assert np.round(btp.determine_width(format_params), 6) == np.round(10*conversion_cm_inch, 6) - - - format_params['column_type'] = 'single' - format_params['width_ratio'] = '1:2' - - assert np.round(btp.determine_width(format_params), 6) == np.round(2.5*conversion_cm_inch, 6) - -def test_determine_height() -> None: - - - width = 1 - - format_params = { - 'aspect_ratio': '1:1' - } - - assert btp.determine_height(format_params=format_params, width=width) == 1 - - format_params['aspect_ratio'] = '3:1' - - assert (btp.determine_height(format_params=format_params, width=width) - 0.333333333333333) < 10e-7 - - assert True - - - -def test_prepare_plot() -> None: - - fig, ax = btp.prepare_plot() - - assert type(fig) == plt.Figure - assert fig.get_dpi() == 600 - assert ax.get_xlim() == (0.0, 1.0) - - - -def test_adjust_plot() -> None: - - fig, ax = btp.prepare_plot() - - options = { - 'xlim': (0.0, 2.0), - 'title': 'Test' - } - - fig, ax = btp.adjust_plot(fig, ax, options) - - - assert ax.get_xlim() == (0.0, 2.0) - assert ax.get_title() == 'Test' - - - -def test_ipywidgets_update() -> None: - - - - def test_func(data, options): - test1 = options['test1'] - test2 = options['test2'] - - assert type(data) == dict - assert test1 == 1 - assert test2 == 2 - - - - data = {} - options = {} - - btp.ipywidgets_update(func=test_func, data=data, options=options, test1=1, test2=2) - diff --git a/nafuma/test/xrd/test_io.py b/nafuma/test/xrd/test_io.py deleted file mode 100644 index e69de29..0000000 diff --git a/nafuma/test/xrd/test_plot.py b/nafuma/test/xrd/test_plot.py deleted file mode 100644 index e69de29..0000000 diff --git a/nafuma/xanes/__init__.py b/nafuma/xanes/__init__.py deleted file mode 100644 index af49ee6..0000000 --- a/nafuma/xanes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import io, calib, plot, edges \ No newline at end of file diff --git a/nafuma/xanes/calib.py b/nafuma/xanes/calib.py deleted file mode 100644 index 3e3eea7..0000000 --- a/nafuma/xanes/calib.py +++ /dev/null @@ -1,1420 +0,0 @@ -from logging import raiseExceptions -from signal import default_int_handler -from jinja2 import TemplateRuntimeError -import pandas as pd -import numpy as np -import os -import matplotlib.pyplot as plt -import nafuma.auxillary as aux - -import nafuma.plotting as btp -import nafuma.xanes as xas -import nafuma.xanes.io as io -from scipy.signal import savgol_filter -from datetime import datetime -import ipywidgets as widgets -from IPython.display import display - -import warnings - - -##Better to make a new function that loops through the files, and performing the split_xanes_scan on - -#Trying to make a function that can decide which edge it is based on the first ZapEnergy-value -def find_element(data: dict, index=0) -> str: - ''' Takes the data dictionary and determines based on the start value of the ZapEnergy-column which element the edge is from.''' - - element_energy_intervals = { - 'Mn': [5.9, 6.5], - 'Fe': [6.9, 7.2], - 'Co': [7.6, 7.8], - 'Ni': [8.0, 8.6] - } - - if (element_energy_intervals['Mn'][0] < data['xanes_data_original']["ZapEnergy"].iloc[index]) & (data['xanes_data_original']["ZapEnergy"].iloc[index] < element_energy_intervals['Mn'][1]): - edge = 'Mn' - elif (element_energy_intervals['Fe'][0] < data['xanes_data_original']["ZapEnergy"].iloc[index]) & (data['xanes_data_original']["ZapEnergy"].iloc[index] < element_energy_intervals['Fe'][1]): - edge = 'Fe' - elif (element_energy_intervals['Co'][0] < data['xanes_data_original']["ZapEnergy"].iloc[index]) & (data['xanes_data_original']["ZapEnergy"].iloc[index] < element_energy_intervals['Co'][1]): - edge = 'Co' - elif (element_energy_intervals['Ni'][0] < data['xanes_data_original']["ZapEnergy"].iloc[index]) & (data['xanes_data_original']["ZapEnergy"].iloc[index] < element_energy_intervals['Ni'][1]): - edge = 'Ni' - - - return(edge) - - - -def pre_edge_fit(data: dict, options={}) -> pd.DataFrame: - - - # FIXME Add log-file - - required_options = ['pre_edge_limits', 'pre_edge_masks', 'pre_edge_polyorder', 'pre_edge_store_data', 'log', 'logfile', 'show_plots', 'save_plots', 'save_folder', 'ylim', 'interactive', 'interactive_session_active'] - default_options = { - 'pre_edge_limits': [None, None], - 'pre_edge_masks': [], - 'pre_edge_polyorder': 1, - 'pre_edge_store_data': False, - 'log': False, - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_pre_edge_fit.log', - 'show_plots': False, - 'save_plots': False, - 'save_folder': './', - 'ylim': [None, None], - 'interactive': False, - 'interactive_session_active': False - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if options['log']: - aux.write_log(message='Starting pre edge fit', options=options) - - # FIXME Implement with finding accurate edge position - # FIXME Allow specification of start of pre-edge area - # Find the cutoff point at which the edge starts - everything to the LEFT of this point will be used in the pre edge function fit - if not options['pre_edge_limits'][0]: - options['pre_edge_limits'][0] = data['xanes_data_original']['ZapEnergy'].min() - - - if not options['pre_edge_limits'][1]: - pre_edge_limit_offset = 0.03 - - data['edge'] = find_element(data) - - edge_position = estimate_edge_position(data, options, index=0) - options['pre_edge_limits'][1] = edge_position - pre_edge_limit_offset - - if options['pre_edge_limits'][0] >= options['pre_edge_limits'][1]: - options['pre_edge_limits'][1] = options['pre_edge_limits'][0] + 0.03 - - # Start inteactive session with ipywidgets. Disables options['interactive'] in order for the interactive loop to not start another interactive session - if options['interactive']: - options['interactive'] = False - options['interactive_session_active'] = True - options['show_plots'] = True - pre_edge_fit_interactive(data=data, options=options) - return - - - - # FIXME There should be an option to specify the interval in which to fit the background - now it is taking everything to the left of edge_start parameter, but if there are some artifacts in this area, it should be possible to - # limit the interval - # Making a dataframe only containing the rows that are included in the background subtraction (points lower than where the edge start is defined) - pre_edge_data = data['xanes_data_original'].loc[(data['xanes_data_original']["ZapEnergy"] > options['pre_edge_limits'][0]) & (data['xanes_data_original']["ZapEnergy"] < options['pre_edge_limits'][1])].copy() - - for mask in options['pre_edge_masks']: - pre_edge_data.loc[(pre_edge_data['ZapEnergy'] > mask[0]) & (pre_edge_data['ZapEnergy'] < mask[1])] = np.nan - - pre_edge_data = pre_edge_data.dropna() - - # Making a new dataframe, with only the ZapEnergies as the first column -> will be filled to include the background data - pre_edge_fit_data = pd.DataFrame(data['xanes_data_original']["ZapEnergy"]) - - data['pre_edge_params'] = {} - - for i, filename in enumerate(data['path']): - - if options['interactive_session_active'] and i > 0: - continue - - if options['log']: - aux.write_log(message=f'... Fitting pre edge on {os.path.basename(filename)} ({i+1}/{len(data["path"])})', options=options) - - #Fitting linear function to the background - params = np.polyfit(pre_edge_data["ZapEnergy"],pre_edge_data[filename],options['pre_edge_polyorder']) - fit_function = np.poly1d(params) - - data['pre_edge_params'][filename] = params - - if options['log']: - aux.write_log(message=f'...... Pre edge fitted between {options["pre_edge_limits"][0]} and {options["pre_edge_limits"][1]} with polynomial of order {options["pre_edge_polyorder"]} with parmameters {params}.', options=options) - if options['pre_edge_masks']: - aux.write_log(message=f'...... Excluded regions: {options["pre_edge_masks"]}', options=options) - - #making a list, y_pre,so the background will be applied to all ZapEnergy-values - background=fit_function(pre_edge_fit_data["ZapEnergy"]) - - #adding a new column in df_background with the y-values of the background - pre_edge_fit_data.insert(1,filename,background) - - if options['show_plots'] or options['save_plots']: - fig, (ax1, ax2) = plt.subplots(1,2,figsize=(20,10)) - data['xanes_data_original'].plot(x='ZapEnergy', y=filename, color='black', ax=ax1) - pre_edge_fit_data.plot(x='ZapEnergy', y=filename, color='red', ax=ax1) - ax1.axvline(x = max(pre_edge_data['ZapEnergy']), ls='--') - ax1.axvline(x = min(pre_edge_data['ZapEnergy']), ls='--') - ax1.set_title(f'{os.path.basename(filename)} - Full view', size=20) - - if options['ylim'][0] != None: - ax1.set_ylim(bottom=options['ylim'][0]) - if options['ylim'][1]: - ax1.set_ylim(top=options['ylim'][1]) - - for mask in options['pre_edge_masks']: - ax1.fill_between(x=mask, y1=0, y2=data['xanes_data_original'][filename].max()*2, alpha=0.2, color='black') - - data['xanes_data_original'].plot(x='ZapEnergy', y=filename, color='black', ax=ax2) - pre_edge_fit_data.plot(x='ZapEnergy', y=filename, color='red', ax=ax2) - ax2.axvline(x = max(pre_edge_data['ZapEnergy']), ls='--') - ax2.set_xlim([min(pre_edge_data['ZapEnergy']), max(pre_edge_data['ZapEnergy'])]) - ax2.set_ylim([min(pre_edge_data[filename]), max(pre_edge_data[filename])]) - ax2.set_title(f'{os.path.basename(filename)} - Fit region', size=20) - - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_pre_edge_fit.png' - plt.savefig(dst, transparent=False) - - if not options['show_plots']: - plt.close() - - - if options['log']: - aux.write_log(message=f'Pre edge fitting done.', options=options) - - if options['pre_edge_store_data']: - data['pre_edge_fit_data'] = pre_edge_fit_data - - return pre_edge_fit_data - - - -def pre_edge_fit_interactive(data: dict, options: dict) -> None: - - - w = widgets.interactive( - btp.ipywidgets_update, func=widgets.fixed(pre_edge_fit), data=widgets.fixed(data), options=widgets.fixed(options), - pre_edge_limits=widgets.FloatRangeSlider(value=[options['pre_edge_limits'][0], options['pre_edge_limits'][1]], min=data['xanes_data_original']['ZapEnergy'].min(), max=data['xanes_data_original']['ZapEnergy'].max(), step=0.0001), - pre_edge_store_data=widgets.Checkbox(value=options['pre_edge_store_data']) - ) - - options['widget'] = w - - display(w) - - - - -def pre_edge_subtraction(data: dict, options={}): - - required_options = ['log', 'logfile', 'show_plots', 'save_plots', 'save_folder', 'pre_edge_subtraction_store_data'] - default_options = { - 'log': False, - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_pre_edge_subtraction.log', - 'show_plots': False, - 'save_plots': False, - 'save_folder': './', - 'pre_edge_subtraction_store_data': True - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - if options['log']: - aux.write_log(message='Starting pre edge subtraction', options=options) - - xanes_data_bkgd_subtracted = pd.DataFrame(data['xanes_data_original']["ZapEnergy"]) - - for i, filename in enumerate(data['path']): - if options['log']: - aux.write_log(message=f'... Subtracting background on {os.path.basename(filename)} ({i}/{len(data["path"])})', options=options) - - xanes_data_bkgd_subtracted.insert(1, filename, data['xanes_data_original'][filename] - data['pre_edge_fit_data'][filename]) - - if options['save_plots'] or options['show_plots']: - - - fig, ax = plt.subplots(figsize=(10,5)) - data['xanes_data_original'].plot(x='ZapEnergy', y=filename, color='black', ax=ax, label='Original data') - xanes_data_bkgd_subtracted.plot(x='ZapEnergy', y=filename, color='red', ax=ax, label='Pre edge subtracted') - ax.set_title(f'{os.path.basename(filename)} - After subtraction', size=20) - - - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_pre_edge_subtraction.png' - - plt.savefig(dst) - - if not options['show_plots']: - plt.close() - - if options['pre_edge_subtraction_store_data']: - data['xanes_data'] = xanes_data_bkgd_subtracted - - return xanes_data_bkgd_subtracted - - - - -def post_edge_fit(data: dict, options={}): - ''' Fit the post edge within the post_edge.limits to a polynomial of post_edge.polyorder order. Allows interactive plotting, as well as showing static plots and saving plots to drive. - - Requires data to have already been read to data['xanes_data_original'] - ''' - - - required_options = ['log', 'logfile', 'post_edge_masks', 'post_edge_limits', 'post_edge_polyorder', 'post_edge_store_data', 'interactive', 'interactive_session_active', 'show_plots', 'save_plots', 'save_folder'] - - default_options = { - 'log': False, - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_post_edge_fit.log', - 'post_edge_limits': [None, None], - 'post_edge_masks': [], - 'post_edge_polyorder': 2, - 'post_edge_store_data': False, - 'interactive': False, - 'interactive_session_active': False, - 'show_plots': False, - 'save_plots': False, - 'save_folder': './', - 'ylim': [None, None] - } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - if not options['post_edge_limits'][0]: - post_edge_limit_offset = 0.03 - - data['edge'] = find_element(data) - - edge_position = estimate_edge_position(data, options, index=0) - options['post_edge_limits'][0] = edge_position + post_edge_limit_offset - - if not options['post_edge_limits'][1]: - options['post_edge_limits'][1] = data['xanes_data_original']['ZapEnergy'].max() - - if options['post_edge_limits'][0] > options['post_edge_limits'][1]: - options['post_edge_limits'][0] = options['post_edge_limits'][1] - 0.1 - - - # Start inteactive session with ipywidgets. Disables options['interactive'] in order for the interactive loop to not start another interactive session - if options['interactive']: - options['interactive'] = False - options['interactive_session_active'] = True - options['show_plots'] = True - post_edge_fit_interactive(data=data, options=options) - return - - - - post_edge_data = data['xanes_data_original'].loc[(data['xanes_data_original']["ZapEnergy"] > options['post_edge_limits'][0]) & (data['xanes_data_original']["ZapEnergy"] < options['post_edge_limits'][1])].copy() - - for mask in options['post_edge_masks']: - post_edge_data.loc[(post_edge_data['ZapEnergy'] > mask[0]) & (post_edge_data['ZapEnergy'] < mask[1])] = np.nan - - post_edge_data = post_edge_data.dropna() #Removing all indexes without any value, as some of the data sets misses the few last data points and fucks up the fit - - - - # Making a new dataframe, with only the ZapEnergies as the first column -> will be filled to include the background data - post_edge_fit_data = pd.DataFrame(data['xanes_data_original']["ZapEnergy"]) - - data['post_edge_params'] = {} - - for i, filename in enumerate(data['path']): - - if options['interactive_session_active'] and i > 0: - continue - - if options['log']: - aux.write_log(message=f'... Fitting post edge on {os.path.basename(filename)} ({i+1}/{len(data["path"])}) with polynomial order {options["post_edge_polyorder"]}', options=options) - - #Fitting linear function to the background - params = np.polyfit(post_edge_data["ZapEnergy"], post_edge_data[filename], options['post_edge_polyorder']) - fit_function = np.poly1d(params) - - if options['log']: - aux.write_log(message=f'...... Post edge fitted between {options["post_edge_limits"][0]} and {options["post_edge_limits"][1]} with polynomial of order {options["post_edge_polyorder"]} with parmameters {params}.', options=options) - if options['post_edge_masks']: - aux.write_log(message=f'...... Excluded regions: {options["post_edge_masks"]}', options=options) - - data['post_edge_params'][filename] = params - - #making a list, y_pre,so the background will be applied to all ZapEnergy-values - background=fit_function(post_edge_fit_data["ZapEnergy"]) - - #adding a new column in df_background with the y-values of the background - post_edge_fit_data.insert(1,filename,background) - - - if options['save_plots'] or options['show_plots']: - - - fig, (ax1, ax2) = plt.subplots(1,2,figsize=(20,10)) - data['xanes_data_original'].plot(x='ZapEnergy', y=filename, color='black', ax=ax1) - post_edge_fit_data.plot(x='ZapEnergy', y=filename, color='red', ax=ax1) - ax1.axvline(x = max(post_edge_data['ZapEnergy']), ls='--') - ax1.axvline(x = min(post_edge_data['ZapEnergy']), ls='--') - ax1.set_title(f'{os.path.basename(filename)} - Full view', size=20) - - for mask in options['post_edge_masks']: - ax1.fill_between(x=mask, y1=0, y2=data['xanes_data_original'][filename].max()*2, alpha=0.2, color='black') - - if options['ylim'][0] != None: - ax1.set_ylim(bottom=options['ylim'][0]) - if options['ylim'][1] != None: - ax1.set_ylim(top=options['ylim'][1]) - - data['xanes_data_original'].plot(x='ZapEnergy', y=filename, color='black', ax=ax2) - post_edge_fit_data.plot(x='ZapEnergy', y=filename, color='red', ax=ax2) - ax2.axvline(x = max(post_edge_data['ZapEnergy']), ls='--') - ax2.set_xlim([min(post_edge_data['ZapEnergy']), max(post_edge_data['ZapEnergy'])]) - ax2.set_ylim([min(post_edge_data[filename]), max(post_edge_data[filename])]) - ax2.set_title(f'{os.path.basename(filename)} - Fit region', size=20) - - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_post_edge_fit.png' - - plt.savefig(dst, transparent=False) - - if not options['show_plots']: - plt.close() - - - if options['log']: - aux.write_log(message='Post edge fitting done!', options=options) - - if options['post_edge_store_data']: - data['post_edge_fit_data'] = post_edge_fit_data.dropna(axis=0) - - - return post_edge_fit_data - - -def post_edge_fit_interactive(data: dict, options: dict) -> None: - ''' Defines the widgets to use with the ipywidgets interactive mode and calls the update function found in btp.ipywidgets. ''' - - w = widgets.interactive( - btp.ipywidgets_update, func=widgets.fixed(post_edge_fit), data=widgets.fixed(data), options=widgets.fixed(options), - post_edge_limits=widgets.FloatRangeSlider(value=[options['post_edge_limits'][0], options['post_edge_limits'][1]], min=data['xanes_data_original']['ZapEnergy'].min(), max=data['xanes_data_original']['ZapEnergy'].max(), step=0.0001), - post_edge_store_data=widgets.Checkbox(value=options['post_edge_store_data']) - ) - - options['widget'] = w - - display(w) - -def smoothing(data: dict, options={}): - ' Smoothes the data using the Savitzky-Golay filter. This is the only algorithm at this moment. ' - - - required_options = ['log', 'logfile', 'show_plots', 'save_plots', 'save_folder', 'interactive', 'smooth_window_length', 'smooth_algorithm', 'smooth_polyorder', 'smooth_save_default', 'smooth_store_data'] - default_options = { - 'log': False, # Toggles logging on / off - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_smoothing.log', # Sets path to log-file. Ignored if log == False - 'show_plots': False, # Toggles showing plots on / off. This is only recommended when working with a handful of scans. - 'save_plots': False, # Toggles saving plots on / off - 'save_folder': './', # Sets path to folder where plots should be saved. Ignored if save_plots == False - 'interactive': False, # Toggles interactive mode on / off. This is only recommended for a single scan to determine proper parameters for smoothing. - 'smooth_window_length': 3, # Determines the window length of smoothing that the savgol-filter uses for smoothing - 'smooth_polyorder': 2, # Determines the order of the polynomial used in the smoothing algorithm - 'smooth_algorithm': 'savgol', # At the present, only Savitzky-Golay filter is implemented. Add Gaussian and Boxcar later. - 'smooth_save_default': False, # Toggles whether or not to run a separate smoothing using default values on / off - 'smooth_store_data': False, # Toggles storing data to data['xanes_data'] on / off - } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - # Initialise new DataFrame with correct x-values - df_smooth = pd.DataFrame(data['xanes_data']['ZapEnergy']) - - # Do the same if smoothing with default values is toggled on - if options['smooth_save_default']: - df_smooth_default = pd.DataFrame(data['xanes_data']['ZapEnergy']) - - if options['log']: - aux.write_log(message='Starting smoothing procedure.', options=options) - - - # Run in interactive mode if enabled - if options['interactive']: - data['xanes_data_backup'] = data['xanes_data'] # Backup the data - options['interactive'] = False # Turn interactive mode off so that it is not called again within the interactive loop - options['show_plots'] = True # Force plotting on as interactive mode is useless without it - smoothing_interactive(data=data, options=options) # Call interactive version of the function - return - - - # FIXME Add other types of filters - for i, filename in enumerate(data['path']): - - - if options['smooth_algorithm'] == 'savgol': - if options['log']: - aux.write_log(message=f'Smoothing {filename} with algorithm: {options["smooth_algorithm"]} ({i+1}/{len(data["path"])})', options=options) - - # Apply savgol filter and add to DataFrame - df_smooth.insert(1, filename, savgol_filter(data['xanes_data'][filename], options['smooth_window_length'], options['smooth_polyorder'])) - - if options['smooth_save_default']: - if options['smooth_algorithm'] == 'savgol': - if options['log']: - aux.write_log(message=f'Smoothing {filename} using default parameters with algorithm: {options["smooth_algorithm"]} ({i+1}/{len(data["path"])})', options=options) - df_smooth_default.insert(1, filename, savgol_filter(data['xanes_data'][filename], default_options['smooth_window_length'], default_options['smooth_polyorder'])) - - - # Make plots ... - if options['save_plots'] or options['show_plots']: - - edge_pos = estimate_edge_position(data=data, options=options) - step_length = data['xanes_data']['ZapEnergy'].iloc[1] - data['xanes_data']['ZapEnergy'].iloc[0] - - - # ... if default smoothing is enabled. Only plotting +- 10 step sizes from the edge position - if options['smooth_save_default']: - fig, (ax1, ax2) = plt.subplots(1,2,figsize=(20,5)) - data['xanes_data'].loc[(data['xanes_data']['ZapEnergy'] > edge_pos-10*step_length) & (data['xanes_data']['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='black', ax=ax1, kind='scatter') - df_smooth.loc[(df_smooth['ZapEnergy'] > edge_pos-10*step_length) & (df_smooth['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='red', ax=ax1) - ax1.set_title(f'{os.path.basename(filename)} - Smooth', size=20) - - data['xanes_data'].loc[(data['xanes_data']['ZapEnergy'] > edge_pos-10*step_length) & (data['xanes_data']['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='black', ax=ax2, kind='scatter') - df_smooth_default.loc[(df_smooth_default['ZapEnergy'] > edge_pos-10*step_length) & (df_smooth_default['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='red', ax=ax2) - ax2.set_title(f'{os.path.basename(filename)} - Smooth (default values)', size=20) - - # ... if only smoothing with user defined variables is enabled. Only plotting +- 10 step sizes from the edge position - elif not options['smooth_save_default']: - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,10)) - data['xanes_data'].plot(x='ZapEnergy', y=filename, ax=ax1, kind='scatter', c='black') - df_smooth.plot(x='ZapEnergy', y=filename, ax=ax1, c='red') - - data['xanes_data'].loc[(data['xanes_data']['ZapEnergy'] > edge_pos-10*step_length) & (data['xanes_data']['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='black', ax=ax2, kind='scatter') - df_smooth.loc[(df_smooth['ZapEnergy'] > edge_pos-10*step_length) & (df_smooth['ZapEnergy'] < edge_pos+10*step_length)].plot(x='ZapEnergy', y=filename, color='red', ax=ax2) - - ax1.set_title(f'{os.path.basename(filename)} - Smooth', size=20) - ax2.set_title(f'{os.path.basename(filename)} - Smooth Edge Region', size=20) - - # Save plots - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_smooth.png' - plt.savefig(dst, transparent=False) - - # Close plots - if not options['show_plots']: - plt.close() - - if not options['smooth_save_default']: - df_smooth_default = None - - if options['smooth_store_data']: - data['xanes_data'] = df_smooth - options['smooth_store_data'] = False - - return df_smooth, df_smooth_default - - - -def smoothing_interactive(data: dict, options: dict) -> None: - ''' Defines the widgets to use with the ipywidgets interactive mode and calls the update function found in btp.ipywidgets. ''' - - w = widgets.interactive( - btp.ipywidgets_update, func=widgets.fixed(smoothing), data=widgets.fixed(data), options=widgets.fixed(options), - smooth_window_length=widgets.IntSlider(value=options['smooth_window_length'], min=3, max=21, step=2), - smooth_polyorder=widgets.IntSlider(value=options['smooth_polyorder'], min=1, max=5, step=1), - smooth_store_data=widgets.Checkbox(value=options['smooth_store_data']) - ) - - options['widget'] = w - - display(w) - -def backup(data): - - data['xanes_data_backup'] = data['xanes_data'].copy() - - -def restore_from_backup(data): - ''' Restores DataFrame from data['xanes_data_backup'] to data['xanes_data']. This can be useful e.g. when smoothing and you want to re-do the smoothing with different parameters. - - If there is no DataFrame stored in data['xanes_data_backup'], this function does nothing. ''' - - if 'xanes_data_bakcup' in data.keys(): - data['xanes_data'] = data['xanes_data_backup'].copy() - - -def find_nearest(array, value): - ''' Finds the value closest to value in array''' - - array = np.asarray(array) - idx = (np.abs(array - value)).argmin() - return array[idx] - - -def estimate_edge_position(data: dict, options={}, index=0): - ''' Gets an estimation of the edge position. This is very similar to determine_edge_position, but provides instead a quick and dirty way where the actual data point closest to the maximum of the differentiated data - is located. ''' - - required_options = ['log','logfile', 'periods'] - default_options = { - 'log': False, # Toggles logging on/off - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_edge_position_estimation.log', # Sets path to log-file - 'periods': 2, #Periods needs to be an even number for the shifting of values to work properly - } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - #making new dataframe to keep the differentiated data - df_diff = pd.DataFrame(data['xanes_data']["ZapEnergy"]) - df_diff[data['path'][index]]=data['xanes_data'][data['path'][index]].diff(periods=options['periods']) - - #shifting column values up so that average differential fits right between the points used in the calculation - df_diff[data['path'][index]]=df_diff[data['path'][index]].shift(-int(options['periods']/2)) - - - if 'pre_edge_masks' in options.keys(): - for mask in options['pre_edge_masks']: - df_diff[data['path'][index]].loc[(df_diff['ZapEnergy'] > mask[0]) & (df_diff['ZapEnergy'] < mask[1])] = 0 - - if 'post_edge_masks' in options.keys(): - for mask in options['post_edge_masks']: - df_diff[data['path'][index]].loc[(df_diff['ZapEnergy'] > mask[0]) & (df_diff['ZapEnergy'] < mask[1])] = 0 - - if 'edge_masks' in options.keys(): - for mask in options['edge_masks']: - df_diff[data['path'][index]].loc[(df_diff['ZapEnergy'] > mask[0]) & (df_diff['ZapEnergy'] < mask[1])] = 0 - - df_diff_max = df_diff[data['path'][index]].dropna().max() - - estimated_edge_pos = df_diff.loc[df_diff[data['path'][index]] == df_diff_max,'ZapEnergy'].values[0] - - if options['log']: - aux.write_log(message=f'Estimated edge position is: {estimated_edge_pos} keV', options=options) - - return estimated_edge_pos - -def determine_edge_position(data: dict, options={}): - ''' Determines the edge position by 1) first differential maximum and/or 2) second differential zero-point. Calculates differential and/or double differential by diff.periods and double_diff.periods respectively. - The differentiated and/or doubly differentiated data is fitted to a polynomial of diff.polyorder and/or double_diff.polyorder around the estimated edge position. The estimated edge position is set to be the x-value of the data - point at maximum of the differentiated data. The region to be fitted to the polynomial is determined by fit_region, which defaults to 5 times the distance between two data points, giving five data points to fit to. - - Allows plotting and saving of three plots to assess the quality of the fit, and also allows logging. - - Requires that XANES-data is already loaded in data['xanes_data']. This allows the user to choose when to determine the edge position - whether before or after normalisation, flattening etc.''' - - required_options = ['save_values', 'log', 'logfile', 'show_plots', 'save_plots', 'save_folder', 'diff', 'diff.polyorder', 'diff.periods', 'double_diff', 'double_diff.polyorder', 'double_diff.periods', 'points_around_edge', 'save_diff_data'] - default_options = { - 'save_values': True, # Whether the edge positions should be stored in a dictionary within the main data dictionary. - 'log': False, # Toggles logging on/off - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_determine_edge_position.log', # Sets the path to the logfile. Ignored if log == False - 'show_plots': False, # Toggles on/off whether plots should be shown. For sequential data, saving the plots and inspecting them there is probably better. - 'save_plots': False, # Toggles on/off whether plots should be saved. - 'save_folder': './', # Sets the path to where the plots should be saved. Creates folder if doesn't exist. Ignored if save_plots == False - 'edge_masks': [], - 'diff': True, # Toggles calculation of the edge position based on differential data - 'diff.polyorder': 2, # Sets the order of the polynomial to fit edge region of the differential to - 'diff.periods': 2, # Sets the number of data points between which the first order difference should be calculated. Needs to be even for subsequent shifting of data to function. - 'double_diff': False, # Toggles calculation of the edge position based on double differential data - 'double_diff.polyorder': 1, # Sets the order of the polynomial to fit edge region of the double differential to - 'double_diff.periods': 2, # Sets the number of data points between which the second order difference should be calculated. Needs to be even for subsequent shifting of data to function. - 'points_around_edge': 1, # The length of the region to find points to fit to a function - 'save_diff_data': False - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - # Check if periods are even - if options['diff'] and options['diff.periods'] % 2 != 0: - if options['log']: - aux.write_log(message='Periods for differentiation is not even. Ending run.', options=options) - raise Exception("NB! Periods needs to be an even number for the shifting of values to work properly") - if options['double_diff'] and options['double_diff.periods'] % 2 != 0: - aux.write_log(message='Periods for double differentiation is not even. Ending run.', options=options) - raise Exception("NB! Periods needs to be an even number for the shifting of values to work properly") - - - if options['interactive']: - data['xanes_data_backup'] = data['xanes_data'] - options['interactive'] = False - options['interactive_session_active'] = True - options['show_plots'] = True - determine_edge_position_interactive(data=data, options=options) - return - - - - - # Prepare dataframes for differential data - if options['diff']: - df_diff = pd.DataFrame(data['xanes_data']['ZapEnergy']) - if options['double_diff']: - df_double_diff = pd.DataFrame(data['xanes_data']['ZapEnergy']) - if options['save_values']: - data['e0_diff'] = {} - data['e0_double_diff'] = {} - - - if options['log']: - aux.write_log(message='Starting edge position determination', options=options) - - - # Get rough estimate of edge position - for i, filename in enumerate(data['path']): - - estimated_edge_pos = estimate_edge_position(data, options=options, index=i) - - - fit_region = (options['points_around_edge']+1)*(data['xanes_data']['ZapEnergy'].iloc[1] - data['xanes_data']['ZapEnergy'].iloc[0]) - - if fit_region < 0: - fit_region = (options['points_around_edge']+1)*(data['xanes_data']['ZapEnergy'].iloc[10] - data['xanes_data']['ZapEnergy'].iloc[9]) - - - #========================== Fitting the first order derivative ========== - - if options['diff']: - df_diff[filename] = data['xanes_data'][filename].diff(periods=options['diff.periods']) - df_diff[filename]=df_diff[filename].shift(-int(options['diff.periods']/2)) # Shifts the data back so that the difference between the points is located in the middle of the two points the caluclated difference is between - - # Picks out the points to be fitted - df_diff_edge = df_diff.loc[(df_diff["ZapEnergy"] <= estimated_edge_pos+fit_region) & ((df_diff["ZapEnergy"] >= estimated_edge_pos-fit_region))] - - - # Fitting a function to the chosen interval - params = np.polyfit(df_diff_edge["ZapEnergy"], df_diff_edge[filename], options['diff.polyorder']) - diff_function = np.poly1d(params) - - x_diff=np.linspace(df_diff_edge["ZapEnergy"].iloc[0],df_diff_edge["ZapEnergy"].iloc[-1],num=10000) - y_diff=diff_function(x_diff) - - df_diff_fit_function = pd.DataFrame(x_diff) - df_diff_fit_function['y_diff'] = y_diff - df_diff_fit_function.columns = ['x_diff', 'y_diff'] - - # Picks out the x-value where the y-value is at a maximum - edge_pos_diff=x_diff[np.where(y_diff == np.amax(y_diff))][0] - - if options['log']: - aux.write_log(message=f"... Edge position of {os.path.basename(filename)} determined by the differential maximum is: {str(round(edge_pos_diff,5))} keV", options=options) - - if options['save_values']: - data['e0_diff'][filename] = edge_pos_diff - - #========================== Fitting the second order derivative ========== - if options['double_diff']: - df_double_diff[filename] = data['xanes_data'][filename].diff(periods=options['double_diff.periods']).diff(periods=options['double_diff.periods']) - df_double_diff[filename]=df_double_diff[filename].shift(-int(options['double_diff.periods'])) - - # Pick out region of interest - df_double_diff_edge = df_double_diff.loc[(df_double_diff["ZapEnergy"] < estimated_edge_pos+fit_region) & ((df_double_diff["ZapEnergy"] > estimated_edge_pos-fit_region))] - - # Fitting a function to the chosen interval - params = np.polyfit(df_double_diff_edge["ZapEnergy"], df_double_diff_edge[filename], options['double_diff.polyorder']) - double_diff_function = np.poly1d(params) - - x_double_diff=np.linspace(df_double_diff_edge["ZapEnergy"].iloc[0], df_double_diff_edge["ZapEnergy"].iloc[-1],num=10000) - y_double_diff=double_diff_function(x_double_diff) - - df_double_diff_fit_function = pd.DataFrame(x_double_diff) - df_double_diff_fit_function['y_diff'] = y_double_diff - df_double_diff_fit_function.columns = ['x_diff', 'y_diff'] - - - # Picks out the x-value where the y-value is closest to 0 - edge_pos_double_diff=x_double_diff[np.where(y_double_diff == find_nearest(y_double_diff,0))][0] - - if options['log']: - aux.write_log(message=f"... Edge position of {os.path.basename(filename)} determined by the double differential zero-point is {str(round(edge_pos_double_diff,5))} keV", options=options) - - if options['diff']: - aux.write_log(message=f"... Difference between edge position estimated from differential maximum and double differential zero-point is {(edge_pos_diff-edge_pos_double_diff)*1000} eV.", options=options) - - if options['save_values']: - data['e0_double_diff'][filename] = edge_pos_double_diff - - - # Make and show / save plots ... - if options['save_plots'] or options['show_plots']: - - - # ... if both are enabled - if options['diff'] and options['double_diff']: - - _, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(ncols=3, nrows=2, figsize=(20,20)) - data['xanes_data'].plot(x='ZapEnergy', y=filename, ax=ax1, c='black') - ax1.axvline(x=edge_pos_diff, ls='--', c='green') - - df_diff.plot(x='ZapEnergy', y=filename, ax=ax2, kind='scatter') - df_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax2) - ax2.set_xlim([edge_pos_diff-fit_region*1.5, edge_pos_diff+fit_region*1.5]) - ax2.axvline(x=estimated_edge_pos-fit_region, ls='--', c='black') - ax2.axvline(x=edge_pos_diff, ls='--', c='green') - ax2.axvline(x=estimated_edge_pos+fit_region, ls='--', c='black') - ax2.set_title('Fit region of differentiated data') - - df_diff_edge.plot(x='ZapEnergy', y=filename, ax=ax3, kind='scatter') - df_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax3) - ax3.axvline(x=edge_pos_diff, ls='--', c='green') - ax3.axvline(x=estimated_edge_pos, ls='--', c='red') - ax3.set_title('Fit of differentiated data') - - - data['xanes_data'].plot(x='ZapEnergy', y=filename, ax=ax4, c='black') - ax4.axvline(x=edge_pos_double_diff, ls='--', c='green') - - df_double_diff.plot(x='ZapEnergy', y=filename, ax=ax5, kind='scatter') - df_double_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax5) - ax5.set_xlim([edge_pos_double_diff-0.0015, edge_pos_double_diff+0.0015]) - ax5.axvline(x=estimated_edge_pos-fit_region, ls='--', c='black') - ax5.axvline(x=edge_pos_double_diff, ls='--', c='green') - ax5.axvline(x=estimated_edge_pos+fit_region, ls='--', c='black') - - df_double_diff_edge.plot(x='ZapEnergy', y=filename, ax=ax6, kind='scatter') - df_double_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax6) - ax6.axvline(x=edge_pos_double_diff, ls='--', c='green') - ax6.axvline(x=estimated_edge_pos, ls='--', c='red') - - - # ... if only first order differentials is enabled - elif options['diff']: - _, (ax1, ax2, ax3) = plt.subplots(ncols=3,nrows=1, figsize=(20, 10)) - - data['xanes_data'].plot(x='ZapEnergy', y=filename, ax=ax1, c='black') - ax1.axvline(x=edge_pos_diff, ls='--', c='green') - - df_diff.plot(x='ZapEnergy', y=filename, ax=ax2, kind='scatter') - df_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax2) - ax2.set_xlim([edge_pos_diff-fit_region*1.5, edge_pos_diff+fit_region*1.5]) - ax2.axvline(x=edge_pos_diff-fit_region, ls='--', c='black') - ax2.axvline(x=edge_pos_diff, ls='--', c='green') - ax2.axvline(x=edge_pos_diff+fit_region, ls='--', c='black') - - df_diff_edge.plot(x='ZapEnergy', y=filename, ax=ax3) - df_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax3) - ax3.axvline(x=edge_pos_diff, ls='--', c='green') - ax3.axvline(x=estimated_edge_pos, ls='--', c='red') - - # ... if only second order differentials is enabled - elif options['double_diff']: - _, (ax1, ax2, ax3) = plt.subplots(ncols=3,nrows=1, figsize=(20, 10)) - - data['xanes_data'].plot(x='ZapEnergy', y=filename, ax=ax1, c='black') - ax1.axvline(x=edge_pos_double_diff, ls='--', c='green') - - df_double_diff.plot(x='ZapEnergy', y=filename, ax=ax2, kind='scatter') - df_double_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax2) - ax2.set_xlim([edge_pos_double_diff-fit_region*1.5, edge_pos_double_diff+fit_region*1.5]) - ax2.axvline(x=edge_pos_double_diff-fit_region, ls='--', c='black') - ax2.axvline(x=edge_pos_double_diff, ls='--', c='green') - ax2.axvline(x=edge_pos_double_diff+fit_region, ls='--', c='black') - - df_double_diff_edge.plot(x='ZapEnergy', y=filename, ax=ax3) - df_double_diff_fit_function.plot(x='x_diff', y='y_diff', ax=ax3) - ax3.axvline(x=edge_pos_double_diff, ls='--', c='green') - ax3.axvline(x=estimated_edge_pos, ls='--', c='red') - - - # Save plots if toggled - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_edge_position.png' - - plt.savefig(dst, transparent=False) - - - # Close plots if show_plots not toggled - if not options['show_plots']: - plt.close() - - - if not options['diff']: - edge_pos_diff = None - if not options['double_diff']: - edge_pos_double_diff = None - - - if options['save_diff_data']: - data['diff_data'] = df_diff if options['diff'] else None - data['double_diff_data'] = df_double_diff if options['double_diff'] else None - - return edge_pos_diff, edge_pos_double_diff - - - -def determine_edge_position_interactive(data: dict, options: dict) -> None: - ''' Defines the widgets to use with the ipywidgets interactive mode and calls the update function found in btp.ipywidgets. ''' - - w = widgets.interactive( - btp.ipywidgets_update, func=widgets.fixed(determine_edge_position), data=widgets.fixed(data), options=widgets.fixed(options), - points_around_edge=widgets.IntSlider(value=options['points_around_edge'], min=1, max=20, step=1), - ) - - options['widget'] = w - - display(w) - -def determine_edge_shift(data: dict, options: dict, edge_pos: float) -> None: - - if 'edge' not in data.keys(): - data['edge'] = find_element(data) - - - reference_energy = xas.edges.K['keV'].loc[xas.edges.K['Atom'] == data['edge']].values[0] - - edge_shift = reference_energy - edge_pos - - if options['log']: - aux.write_log(message=f'Edge shift vs. reference value for {data["edge"]} is {edge_shift*1000} eV', options=options) - - return edge_shift - -def normalise(data: dict, options={}): - ''' Normalises the data so that the difference between the fitted pre- and post-edge functions is 1 at the edge position. - - Requires that edge positions have already been determined with determine_edge_position() and stored in data['e0_diff']. ''' - - - required_options = ['log', 'logfile', 'normalisation_store_data'] - default_options = { - 'log': False, # Toggles logging on/off - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_normalisation.log', # Sets path to log-file - 'show_plots': False, # Toggles on/off whether plots should be shown. For sequential data, saving the plots and inspecting them there is probably better. - 'save_plots': False, # Toggles on/off whether plots should be saved. - 'save_folder': './', # Sets the path to where the plots should be saved. Creates folder if doesn't exist. Ignored if save_plots == False - 'normalisation_store_data': False, # Toggles storing of the flattened data in data['xanes_data'] on/off - } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - normalised_df = pd.DataFrame(data['xanes_data']['ZapEnergy']) - data['normalisation_constants'] = {} - - if options['normalisation_store_data']: - pre_edge_fit_data_norm = pd.DataFrame(data['pre_edge_fit_data']['ZapEnergy']) - post_edge_fit_data_norm = pd.DataFrame(data['post_edge_fit_data']['ZapEnergy']) - - #Finding the normalisation constant µ_0(E_0), by subtracting the value of the pre-edge-line from the value of the post-edge line at e0 - for filename in data['path']: - e0_ind = data['post_edge_fit_data'].loc[data['post_edge_fit_data']['ZapEnergy'] == find_nearest(data['post_edge_fit_data']['ZapEnergy'], data['e0_diff'][filename])].index.values[0] - - #norm = data['post_edge_fit_data'][filename].iloc[find_nearest(data['post_edge_fit_data'][filename], data['e0'][filename])] - normalisation_constant = data['post_edge_fit_data'][filename].iloc[e0_ind] - data['pre_edge_fit_data'][filename].iloc[e0_ind] - normalised_df.insert(1, filename, data['xanes_data'][filename] / normalisation_constant) - - - if options['show_plots'] or options['save_plots']: - - fig, ax = plt.subplots(figsize=(10,5)) - - normalised_df.plot(x='ZapEnergy', y=filename, ax=ax, color='red', label='Normalised data') - ax.set_title(f'{os.path.basename(filename)} - After normalisation', size=20) - ax.set_ylabel('Normalised x$\mu$(E)', size=20) - ax.set_xlabel('Energy (keV)', size=20) - ax.axhline(y=1, ls='--', c='black') - - - # Save plots if toggled - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_normalisation.png' - - plt.savefig(dst, transparent=False) - - - # Close plots if show_plots not toggled - if not options['show_plots']: - plt.close() - - - if options['normalisation_store_data']: - pre_edge_fit_data_norm.insert(1, filename, data['pre_edge_fit_data'][filename] / normalisation_constant) - post_edge_fit_data_norm.insert(1, filename, data['post_edge_fit_data'][filename] / normalisation_constant) - - - - - - if options['normalisation_store_data']: - data['xanes_data'] = normalised_df - # Normalise the pre-edge and post-edge fit function data - data['pre_edge_fit_data_norm'] = pre_edge_fit_data_norm - data['post_edge_fit_data_norm'] = post_edge_fit_data_norm - - data['normalisation_constants'][filename] = normalisation_constant - - - return normalised_df - - -def flatten(data:dict, options={}): - ''' Flattens the post-edge region (from edge position and up). Only for visual purposes. - - Requires data['xanes_data'] that is normalised through normalise() and that normalised versions of the post_edge_fit_data is stored in data['post_edge_fit_data_norm']. - Also assumes that the pre edge-fit data is already subtracted from the data''' - - - required_options = ['log', 'logfile', 'flatten_store_data'] - default_options = { - 'log': False, # Toggles logging on/off - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_flattening.log', # Sets path to log-file - 'flatten_store_data': False, # Toggles storing of the flattened data in data['xanes_data'] on/off - } - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - # Initialise DataFrame with x-values - flattened_df = pd.DataFrame(data['xanes_data']['ZapEnergy']) - - # Loop through all files - for filename in data['path']: - - # Subtract 1 from the _normalised_ post edge fit function - fit_function_diff = data['post_edge_fit_data_norm'][filename] - 1 - data['pre_edge_fit_data_norm'][filename] - - # Set all values from edge position and downwards to 0 so that only data above the edge position will be adjusted - fit_function_diff.loc[flattened_df['ZapEnergy'] <= data['e0_diff'][filename]] = 0 - - # Subtract the difference between 1 and the post edge fit function from the normalised data. - flattened_df[filename] = data['xanes_data'][filename] - fit_function_diff - - - if options['show_plots'] or options['save_plots']: - - fig, ax = plt.subplots(figsize=(10,5)) - - flattened_df.plot(x='ZapEnergy', y=filename, ax=ax, color='red', label='Flattened data') - ax.set_title(f'{os.path.basename(filename)} - After flattening', size=20) - ax.set_ylabel('Normalised x$\mu$(E)', size=20) - ax.set_xlabel('Energy (keV)', size=20) - ax.axhline(y=1, ls='--', c='black') - - - # Save plots if toggled - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_flattened.png' - - plt.savefig(dst, transparent=False) - - - # Close plots if show_plots not toggled - if not options['show_plots']: - plt.close() - - - # Saves the flattened DataFrame - if options['flatten_store_data']: - data['xanes_data'] = flattened_df - - - return flattened_df, fit_function_diff - - -def extract_partial_range(data: dict, options={}): - - default_options = { - 'extract_range': None, - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - if not options['extract_range']: - warnings.warn('You did not specify a range - do so with the keyword "extract_range" in the options dictionary. Returning data without modification') - return data - - - partial_data = data['xanes_data'].loc[(data['xanes_data']['ZapEnergy'] > options['extract_range'][0]) & (data['xanes_data']['ZapEnergy'] < options['extract_range'][1])] - - return partial_data - - - - -def fit_pre_edge_feautre(data: dict, options={}) -> pd.DataFrame: - - from scipy.interpolate import UnivariateSpline - from scipy.optimize import curve_fit - from scipy.stats import norm - - default_options = { - 'remove_background': True, - 'background_model': 'exponential', - 'peak_model': 'gaussian', - 'extract_range': None, - 'extract_range_increments': [0, 0], - 'background_limits': [None, None], - 'background_limits_increments': [0, 0], - 'background_polyorder': 2, - 'log': False, - 'logfile': f'{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_peak_fit.log', - 'show_plots': False, - 'save_plots': False, - 'save_folder': './', - 'ylim': None, - 'xlim': None, - 'interactive': False, - 'interactive_session_active': False - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - if not options['extract_range']: - warnings.warn('You did not specify a range - do so with the keyword "extract_range" in the options dictionary. No modification is done.') - return None, None, None - - if options['log']: - aux.write_log(message='Starting fit of pre edge feature', options=options) - - - centroids = [] - errors = [] - for i, filename in enumerate(data['path']): - - options['extract_range'][0] += options['extract_range_increments'][0] - options['extract_range'][1] += options['extract_range_increments'][1] - - partial_data = extract_partial_range(data, options) - - removed_background_df = pd.DataFrame(partial_data["ZapEnergy"]) - background_df = pd.DataFrame(partial_data["ZapEnergy"]) - - if options['remove_background']: - if not options['background_limits'][0]: - options['background_limits'][0] = partial_data[1].max() - 0.003 - if not options['background_limits'][1]: - options['background_limits'][1] = partial_data[1].max() + 0.003 - - if i > 0: - options['background_limits'][0][0] += options['background_limits_increments'][0] - options['background_limits'][0][1] += options['background_limits_increments'][0] - options['background_limits'][1][0] += options['background_limits_increments'][1] - options['background_limits'][1][1] += options['background_limits_increments'][1] - - peak_background = partial_data.copy() - - #peak_background.loc[(peak_background['ZapEnergy'] > options['background_limits'][0]) & (peak_background['ZapEnergy'] < options['background_limits'][1])] = np.nan - peak_background.loc[(peak_background['ZapEnergy'] < options['background_limits'][0][0]) | - ((peak_background['ZapEnergy'] > options['background_limits'][0][1]) & - (peak_background['ZapEnergy'] < options['background_limits'][1][0])) | - (peak_background['ZapEnergy'] > options['background_limits'][1][1])] = np.nan - peak_background = peak_background.dropna() - - - # FIXME Originally tried with spline and polynomials, but they worked very poorly. This is as best as it gets at this moment, but alternatives should be considered. - if options['background_model'] == 'exponential': - def linear(x, a, b): - return a*x + b - - # Fit linear curve to the logarithm of the background - popt, pcov = curve_fit(linear, peak_background['ZapEnergy'], np.log(peak_background[filename])) - #fit_function = np.poly1d(popt) - - # Restore exponential nature of background - background = np.exp(linear(background_df['ZapEnergy'], *popt)) - background_df.insert(1,filename,background) - - removed_background_df.insert(1, filename, partial_data[filename]-background_df[filename]) - removed_background_df = removed_background_df.loc[(removed_background_df['ZapEnergy'] > options['background_limits'][0][1]) & - (removed_background_df['ZapEnergy'] < options['background_limits'][1][0]) - ] - - - elif options['background_model'] == 'arctan': - - popt, pcov = curve_fit(arctan, peak_background['ZapEnergy']-data['e0_diff'][filename], peak_background[filename]) - - background = arctan(background_df['ZapEnergy']-data['e0_diff'][filename], *popt) - background_df.insert(1, filename, background) - - - removed_background_df.insert(1, filename, partial_data[filename]-background_df[filename]) - removed_background_df = removed_background_df.loc[(removed_background_df['ZapEnergy'] > options['background_limits'][0][1]) & - (removed_background_df['ZapEnergy'] < options['background_limits'][1][0]) - ] - - - - # Fit Gaussian - # FIXME Should have options for Lorentzian and Pseudo-Voigt here as well. - - # FIXME Understand what this deprecation warning means and what changes should be made to make it future proof - warnings.filterwarnings(action='ignore', category=np.VisibleDeprecationWarning) - - - mu_init = float(removed_background_df['ZapEnergy'].loc[removed_background_df[filename] == removed_background_df[filename].max()]) - - if options['peak_model'] == 'gaussian': - popt, pcov = curve_fit(gauss, - removed_background_df['ZapEnergy'], - removed_background_df[filename], - p0=[0.0005, mu_init, 0.001], - bounds=[ - (0, mu_init-0.002, -np.inf), - (0.5, mu_init+0.002, np.inf) - ] - ) - - - elif options['peak_model'] == '2gaussian': - popt, pcov = curve_fit(_2gauss, - removed_background_df['ZapEnergy'], - removed_background_df[filename], - p0=[0.0005, mu_init, 0.001, 0.0005, mu_init+0.003, 0.0005], - #bounds=[ - # (0, mu_init-0.002, -np.inf), - # (0.5, mu_init+0.002, np.inf) - # ] - #) - ) - - elif options['peak_model'] == 'lorentzian': - - popt, pcov = curve_fit(lorentz, - removed_background_df['ZapEnergy'], - removed_background_df[filename], - p0=[1, mu_init, 0.001], - # bounds=[ - # (mu_init-0.001, 0.00001), - # (mu_init+0.001, 0.01) - # ] - ) - - - elif options['peak_model'] == '2lorentzian': - - popt, pcov = curve_fit(_2lorentz, - removed_background_df['ZapEnergy'], - removed_background_df[filename], - p0=[1, mu_init, 0.001, 1, mu_init+0.003, 0.001], - # bounds=[ - # (mu_init-0.001, 0.00001), - # (mu_init+0.001, 0.01) - # ] - ) - - - - elif options['peak_model'] == 'pseudo-voigt': - - popt, pcov = curve_fit(pseudo_voigt, - removed_background_df['ZapEnergy'], - removed_background_df[filename], - p0=[1, mu_init, 0.001, 0.5] - ) - - - centroids.append(popt) - errors.append(pcov) - - - if options['show_plots'] or options['save_plots']: - - fig, axes = plt.subplots(figsize=(10,5), ncols=2) - - - # Background removal - partial_data.plot(x='ZapEnergy', y=filename, ax=axes[0], color='black', label='Original data', kind='scatter') - background_df.plot(x='ZapEnergy', y=filename, ax=axes[0], color='black', ls='--', label='Fitted background') - removed_background_df.plot(x='ZapEnergy', y=filename, ax=axes[0], color='red', label='Background subtracted', kind='scatter') - axes[0].axvline(x=options['background_limits'][0][0], color='black', ls='--') - axes[0].axvline(x=options['background_limits'][0][1], color='black', ls='--') - axes[0].axvline(x=options['background_limits'][1][0], color='black', ls='--') - axes[0].axvline(x=options['background_limits'][1][1], color='black', ls='--') - axes[0].set_title(f'{os.path.basename(filename)} - Background removal', size=10) - - peak_background.plot(x='ZapEnergy', y=filename, ax=axes[0], color='green', kind='scatter') - if options['xlim']: - axes[0].set_xlim(options['xlim']) - if options['ylim']: - axes[0].set_ylim(options['ylim']) - - axes[0].set_ylabel('Normalised x$\mu$(E)', size=20) - axes[0].set_xlabel('Energy (keV)', size=20) - axes[0].axhline(y=0, ls='--', color='black') - - # Fitted curve - - if options['peak_model'] == 'gaussian': - y_fit = gauss(removed_background_df['ZapEnergy'], *popt) - components = [y_fit] - - elif options['peak_model'] == 'lorentzian': - y_fit = lorentz(removed_background_df['ZapEnergy'], *popt) - components = [y_fit] - - elif options['peak_model'] == 'pseudo-voigt': - y_fit = pseudo_voigt(removed_background_df['ZapEnergy'], *popt) - components = [y_fit] - - elif options['peak_model'] == '2gaussian': - y_fit = _2gauss(removed_background_df['ZapEnergy'], *popt) - y_fit1 = gauss(removed_background_df['ZapEnergy'], *popt[0:3]) - y_fit2 = gauss(removed_background_df['ZapEnergy'], *popt[3:6]) - - components = [y_fit1, y_fit2] - - elif options['peak_model'] == '2lorentzian': - y_fit = _2lorentz(removed_background_df['ZapEnergy'], *popt) - y_fit1 = lorentz(removed_background_df['ZapEnergy'], *popt[0:3]) - y_fit2 = lorentz(removed_background_df['ZapEnergy'], *popt[3:6]) - - components = [y_fit1, y_fit2] - - removed_background_df.plot(x='ZapEnergy', y=filename, ax=axes[1], color='black', label='Background subtracted', kind='scatter') - - axes[1].plot(removed_background_df['ZapEnergy'], y_fit, color='red', label=f'Fit data ({options["peak_model"]})') - for comp in components: - axes[1].fill_between(x=removed_background_df['ZapEnergy'], y1=comp, y2=0, alpha=0.2) - - - residuals = (removed_background_df[filename] - y_fit) - 0.1*removed_background_df[filename].max() - axes[1].scatter(x=removed_background_df['ZapEnergy'], y=residuals) - axes[1].axhline(y=-0.1*removed_background_df[filename].max(), ls='--', color='black') - - axes[1].set_title(f'{os.path.basename(filename)} - Pre-edge feature fit', size=10) - axes[1].set_ylabel('Normalised x$\mu$(E)', size=20) - axes[1].set_xlabel('Energy (keV)', size=20) - axes[1].legend() - axes[1].axhline(y=0, ls='--', color='black') - - - if options['xlim']: - axes[1].set_xlim(options['xlim']) - - - # Save plots if toggled - if options['save_plots']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - dst = os.path.join(options['save_folder'], os.path.basename(filename)) + '_pre_edge_feature_fit.png' - - plt.savefig(dst, transparent=False) - - - # Close plots if show_plots not toggled - if not options['show_plots']: - plt.close() - - return centroids, errors, removed_background_df - - - - -def gauss(x, A, mu, sigma): - return (A/(sigma*np.sqrt(np.pi)))*np.exp(-(x-mu)**2/(2*sigma**2)) - -def _2gauss(x, A1, mu1, sigma1, A2, mu2, sigma2): - return (A1/(sigma1*np.sqrt(np.pi)))*np.exp(-(x-mu1)**2/(2*sigma1**2))+(A2/(sigma2*np.sqrt(np.pi)))*np.exp(-(x-mu2)**2/(2*sigma2**2)) - - -def lorentz(x, A, mu, sigma): - return (A/np.pi * ((sigma)/(((x-mu)**2) + (sigma)**2))) - -def _2lorentz(x, A1, mu1, sigma1, A2, mu2, sigma2): - return (A1/np.pi * ((sigma1)/(((x-mu1)**2) + (sigma1)**2))) + (A2/np.pi * ((sigma2)/(((x-mu2)**2) + (sigma2)**2))) - - -def pseudo_voigt(x, A, mu, sigma, eta): - - G = gauss(x, A, mu, sigma) - L = lorentz(x, A, mu, sigma) - - return eta*G + (1-eta)*L - -def arctan(x,a,b,c,d): - return a*np.arctan(x*b+c) + d - - - - - -def save_data(data, options={}): - - default_options = { - 'save_data': 'xanes_data', - 'save_folder': '.' - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - filenames = [filename for filename in data[options["save_data"]].columns if not 'ZapEnergy' in filename] - - for filename in filenames: - - options['save_filename'] = os.path.basename(filename).split('.')[0] + '_exported.dat' - - save_path = os.path.join(options['save_folder'], options['save_filename']) - - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - to_export = data[options['save_data']][['ZapEnergy', filename]] - to_export.columns = ['E', 'I'] - to_export.to_csv(save_path) - - - - - - -def save_centroids(data: dict, options={}): - - default_options = { - 'save_path': 'centroids.dat', - 'overwrite': False, - 'append': False, - } - - options = aux.update_options(options=options, default_options=default_options) - - if options['overwrite']: - mode = 'w' - elif options['append']: - mode = 'a' - else: - mode = False - - if os.path.exists(options['save_path']) and not options['overwrite']: - with open(options['save_path'], 'r') as f: - reference = float(f.readline().split()[1]) - - else: - reference = data['centroid_fit'][0][1]*1000 - - - if not os.path.exists(os.path.dirname(options['save_path'])): - os.makedirs(os.path.dirname(options['save_path'])) - - - - if mode: - with open(options['save_path'], mode) as f: - for path, fit, error in zip(data['path'], data['centroid_fit'], data['centroid_fit_errors']): - - A = fit[0] - mu = fit[1] - sigma = fit[2] - mu_adj = (fit[1]*1000)-reference - - stddevs = np.sqrt(np.diag(error)) - - #f.write(f'{path} \t {fit[1]*1000} \t {(fit[1]-reference)*1000} \t {fit[0]} \t {fit[2]} \n') - f.write('{: <40} \t {: <25} \t {: <25} \t {: <25} \t {: <25} \t {: <25} \t {: <25} \t {: <25}\n'.format(path, mu*1000, mu_adj, A, sigma, stddevs[1]*1000, stddevs[0], stddevs[2])) - - - -def read_centroids(path): - - df = pd.read_csv(path, delim_whitespace=True, header=None) - - df.columns = ['scan', 'mu', 'mu_adj', 'A', 'sigma', 'mu_err', 'A_err', 'sigma_err'] - - return df - - diff --git a/nafuma/xanes/edges.py b/nafuma/xanes/edges.py deleted file mode 100644 index abce1c6..0000000 --- a/nafuma/xanes/edges.py +++ /dev/null @@ -1,30 +0,0 @@ -import pandas as pd -import numpy as np -from scipy.constants import c, h - -# From 2019 redefinition of SI base units: https://en.wikipedia.org/wiki/2019_redefinition_of_the_SI_base_units -keV_per_J = (1 / 1.602176634e-19) / 1000 - -# kXu values taken from International Tables for Crystallography Volume , Kulwer Academic Publishers - Dordrect / Boston / London (1992) -K = { 'Z': [ 1, 2, - 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, - 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48], - 'Atom': [ 'H', 'He', - 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', - 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', - 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', - 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd'], - 'kXu': [ np.nan, np.nan, - 226.5, np.nan, np.nan, 43.68, 30.99, 23.32, np.nan, np.nan, - np.nan, 9.5117, 7.9511, 6.7446, 5.7866, 5.0182, 4.3969, 3.8707, - 3.43645, 3.07016, 2.7573, 2.49730, 2.26902, 2.07012, 1.89636, 1.74334, 1.60811, 1.48802, 1.38043, 1.2833, 1.19567, 1.11652, 1.04497, 0.97978, 0.91995, 0.86547, - 0.81549, 0.76969, 0.72762, 0.68877, 0.65291, 0.61977, 0.5891, 0.56047, 0.53378, 0.50915, 0.48582, 0.46409]} - - -K = pd.DataFrame(K) -K['keV'] = np.round(h*c/(K['kXu']*10**-10) * keV_per_J, 3) - - -# FIXME If needed, add energies for L-edges as well. \ No newline at end of file diff --git a/nafuma/xanes/io.py b/nafuma/xanes/io.py deleted file mode 100644 index 30b6759..0000000 --- a/nafuma/xanes/io.py +++ /dev/null @@ -1,523 +0,0 @@ -import pandas as pd -import matplotlib.pyplot as plt -import os -import numpy as np -import nafuma.auxillary as aux -from nafuma.xanes.calib import find_element -import datetime - -def split_scan_data(data: dict, options={}) -> list: - ''' Splits a XANES-file from BM31 into different files depending on the edge. Has the option to add intensities of all scans of same edge into the same file. - As of now only picks out xmap_rois (fluoresence mode) and for Mn, Fe, Co and Ni K-edges.''' - - required_options = ['log', 'logfile', 'save', 'save_folder', 'replace', 'active_roi', 'add_rois', 'return', 'skip_if_no_roi'] - - default_options = { - 'log': False, - 'logfile': f'{datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_split_edges.log', - 'save': False, # whether to save the files or not - 'save_folder': '.', # root folder of where to save the files - 'replace': False, # whether to replace the files if they already exist - 'active_roi': None, - 'add_rois': False, # Whether to add the rois of individual scans of the same edge together - 'return': True, - 'skip_if_no_roi': True - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - all_scans = [] - - if options['log']: - aux.write_log(message='Starting file splitting...', options=options) - - for filename in data['path']: - - if options['log']: - aux.write_log(message=f'Reading {filename}...', options=options) - - with open(filename, 'r') as f: - lines = f.readlines() - - timestamps = [] - scan_datas, scan_data = [], [] - headers, header = [], '' - read_data = False - - for i, line in enumerate(lines): - # Header line starts with #L - reads headers, and toggles data read-in on - if 'zapline mono' in line: - timestamps.append(lines[i+1].strip('#D')) - - elif line[0:2] == "#L": - header, read_data = line[2:].split(), True - - if options['log']: - aux.write_log(message='... Found scan data. Starting read-in...', options=options) - continue - - # First line after data started with #C - stops data read-in - elif line[0:2] == "#C" or line[0:2] == '#S': - read_data = False - - if scan_data: - scan_datas.append(scan_data); scan_data = [] - - if header: - headers.append(header); header = '' - - # Ignore line if read-in not toggled - if read_data == False: - continue - - # Read in data if it is - else: - scan_data.append(line.split()) - - - edges = {'Mn': [], 'Fe': [], 'Co': [], 'Ni': []} - - - for i, scan_data in enumerate(scan_datas): - - if 'ZapEnergy' not in headers[i]: - if options['log']: - aux.write_log(message=f'... No valid scan data found... ({i+1}/{len(scan_datas)})', options=options) - continue - - xanes_df = pd.DataFrame(scan_data).apply(pd.to_numeric) - xanes_df.columns = headers[i] - - - edge = find_element({'xanes_data_original': xanes_df}) - - - - if options['log']: - aux.write_log(message=f'... Starting data clean-up ({edge}-edge)... ({i+1}/{len(scan_datas)})', options=options) - - - if not ('xmap_roi00' in headers[i]) and (not 'xmap_roi01' in headers[i]): - if options['skip_if_no_roi']: - if options['log']: - aux.write_log(message='... ... Did not find fluoresence data. Skipping...', options=options) - continue - if options['log']: - aux.write_log(message='... ... Did not find fluoresence data, but still proceeding ...', options=options) - - - - - - edges[edge].append(xanes_df) - - - if options['add_rois']: - - if options['log']: - aux.write_log(message=f'... Addition of rois enabled. Starting addition...', options=options) - - added_edges = {'Mn': [], 'Fe': [], 'Co': [], 'Ni': []} - for edge, scans in edges.items(): - - if options['log']: - aux.write_log(message=f'... ... Adding rois of the {edge}-edge...', options=options) - - if scans: - xanes_df = scans[0] - - for i, scan in enumerate(scans): - if i > 0: - - if options['log']: - aux.write_log(message=f'... ... ... Adding {i+1}/{len(scans)}', options=options) - - if 'xmap_roi00' in xanes_df.columns: - xanes_df['xmap_roi00'] += scan['xmap_roi00'] - if 'xmap_roi01' in xanes_df.columns: - xanes_df['xmap_roi01'] += scan['xmap_roi01'] - - added_edges[edge].append(xanes_df) - - edges = added_edges - - if options['save']: - #FIXME If there is something wrong with the input file, the file will not be saved but log-file still sais it is saved. Goes from "Saving data to ..." to "All done!" no matter if it fals or not. - if options['log']: - aux.write_log(message=f'... Saving data to {options["save_folder"]}', options=options) - - if not os.path.isdir(options['save_folder']): - if options['log']: - aux.write_log(message=f'... ... {options["save_folder"]} does not exist. Creating folder.', options=options) - - os.makedirs(options['save_folder']) - - - filename = os.path.basename(filename).split('.')[0] - - for edge, scans in edges.items(): - for i, scan in enumerate(scans): - count = '' if options['add_rois'] else '_'+str(i).zfill(4) - path = os.path.join(options['save_folder'], f'{filename}_{edge}{count}.dat') - - if not os.path.isfile(path): - - with open(path, 'w', newline = '\n') as f: - - f.write(f'# Time: {timestamps[i]}') - scan.to_csv(f) - - if options['log']: - aux.write_log(message=f'... ... Scan saved to {path}', options=options) - - elif options['replace'] and os.path.isfile(path): - with open(path, 'w', newline = '\n') as f: - scan.to_csv(f) - - if options['log']: - aux.write_log(message=f'... ... File already exists. Overwriting to {path}', options=options) - - elif not options['replace'] and os.path.isfile(path): - if options['log']: - aux.write_log(message=f'... ... File already exists. Skipping...', options=options) - - all_scans.append(edges) - - if options['log']: - aux.write_log(message=f'All done!', options=options) - - - if options['return']: - return all_scans - else: - return - - - -def save_data(data: dict, options={}) -> None: - - required_options = ['save_folder', 'overwrite', 'log', 'logfile', 'filename'] - - default_options = { - 'log': False, - 'logfile': f'{datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_save_files.log', - 'save_folder': 'saved_scans', - 'overwrite': False, - 'filename': f'{datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}_exported_data.dat', - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - # Check if there is any data to be saved - if not 'xanes_data' in data.keys(): - if options['log']: - aux.write_log(message=f'There is not saved scan data in data. Exiting without saving...', options=options) - - return None - - if not isinstance(data['xanes_data'], pd.DataFrame): - if options['log']: - aux.write_log(message=f'data["xanes_data"] has an invalid format. Exiting without saving...', options=options) - - return None - - - # Make folder(s) if it/they do(es)n't exist - if not os.path.exists(options['save_folder']): - if options['log']: - aux.write_log(message=f'Destination folder does not exist. Creating folder...', options=options) - - os.makedirs(options['save_folder']) - - - - if os.path.exists(os.path.join('save_folder', options['filename'])): - if not options['overwrite']: - if options['log']: - aux.write_log(message=f'File already exists and overwrite disabled. Exiting without saving...', options=options) - return None - - with open(os.path.join(options['save_folder'], options['filename']), 'w') as f: - - if 'e0_diff' in data.keys(): - f.write(f'# Number of header lines: {len(data["path"])+1} \n') - - for i, (path, e0) in enumerate(data['e0_diff'].items()): - f.write(f'# Scan_{i} \t {e0} \n') - - else: - f.write(f'# Number of header lines: {1}') - - - data['xanes_data'].to_csv(f, sep='\t', index=False) - - - #data['xanes_data'].to_csv(os.path.join(options['save_folder'], options['filename']), sep='\t', index=False) - - - -def load_data(path: str) -> dict: - # FIXME Let this function be called by read_data() if some criterium is passed - - data = {} - - - with open(path, 'r') as f: - line = f.readline() - header_lines = int(line.split()[-1]) - - if header_lines > 1: - edge_positions = [] - line = f.readline() - while line[0] == '#': - edge_positions.append(line.split()[-1]) - line = f.readline() - - data['xanes_data'] = pd.read_csv(path, sep='\t', skiprows=header_lines) - data['path'] = data['xanes_data'].columns.to_list() - data['path'].remove('ZapEnergy') - - if header_lines > 1: - data['e0_diff'] = {} - - for path, edge_position in zip(data['path'], edge_positions): - data['e0_diff'][path] = float(edge_position) - - - - return data - - -def read_data(data: dict, options={}) -> pd.DataFrame: - - - # FIXME Handle the case when dataseries are not the same size - # FIXME Add possibility to extract TIME (for operando runs) and Blower Temp (for variable temperature runs) - # FIXME Add possibility to iport transmission data - required_options = ['adjust', 'mode'] - default_options = { - 'adjust': 0, - 'mode': 'fluoresence', - 'active_roi': None - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - columns = ['ZapEnergy'] - - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - # Initialise DataFrame with only ZapEnergy-column - xanes_data = pd.read_csv(data['path'][0], skiprows=1)[['ZapEnergy']] - xanes_data['ZapEnergy'] += options['adjust'] - - - for filename in data['path']: - columns.append(filename) - - scan_data = pd.read_csv(filename, skiprows=1) - - if options['mode'] == 'fluoresence': - if not options['active_roi']: - scan_data = scan_data[[determine_active_roi(scan_data)]] - else: - scan_data = scan_data[options['active_roi']] - - elif options['mode'] == 'transmission': - scan_data = scan_data['MonEx'] / scan_data['Ion1'] - - xanes_data = pd.concat([xanes_data, scan_data], axis=1) - - - xanes_data.columns = columns - - - return xanes_data - - -def read_metadata(data: dict, options={}) -> dict: - - required_options = ['get_temperature', 'get_timestamp', 'adjust_time', 'convert_time', 'time_unit', 'reference_time'] - - default_options = { - 'get_temperature': True, - 'get_timestamp': True, - 'adjust_time': False, - 'convert_time': False, - 'reference_time': None, - 'time_unit': 's' - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - temperatures = [] - timestamps = [] - - for filename in data['path']: - scan_data = pd.read_csv(filename, skiprows=1) - - if options['get_temperature']: - temperatures.append(scan_data['ZBlower2'].mean()) - - if options['get_timestamp']: - - with open(filename, 'r') as f: - #time = f.readline().strip('# Time: ') #<-- Previous code - time = f.readline().split('# Time: ')[-1] #Hope this does not fuck you up, Rasmus - but I needed another space here - split_operator=time[-9] #This should be the operator that splits hours, minutes and seconds - if split_operator == ".": - time = datetime.datetime.strptime(time, "%a %b %d %H.%M.%S %Y ") - if split_operator == ":": - time = datetime.datetime.strptime(time, "%a %b %d %H:%M:%S %Y ") - - if options['adjust_time']: - time_elapsed = scan_data['Htime'].iloc[-1] - scan_data['Htime'].iloc[0] - - time += datetime.timedelta(microseconds=time_elapsed)/2 - - - timestamps.append(time) - - - if options['reference_time'] and options['convert_time']: - from . import unit_tables - new_times = [] - - if isinstance(options['reference_time'], str): - options['reference_time'] = datetime.datetime.strptime(options['reference_time'], "%d.%b %y %H.%M.%S") - - for time in timestamps: - new_time = (time.timestamp() - options['reference_time'].timestamp()) * unit_tables.time()['s'].loc[options['time_unit']] - - new_times.append(new_time) - - - timestamps = new_times - - - metadata = {'time': timestamps, 'temperature': temperatures} - - # Match timestamps against electrochemistry-data - # TODO This could be generalised to match up against any other dataset with timestamps. - if 'cycles' in data.keys(): - ions, specific_capacity = [], [] - i = 0 - for timestamp in timestamps: - if timestamp < 0: - ions.append(0) - - else: - closest_chg = aux.find_neighbours(value=timestamp, df=data['cycles'][i][0], colname='time') - closest_dchg = aux.find_neighbours(value=timestamp, df=data['cycles'][i][1], colname='time') - - if not isinstance(closest_chg, list): - closest_chg = [closest_chg, closest_chg] - if not isinstance(closest_dchg, list): - closest_dchg = [closest_dchg, closest_dchg] - - - if all([x==x for x in closest_chg]): - ions.append(np.mean([data['cycles'][i][0]['ions'].loc[data['cycles'][i][0].index == closest_chg[0]], data['cycles'][i][0]['ions'].loc[data['cycles'][i][0].index == closest_chg[1]]])) - specific_capacity.append(np.mean([data['cycles'][i][0]['specific_capacity'].loc[data['cycles'][i][0].index == closest_chg[0]], data['cycles'][i][0]['specific_capacity'].loc[data['cycles'][i][0].index == closest_chg[1]]])) - continue - - elif all([x==x for x in closest_dchg]): - ions.append(np.mean([data['cycles'][i][1]['ions'].loc[data['cycles'][i][1].index == closest_dchg[0]], data['cycles'][i][1]['ions'].loc[data['cycles'][i][1].index == closest_dchg[1]]])) - specific_capacity.append(np.mean([data['cycles'][i][1]['specific_capacity'].loc[data['cycles'][i][1].index == closest_dchg[0]], data['cycles'][i][1]['specific_capacity'].loc[data['cycles'][i][1].index == closest_dchg[1]]])) - continue - - elif aux.isnan(closest_chg[1]) and aux.isnan(closest_dchg[0]): - ions.append(np.nan) - specific_capacity.append(np.nan) - continue - else: - ions.append(np.nan) - specific_capacity.append(np.nan) - i += 1 - - if i > len(data['cycles'])-1: - break - - for i, (ion, cap) in enumerate(zip(ions, specific_capacity)): - if aux.isnan(ion): # if a resting step, assign a meaningful value - if i < len(ions)-1: # if resting step in the middle of the run, take the mean between the last of previous and first of next run - ions[i] = np.mean([ions[i-1], ions[i+1]]) - - else: # If last element, set to last values plus the delta between the last two previous measurements - ions[i] = ions[i-1] + (ions[i-1]-ions[i-2]) - - if aux.isnan(cap) and i < len(specific_capacity)-1: # do same thing for specific capacity - if i < len(specific_capacity)-1: - specific_capacity[i] = np.mean([specific_capacity[i-1], specific_capacity[i+1]]) - - else: - specific_capacity[i] = specific_capacity[i-1] + (specific_capacity[i-1]-specific_capacity[i-2]) - - - metadata['ions'] = ions - metadata['specific_capacity'] = specific_capacity - - - return metadata - - - - -def determine_active_roi(scan_data): - - # FIXME For Co-edge, this gave a wrong scan - - #Trying to pick the roi with the highest difference between maximum and minimum intensity --> biggest edge shift - # if max(scan_data["xmap_roi00"])-min(scan_data["xmap_roi00"])>max(scan_data["xmap_roi01"])-min(scan_data["xmap_roi01"]): - # active_roi = 'xmap_roi00' - # else: - # active_roi = 'xmap_roi01' - - - if not ('xmap_roi00' in scan_data.columns) or not ('xmap_roi01' in scan_data.columns): - if 'xmap_roi00' in scan_data.columns: - active_roi = 'xmap_roi00' - elif 'xmap_roi01' in scan_data.columns: - active_roi = 'xmap_roi01' - - elif (scan_data['xmap_roi00'].iloc[0:100].mean() < scan_data['xmap_roi00'].iloc[-100:].mean()) and (scan_data['xmap_roi01'].iloc[0:100].mean() < scan_data['xmap_roi01'].iloc[-100:].mean()): - if (scan_data['xmap_roi00'].iloc[:int(scan_data.shape[0]/2)].max() - scan_data['xmap_roi00'].iloc[0])/scan_data['xmap_roi00'].max() > (scan_data['xmap_roi01'].iloc[:int(scan_data.shape[0]/2)].max() - scan_data['xmap_roi01'].iloc[0])/scan_data['xmap_roi01'].max(): - active_roi = 'xmap_roi00' - else: - active_roi = 'xmap_roi01' - - elif scan_data['xmap_roi00'].iloc[0:100].mean() < scan_data['xmap_roi00'].iloc[-100:].mean(): - active_roi = 'xmap_roi00' - - elif scan_data['xmap_roi01'].iloc[0:100].mean() < scan_data['xmap_roi01'].iloc[-100:].mean(): - active_roi = 'xmap_roi01' - - else: - active_roi = None - - return active_roi - - - -def write_data(data: dict, options={}): - - - default_options = { - 'save_filenames': None, - 'save_dir': '.', - } - - options = aux.update_options(options=options, default_options=default_options, required_options=default_options.keys()) - - - if not options['save_filenames']: - options['save_filenames'] = [os.path.basename(col).split('.')[0]+'_exported.dat' for col in data['xanes_data'].columns if 'ZapEnergy' not in col] - - - print(options['save_filenames']) \ No newline at end of file diff --git a/nafuma/xanes/plot.py b/nafuma/xanes/plot.py deleted file mode 100644 index c98932f..0000000 --- a/nafuma/xanes/plot.py +++ /dev/null @@ -1,182 +0,0 @@ -import matplotlib.pyplot as plt -from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) - -import pandas as pd -import numpy as np -import math -import datetime - -#import ipywidgets as widgets -#from IPython.display import display - -import nafuma.xanes as xas -import nafuma.plotting as btp -import nafuma.auxillary as aux - - -def plot_xanes(data, options={}): - - - # Update options - default_options = { - 'which_scans': 'all', # Use real numbers, not indices - update_scans_list() will adjust. - 'highlight': [], - 'xlabel': 'Energy', 'ylabel': 'Intensity', - 'xunit': 'keV', 'yunit': 'arb. u.', - 'exclude_scans': [], - 'colours': None, - 'gradient': False, - 'rc_params': {}, - 'format_params': {}} - - options = aux.update_options(options=options, default_options=default_options) - - - if not 'xanes_data' in data.keys(): - data['xanes_data'] = xas.io.load_data(data=data, options=options) - - # Update list of cycles to correct indices - update_scans_list(data=data, options=options) - - colours = generate_colours(scans=options['which_scans'], options=options) - - # Prepare plot, and read and process data - - fig, ax = btp.prepare_plot(options=options) - - - # Add counter to pick out correct colour - counter = 0 - for i, path in enumerate(data['path']): - if i in options['which_scans']: - lw = plt.rcParams['lines.linewidth']*5 if i in options['highlight'] else plt.rcParams['lines.linewidth'] - - data['xanes_data'].plot(x='ZapEnergy', y=path, ax=ax, c=colours[counter], lw=lw) - counter += 1 - - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - #if options['interactive_session_active']: - - - return fig, ax - - -def pick_out_scans(metadata: dict, timestamp: list): - - # If either start or end are None, set to way back when or way into the future - split_operator=timestamp[0][-3] #Adding this to enable reading of both "." and ":" as operators to split hour:minute:second - - if not timestamp[0]: - timestamp[0] = datetime.datetime.strptime('1970 01 01 00:00:00', '%Y %m %d %H:%M:%S') - else: - if split_operator == ".": - timestamp[0] = datetime.datetime.strptime(timestamp[0], "%d.%b %y %H.%M.%S") - if split_operator == ":": - timestamp[0] = datetime.datetime.strptime(timestamp[0], "%d.%b %y %H:%M:%S") - if not timestamp[1]: - timestamp[1] = datetime.datetime.strptime('3000 01 01 00:00:00', '%Y %m %d %H:%M:%S') - else: - if split_operator == ".": - timestamp[1] = datetime.datetime.strptime(timestamp[1], "%d.%b %y %H.%M.%S") - if split_operator == ":": - timestamp[1] = datetime.datetime.strptime(timestamp[1], "%d.%b %y %H:%M:%S") - - scans = [] - for i, time in enumerate(metadata['time']): - if time >= timestamp[0] and time <= timestamp[1]: - scans.append(i) - - - return scans - - - - - - -def update_scans_list(data, options: dict) -> None: - - if options['which_scans'] == 'all': - options['which_scans'] = [i for i in range(len(data['path']))] - - - elif isinstance(options['which_scans'], list): - - scans =[] - - for scan in options['which_scans']: - if isinstance(scan, int): - scans.append(scan-1) - - elif isinstance(scan, tuple): - interval = [i-1 for i in range(scan[0], scan[1]+1)] - scans.extend(interval) - - - options['which_scans'] = scans - - - # Tuple is used to define an interval - as elements tuples can't be assigned, I convert it to a list here. - elif isinstance(options['which_scans'], tuple): - which_scans = list(options['which_scans']) - - if which_scans[0] <= 0: - which_scans[0] = 1 - - elif which_scans[1] < 0: - which_scans[1] = len(options['which_scans']) - - - options['which_scans'] = [i-1 for i in range(which_scans[0], which_scans[1]+1)] - - - for i, scan in enumerate(options['which_scans']): - if scan in options['exclude_scans']: - del options['which_scans'][i] - - - - -def generate_colours(scans, options): - # FIXME Make this a generalised function and use this instead of this and in the electrochemsitry submodule - - # Assign colours from the options dictionary if it is defined, otherwise use standard colours. - if options['colours']: - colour = options['colours'] - - else: - #colour = (214/255, 143/255, 214/255) # Plum Web (#D68FD6), coolors.co - colour = (90/255, 42/255, 39/255) # Caput Mortuum(#5A2A27), coolors.co - - # If gradient is enabled, find start and end points for each colour - if options['gradient']: - - if isinstance(colour, list) and len(colour) == 2: - options['number_of_colours'] = len(scans) - colours = btp.mix_colours(colour1=colour[0], colour2=colour[1], options=options) - - - else: - add = min([(1-x)*0.75 for x in colour]) - - colour_start = colour - colour_end = [x+add for x in colour] - - - - # Generate lists of colours - if not isinstance(colour, list): - colours = [] - for scan_number in range(0, len(scans)): - - if options['gradient']: - weight_start = (len(scans) - scan_number)/len(scans) - weight_end = scan_number/len(scans) - - colour = [weight_start*start_colour + weight_end*end_colour for start_colour, end_colour in zip(colour_start, colour_end)] - - colours.append(colour) - - return colours \ No newline at end of file diff --git a/nafuma/xanes/unit_tables.py b/nafuma/xanes/unit_tables.py deleted file mode 100644 index 52ca61c..0000000 --- a/nafuma/xanes/unit_tables.py +++ /dev/null @@ -1,11 +0,0 @@ -import pandas as pd - -def time(): - # Define matrix for unit conversion for time - time = {'h': [1, 60, 3600, 3600000], 'min': [1/60, 1, 60, 60000], 's': [1/3600, 1/60, 1, 1000], 'ms': [1/3600000, 1/60000, 1/1000, 1]} - time = pd.DataFrame(time) - time.index = ['h', 'min', 's', 'ms'] - - return time - - diff --git a/nafuma/xrd/__init__.py b/nafuma/xrd/__init__.py deleted file mode 100644 index d89f20e..0000000 --- a/nafuma/xrd/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import io, plot, refinement \ No newline at end of file diff --git a/nafuma/xrd/io.py b/nafuma/xrd/io.py deleted file mode 100644 index 0d3f16f..0000000 --- a/nafuma/xrd/io.py +++ /dev/null @@ -1,939 +0,0 @@ -from sympy import re -import fabio, pyFAI -import pandas as pd -import numpy as np -import os -import shutil -import sys -import datetime - -import zipfile -import xml.etree.ElementTree as ET - - -import nafuma.auxillary as aux - - -def get_image_array(path): - - beamline_extension = ['.edf', '.cbf', '.mar3450'] - - if path.endswith(tuple(beamline_extension)): - image = fabio.open(path) - image_array = image.data - - elif path.endswith('.dat'): - image_array = np.loadtxt(path, skiprows=1, delimiter=';') - - return image_array - - -def get_image_headers(path): - - image = fabio.open(path) - - return image.header - - - -def integrate_scans(data: dict, options={}): - - default_options = { - 'extension': '.dat', - 'save': True, - 'integration_save_folder': './integrated/', - 'filename_base': 'integrated', - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - if not isinstance(data['path'], list): - imgs = aux.get_filenames(data['path'], ext=options['extension']) - - - diffractograms, wavelengths = [], [] - for i, img in enumerate(imgs): - data['image'] = get_image_array(img) - - options['integration_save_filename'] = options['filename_base'] + '_' + f'{i}'.zfill(4) + '.xy' - - diff, wl = integrate_1d(data=data, options=options) - - diffractograms.append(diff) - wavelengths.append(wl) - - return diffractograms, wavelengths - - -def integrate_1d(data, options={}, index=0): - ''' Integrates an image file to a 1D diffractogram. - - Required content of data: - calibrant (str): path to .poni-file - nbins (int): Number of bins to divide image into - path (str) (optional, dependent on image): path to image file - either this or image must be specified. If both is passed, image is prioritsed - image (NumPy 2D Array) (optional, dependent on path): image array as extracted from get_image_array - - Output: - df: DataFrame contianing 1D diffractogram if option 'return' is True - ''' - - required_options = ['unit', 'npt', 'save', 'integration_save_filename', 'save_extension', 'integration_save_folder', 'overwrite', 'extract_folder', 'error_model'] - - default_options = { - 'unit': '2th_deg', - 'npt': 5000, - 'extract_folder': 'tmp', - 'error_model': None, - 'save': False, - 'integration_save_filename': None, - 'save_extension': '_integrated.xy', - 'integration_save_folder': '.', - 'overwrite': False} - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - - # Get image array from filename if not passed - if 'image' not in data.keys() or not isinstance(data['image'], np.ndarray): - data['image'] = get_image_array(data['path'][index]) - - - # Load mask - if 'mask' in data.keys(): - mask = get_image_array(data['mask']) - else: - mask = None - - - # Instanciate the azimuthal integrator from pyFAI from the calibrant (.poni-file) - ai = pyFAI.load(data['calibrant']) - - # Determine filename - filename = make_filename(options=options, path=data['path'][index]) - - # Make save_folder if this does not exist already - if not os.path.isdir(options['extract_folder']): - os.makedirs(options['extract_folder']) - - if not os.path.isdir(options['integration_save_folder']): - os.makedirs(options['integration_save_folder']) - - - - - res = ai.integrate1d(data['image'], npt=options['npt'], mask=mask, error_model=options['error_model'], unit=options['unit'], filename=filename) - - data['path'][index] = filename - diffractogram, _ = read_xy(data=data, options=options, index=index) - wavelength = find_wavelength_from_poni(path=data['calibrant']) - - if not options['save']: - os.remove(filename) - shutil.rmtree(f'tmp') - - return diffractogram, wavelength - - - -def make_filename(options, path=None): - - # Define save location for integrated diffractogram data - if not options['save']: - filename = os.path.join(options['extract_folder'], 'tmp_diff.dat') - - elif options['save']: - - # Case 1: No filename is given. - if not options['integration_save_filename']: - # If a path is given instead of an image array, the path is taken as the trunk of the savename - if path: - # Make filename by joining the save_folder, the filename (with extension deleted) and adding the save_extension - filename = os.path.join(options['integration_save_folder'], os.path.split(path)[-1].split('.')[0] + options['save_extension']) - else: - # Make filename just "integrated.dat" in the save_folder - filename = os.path.join(options['integration_save_folder'], 'integrated.xy') - - - else: - filename = os.path.join(options['integration_save_folder'], options['integration_save_filename']) - - if not options['overwrite']: - trunk = filename.split('.')[0] - extension = filename.split('.')[-1] - counter = 0 - - while os.path.isfile(filename): - - # Rename first file to match naming scheme if already exists - if counter == 0: - os.rename(filename, trunk + '_' + str(counter).zfill(4) + '.' + extension) - - # Increment counter and make new filename - counter += 1 - counter_string = str(counter) - filename = trunk + '_' + counter_string.zfill(4) + '.' + extension - - - return filename - - - -def generate_image_list(path, options=None): - ''' Generates a list of paths to pass to the average_images() function''' - - required_options = ['scans_per_image'] - default_options = { - 'scans_per_image': 5 - } - - -def process_2d_scans(data: dict, options={}): - - default_options = { - 'scans': 15, # number of scans per image - 'img_filename': 'img_', - 'extension': '.edf', - 'darks': True, # whether there are darks - 'dark_filename': 'dark_', - 'save': False, - 'save_folder': './average/', - 'save_filename': 'avg_', - 'save_extension': '.dat' - } - - options = aux.update_options(options=options, required_options=default_options.keys(), default_options=default_options) - - - all_imgs = [os.path.join(data['path'], img) for img in os.listdir(data['path']) if img.endswith(options['extension']) and img.startswith(options['img_filename'])] - - if options['darks']: - all_darks = [os.path.join(data['path'], img) for img in os.listdir(data['path']) if img.endswith(options['extension']) and img.startswith(options['dark_filename'])] - - - scans = int(len(all_imgs) / options['scans']) - - - assert scans - (len(all_imgs) / options['scans']) == 0 - - - imgs = [] - darks = [] - - for i in range(scans): - img = [] - dark = [] - - for j in range(options['scans']): - img.append(all_imgs.pop(0)) - - if options['darks']: - dark.append(all_darks.pop(0)) - - imgs.append(img) - - if options['darks']: - darks.append(dark) - - - img_avgs = [] - headers = [] - - for img, dark in zip(imgs,darks): - img_avg = average_images(img) - header = get_image_headers(img[0]) - - if options['darks']: - dark_avg = average_images(dark) - img_avg = subtract_dark(img_avg, dark_avg) - - img_avgs.append(img_avg) - headers.append(header) - - - if options['save']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - for i, img in enumerate(img_avgs): - if options['save_extension'] == '.dat': - with open(os.path.join(options['save_folder'], options['save_filename']+f'{i}'.zfill(4)+options['save_extension']), 'w') as f: - f.write(f'# Time: {headers[i]["time"]}\n') - np.savetxt(f, img, fmt='%.2f', delimiter=";") - - - return img_avgs - - - - - - - - - -def average_images(images): - ''' Takes a list of path to image files, reads them and averages them before returning the average image''' - - image_arrays = [] - - for image in images: - image_array = get_image_array(image) - image_arrays.append(image_array) - - - image_arrays = np.array(image_arrays) - - image_average = image_arrays.mean(axis=0) - - - return image_average - - -def subtract_dark(image, dark): - - return image - dark - - - -def view_integrator(calibrant): - ''' Prints out information about the azimuthal integrator - - Input: - calibrant: Path to the azimuthal integrator file (.PONI) - - Output: - None''' - - ai = pyFAI.load(calibrant) - - print("pyFAI version:", pyFAI.version) - print("\nIntegrator: \n", ai) - - - - -def read_brml(data, options={}, index=0): - - - # FIXME: Can't read RECX1-data, apparently must be formatted differently from RECX2. Check the RawData-files and compare between the two files. - - - required_options = ['extract_folder', 'save_folder'] - default_options = { - 'extract_folder': 'tmp', - 'save_folder': None - } - - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - - if not os.path.isdir(options['extract_folder']): - os.mkdir(options['extract_folder']) - - - # Extract the RawData0.xml file from the brml-file - with zipfile.ZipFile(data['path'][index], 'r') as brml: - for info in brml.infolist(): - if "RawData" in info.filename: - brml.extract(info.filename, options['extract_folder']) - - - - # Parse the RawData0.xml file - path = os.path.join(options['extract_folder'], 'Experiment0/RawData0.xml') - - tree = ET.parse(path) - root = tree.getroot() - - shutil.rmtree(options['extract_folder']) - - diffractogram = [] - - for chain in root.findall('./DataRoutes/DataRoute'): - - - # Get the scan type to be able to handle different data formats - scantype = chain.findall('ScanInformation')[0].get('VisibleName') - - # Check if the chain is the right one to extract the data from - if chain.get('Description') == 'Originally measured data.': - - - if scantype == 'TwoTheta': - for scandata in chain.findall('Datum'): - scandata = scandata.text.split(',') - twotheta, intensity = float(scandata[2]), float(scandata[3]) - - if twotheta > 0: - diffractogram.append({'2th': twotheta, 'I': intensity}) - - elif scantype == 'Coupled TwoTheta/Theta': - for scandata in chain.findall('Datum'): - scandata = scandata.text.split(',') - twotheta, intensity = float(scandata[2]), float(scandata[4]) - - if twotheta > 0: - diffractogram.append({'2th': twotheta, 'I': intensity}) - - elif scantype == 'Still (Eiger2R_500K (1D mode))': - - start = float(chain.findall('ScanInformation/ScaleAxes/ScaleAxisInfo/Start')[0].text) - stop = float(chain.findall('ScanInformation/ScaleAxes/ScaleAxisInfo/Stop')[0].text) - - - - - for scandata in chain.findall('Datum'): - scandata = scandata.text.split(',') - raw = [float(i) for i in scandata] - - intensity = [] - for r in raw: - if r > 601: - intensity.append(r) - - intensity = np.array(intensity) - - - - - twotheta = np.linspace(start, stop, len(intensity)) - - diffractogram = {'2th': twotheta, 'I': intensity} - - - - #if 'wavelength' not in data.keys(): - # Find wavelength - - if not data['wavelength'][index]: - for chain in root.findall('./FixedInformation/Instrument/PrimaryTracks/TrackInfoData/MountedOptics/InfoData/Tube/WaveLengthAlpha1'): - wavelength = float(chain.attrib['Value']) - else: - wavelength = data['wavelength'][index] - - - diffractogram = pd.DataFrame(diffractogram) - - - - - if options['save_folder']: - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - diffractogram.to_csv(options['save_folder']) - - - - return diffractogram, wavelength - - -def read_htxrd(data, options={}, index=0): - - required_options = ['extract_folder', 'save_folder', 'save_filename', 'adjust_time'] - default_options = { - 'extract_folder': 'tmp', - 'save_folder': None, - 'save_filename': None, - 'adjust_time': True - } - - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - if 'wavelength' not in data.keys(): - data['wavelength'] = [None for i in range(len(data['path']))] - - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - # Extract the RawData0.xml file from the brml-file - with zipfile.ZipFile(data['path'][index], 'r') as brml: - for info in brml.infolist(): - if "RawData" in info.filename: - brml.extract(info.filename, options['extract_folder']) - - - # Get all filenames - files = os.listdir(os.path.join(options['extract_folder'], 'Experiment0')) - - # initalise empty list to store all DataFrames - diffractograms = [] - wavelengths = [] - - active_scan = False - timestamps = [] - - # Loop through all RawData-files and extract all data and temperatures - for i, file in enumerate(files): - - # Create all filenames as strings - filename = os.path.join('tmp/Experiment0/', f'RawData{i}.xml') - - # Parse the .xml-files - tree = ET.parse(filename) - root = tree.getroot() - - # initalise empty list to store data from this particular scan - diffractogram = [] - - for chain in root.findall('./DataRoutes/DataRoute'): - - scantypes = chain.findall('ScanInformation') - - for scantype in scantypes: - if scantype.get('VisibleName') == 'Still (TCU1000N)': - continue - - else: - active_scan = True - if chain.get('RouteFlag') == 'Final': - for scandata in chain.findall('Datum'): - scandata = scandata.text.split(',') - twotheta, intensity, temperature = float(scandata[2]), float(scandata[3]), float(scandata[5]) - - diffractogram.append({'2th': twotheta, 'I': intensity, 'T': temperature}) - - diffractogram = pd.DataFrame(diffractogram) - diffractograms.append(diffractogram) - - - if not data['wavelength'][index]: - for chain in root.findall('./FixedInformation/Instrument/PrimaryTracks/TrackInfoData/MountedOptics/InfoData/Tube/WaveLengthAlpha1'): - wavelength = float(chain.attrib['Value']) - else: - wavelength = data['wavelength'][index] - - wavelengths.append(wavelength) - - - if active_scan: - for chain in root.findall('./TimeStampStarted'): - time_start = datetime.datetime.strptime(chain.text[:-7], "%Y-%m-%dT%H:%M:%S.%f") - for chain in root.findall('./TimeStampFinished'): - time_end = datetime.datetime.strptime(chain.text[:-7], "%Y-%m-%dT%H:%M:%S.%f") - - - time_diff = time_end - time_start - - if options['adjust_time']: - timestamps.append(time_start + time_diff/2) - - - if options['save_folder']: - for i, (diffractogram, wavelength, timestamp) in enumerate(zip(diffractograms, wavelengths, timestamps)): - if not options['save_filename']: - filename = os.path.basename(data['path'][index]).split('.')[0] + '_' + str(i).zfill(4) +'.xy' - else: - filename = options['save_filename'] + '_' + str(i).zfill(4) +'.xy' - - - if not os.path.isdir(options['save_folder']): - os.makedirs(options['save_folder']) - - save_htxrd_as_xy(diffractogram, wavelength, timestamp, filename, options['save_folder']) - - - - - - shutil.rmtree(options['extract_folder']) - - return diffractograms, wavelengths - -def save_htxrd_as_xy(diffractogram, wavelength, timestamp, filename, save_path): - - headers = '\n'.join( - [line for line in - [f'# Temperature {np.round(diffractogram["T"].mean())}', - f'# Wavelength {wavelength}', - f'# Time {timestamp}', - '# 2th \t I' - ] - ] - ) - - diffractogram = diffractogram.drop('T', axis=1) - - with open(os.path.join(save_path, filename), 'w', newline='\n') as f: - for line in headers: - f.write(line) - - f.write('\n') - - diffractogram.to_csv(f, index=False, header=False, sep='\t') - - -def read_xy(data, options={}, index=0): - - #if 'wavelength' not in data.keys(): - # Get wavelength from scan - - if 'wavelength' in data.keys() and not type(data['wavelength']) == list: - data['wavelength'] = [data['wavelength']] - - if not 'wavelength' in data.keys() or not data['wavelength'][index]: - wavelength = read_metadata_from_xy(path=data['path'][index])['wavelength'] - - else: - wavelength = data['wavelength'][index] - - with open(data['path'][index], 'r') as f: - position = 0 - - current_line = f.readline() - - while current_line[0] == '#' or current_line[0] == '\'': - position = f.tell() - current_line = f.readline() - - f.seek(position) - - diffractogram = pd.read_csv(f, header=None, delim_whitespace=True) - - - if diffractogram.shape[1] == 2: - diffractogram.columns = ['2th', 'I'] - elif diffractogram.shape[1] == 3: - diffractogram.columns = ['2th', 'I', 'sigma'] - - - return diffractogram, wavelength - - - -def read_metadata_from_xy(path): - - metadata = {} - wavelength_dict = {'Cu': 1.54059, 'Mo': 0.71073} - - with open(path, 'r') as f: - lines = f.readlines() - - for line in lines: - # For .xy-files output from EVA - if 'Anode' in line: - anode = line.split()[8].strip('"') - metadata['wavelength'] = wavelength_dict[anode] - - - elif 'Wavelength' in line: - # For .xy-files output from pyFAI integration - if line.split()[-1] == 'm': - metadata['wavelength'] = float(line.split()[2])*10**10 - - else: - metadata['wavelength'] = float(line.split()[-1]) - - - # Get temperature - exists in .xy-files saved from HTXRD-runs in .brml-files - if 'Temperature' in line: - metadata['temperature'] = line.split()[-1] - - # Get timestamp - exists in .xy-files saved from .brml-files - if 'Time' in line: - metadata['time'] = " ".join(line.split()[2:]) - - - - - if 'wavelength' not in metadata.keys(): - metadata['wavelength'] = None - if 'temperature' not in metadata.keys(): - metadata['temperature'] = None - if 'time' not in metadata.keys(): - metadata['time'] = None - - return metadata - - -def find_wavelength_from_poni(path): - - with open(path, 'r') as f: - lines = f.readlines() - - for line in lines: - if 'Wavelength' in line: - wavelength = float(line.split()[-1])*10**10 - - - return wavelength - - - - -def strip_headers_from_xy(path: str, filename=None) -> None: - ''' Strips headers from a .xy-file''' - - - xy = [] - with open(path, 'r') as f: - lines = f.readlines() - - headerlines = 0 - for line in lines: - if line[0] == '#': - headerlines += 1 - elif line[0] == "\'": - headerlines += 1 - - else: - xy.append(line) - - - if not filename: - ext = path.split('.')[-1] - filename = path.split(f'.{ext}')[0] + f'_noheaders.{ext}' - - - with open(filename, 'w') as f: - for line in xy: - f.write(line) - - - - - - -def read_data(data, options={}, index=0): - - beamline_extensions = ['mar3450', 'edf', 'cbf'] - file_extension = data['path'][index].split('.')[-1] - - if file_extension in beamline_extensions: - diffractogram, wavelength = integrate_1d(data=data, options=options, index=index) - - elif file_extension == 'brml': - diffractogram, wavelength = read_brml(data=data, options=options, index=index) - - elif file_extension in['xy', 'xye']: - diffractogram, wavelength = read_xy(data=data, options=options, index=index) - - - if options['exclude']: - - if not isinstance(options['exclude'], list): - options['exclude'] = [options['exclude']] - - for excl in options['exclude']: - diffractogram['I'].loc[(diffractogram['2th'] > excl[0]) & (diffractogram['2th'] < excl[1])] = 0 - - - if options['offset'] or options['normalise']: - # Make copy of the original intensities before any changes are made through normalisation or offset, to easily revert back if need to update. - diffractogram['I_org'] = diffractogram['I'] - diffractogram['2th_org'] = diffractogram['2th'] - - diffractogram = adjust_intensities(diffractogram, wavelength, index, options) - - - - diffractogram = translate_wavelengths(data=diffractogram, wavelength=wavelength) - - return diffractogram, wavelength - - -def adjust_intensities(diffractogram, wavelength, index, options): - - if 'current_offset_y' not in options.keys(): - options['current_offset_y'] = options['offset_y'] - else: - if options['current_offset_y'] != options['offset_y']: - options['offset_change'] = True - - options['current_offset_y'] = options['offset_y'] - - options['current_offset_x'] = options['offset_x'] - - - - #Apply offset along y-axis - diffractogram['I'] = diffractogram['I_org'] # Reset intensities - - if options['normalise']: - diffractogram['I'] = diffractogram['I'] / diffractogram['I'].max() - - if not isinstance(options['multiply'], list): - options['multiply'] = [options['multiply']] - - diffractogram['I'] = diffractogram['I'] * options['multiply'][index] - - if options['drawdown']: - diffractogram['I'] = diffractogram['I'] - diffractogram['I'].mean() - - diffractogram['I'] = diffractogram['I'] + index*options['offset_y'] - - # Apply offset along x-axis - relative_shift = (wavelength / 1.54059)*options['offset_x'] # Adjusts the offset-factor to account for wavelength, so that offset_x given is given in 2th_cuka-units - diffractogram['2th'] = diffractogram['2th_org'] - diffractogram['2th'] = diffractogram['2th'] + index*relative_shift - - - return diffractogram - -def revert_offset(diffractogram,which=None): - - if which == 'both': - diffractogram['2th'] = diffractogram['2th_org'] - diffractogram['I'] = diffractogram['I_org'] - - if which == 'y': - diffractogram['I'] = diffractogram['I_org'] - - if which == 'x': - diffractogram['2th'] = diffractogram['2th_org'] - - return diffractogram - -def load_reflection_table(data: dict, reflections_params: dict, options={}): - - required_options = ['ref_wavelength', 'to_wavelength'] - - default_options = { - 'ref_wavelength': 1.54059, - 'to_wavelength': None - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - # VESTA outputs the file with a header that has a space between the parameter and units - so there is some extra code to rectify the issue - # that ensues from this formatting - reflections = pd.read_csv(reflections_params['path'], delim_whitespace=True) - - # Remove the extra column that appears from the headers issue - reflections.drop(reflections.columns[-1], axis=1, inplace=True) - - with open(reflections_params['path'], 'r') as f: - line = f.readline() - - headers = line.split() - - # Delete the fourth element which is '(Å)' - del headers[4] - - # Change name of column to avoid using greek letters - headers[7] = '2th' - - # Set the new modified headers as the headers of - reflections.columns = headers - - reflections = translate_wavelengths(data=reflections, wavelength=options['ref_wavelength'], to_wavelength=options['to_wavelength']) - - if 'heatmap' in data.keys(): - - start_2th, stop_2th = data['diffractogram'][0]['2th'].min(), data['diffractogram'][0]['2th'].max() - len_2th = stop_2th - start_2th - #print(start_2th, stop_2th, len_2th) - - start_heatmap, stop_heatmap = 0, data['heatmap'].shape[1] - len_heatmap = stop_heatmap - start_heatmap - #print(start_heatmap, stop_heatmap, len_heatmap) - - scale = len_heatmap/len_2th - - #print(scale) - #print(stop_2th * scale) - - reflections['heatmap'] = (reflections['2th']-start_2th) * scale - - return reflections - - - -def translate_wavelengths(data: pd.DataFrame, wavelength: float, to_wavelength=None) -> pd.DataFrame: - # FIXME Somewhere here there is an invalid arcsin-argument. Not sure where. - - pd.options.mode.chained_assignment = None - - # Translate to CuKalpha - cuka = 1.54059 # Å - - if cuka > wavelength: - max_2th_cuka = 2*np.arcsin(wavelength/cuka) * 180/np.pi - else: - max_2th_cuka = data['2th'].max() - - data['2th_cuka'] = np.NAN - - data['2th_cuka'].loc[data['2th'] <= max_2th_cuka] = 2*np.arcsin(cuka/wavelength * np.sin((data['2th'].loc[data['2th'] <= max_2th_cuka]/2) * np.pi/180)) * 180/np.pi - - # Translate to MoKalpha - moka = 0.71073 # Å - - if moka > wavelength: - max_2th_moka = 2*np.arcsin(wavelength/moka) * 180/np.pi - else: - max_2th_moka = data['2th'].max() - - data['2th_moka'] = np.NAN - - data['2th_moka'].loc[data['2th'] <= max_2th_moka] = 2*np.arcsin(moka/wavelength * np.sin((data['2th'].loc[data['2th'] <= max_2th_moka]/2) * np.pi/180)) * 180/np.pi - - - # Convert to other parameters - data['d'] = wavelength / (2*np.sin((2*data['2th']*np.pi/180)/2)) - data['1/d'] = 1/data['d'] - data['q'] = np.abs((4*np.pi/wavelength)*np.sin(data['2th']/2 * np.pi/180)) - data['q2'] = data['q']**2 - data['q4'] = data['q']**4 - - - if to_wavelength: - if to_wavelength >= cuka: - max_2th = 2*np.arcsin(cuka/to_wavelength) * 180/np.pi - else: - max_2th = data['2th_cuka'].max() - - - data['2th'] = np.NAN - data['2th'].loc[data['2th_cuka'] <= max_2th] = 2*np.arcsin(to_wavelength/cuka * np.sin((data['2th_cuka'].loc[data['2th_cuka'] <= max_2th]/2) * np.pi/180)) * 180/np.pi - - - - - return data - - - - -def trim_xy_region(path, region): - - df = pd.read_csv(path, header=None, delim_whitespace=True) - df.columns = ['2th', 'I'] - - df = df.loc[(df['2th'] > region[0]) & (df['2th'] < region[1])] - - folder = os.path.dirname(path) - save_folder = os.path.join(folder, 'trimmed') - - if not os.path.exists(save_folder): - os.makedirs(save_folder) - - df.to_csv(os.path.join(save_folder, os.path.basename(path)), sep='\t', header=None, index=None) - - -def raise_intensities_xy(path, region=None): - - df = pd.read_csv(path, header=None, delim_whitespace=True) - df.columns = ['2th', 'I'] - - if region: - df = df.loc[(df['2th'] > region[0]) & (df['2th'] < region[1])] - - df['I'] = df['I'] - df['I'].min() - - - folder = os.path.dirname(path) - save_folder = os.path.join(folder, 'raised') - - if not os.path.exists(save_folder): - os.makedirs(save_folder) - - df.to_csv(os.path.join(save_folder, os.path.basename(path)), sep='\t', header=None, index=None) \ No newline at end of file diff --git a/nafuma/xrd/plot.py b/nafuma/xrd/plot.py deleted file mode 100644 index a984bc1..0000000 --- a/nafuma/xrd/plot.py +++ /dev/null @@ -1,1114 +0,0 @@ -import seaborn as sns -import matplotlib.pyplot as plt -from matplotlib.ticker import MultipleLocator - -import pandas as pd -import numpy as np -import math -import os -import shutil - -import ipywidgets as widgets -from IPython.display import display -from PIL import Image - -import nafuma.xrd as xrd -import nafuma.auxillary as aux -import nafuma.plotting as btp - -def plot_diffractogram(data, options={}): - ''' Plots a diffractogram. - - Input: - data (dict): Must include path = string to diffractogram data, and plot_kind = (recx, beamline, image)''' - - default_options = { - 'x_vals': '2th', 'y_vals': 'I', - 'xlabel': '2$\\theta$', 'ylabel': None, - 'xunit': '$^{\circ}$', 'yunit': None, - 'xlim': None, 'ylim': None, - 'normalise': True, - 'exclude': None, - 'multiply': 1, # Factor to multiply the normalised data - only used if normalising. - 'drawdown': False, - 'offset': True, - 'offset_x': 0, - 'offset_y': 1, - 'offset_change': False, - 'line': True, # whether or not to plot diffractogram as a line plot - 'scatter': False, # whether or not to plot individual data points - 'reflections_plot': False, # whether to plot reflections as a plot - 'reflections_indices': False, # whether to plot the reflection indices - 'reflections_data': None, # Should be passed as a list of dictionaries on the form {path: rel_path, reflection_indices: number of indices, colour: [r,g,b], min_alpha: 0-1] - 'heatmap': False, - 'heatmap_reverse': False, - 'cmap': 'viridis', - 'plot_kind': None, - 'palettes': [('qualitative', 'Dark2_8')], - 'highlight': None, - 'highlight_colours': ['red'], - 'interactive': False, - 'interactive_session_active': False, - 'rc_params': {}, - 'format_params': {}, - 'plot_diff': False, - } - - if 'offset_y' not in options.keys(): - if len(data['path']) > 10: - default_options['offset_y'] = 0.05 - - options = aux.update_options(options=options, default_options=default_options) - #options['current_offset_y'] = options['offset_y'] - - # Convert data['path'] to list to allow iteration over this to accommodate both single and multiple diffractograms - if not isinstance(data['path'], list): - data['path'] = [data['path']] - - - ############################################################################################################################################################ - ##### LOADING DATA ######################################################################################################################################### - ############################################################################################################################################################ - - # Check if there is some data stored already, load in data if not. This speeds up replotting in interactive mode. - if not 'diffractogram' in data.keys(): - - # This is to set the default values of the diffractogram y-label and -unit so that the actual yunit and ylable can switch back and forth between these and the heatmap values - options['diff.yunit'] = 'a.u.' - options['diff.ylabel'] = 'Intensity' - - # Initialise empty list for diffractograms and wavelengths. If wavelength is not manually passed it should be automatically gathered from the .xy-file - data['diffractogram'] = [None for _ in range(len(data['path']))] - - if 'wavelength' not in data.keys(): - data['wavelength'] = [None for _ in range(len(data['path']))] - else: - # If only a single value is passed it should be set to be the same for all diffractograms passed - if not isinstance(data['wavelength'], list): - data['wavelength'] = [data['wavelength'] for _ in range(len(data['path']))] - - - - - # LOAD DIFFRACTOGRAMS - - if 'htxrd' in data.keys() and data['htxrd']: - data['diffractogram'], data['wavelength'] = xrd.io.read_htxrd(data=data, options=options, index=0) - - else: - for index in range(len(data['path'])): - diffractogram, wavelength = xrd.io.read_data(data=data, options=options, index=index) - - - data['diffractogram'][index] = diffractogram - data['wavelength'][index] = wavelength - - - # FIXME This is a quick fix as the image is not reloaded when passing multiple beamline datasets. Should probably be handled in io? - data['image'] = None - - - # Sets the xlim if this has not been specified - if not options['xlim']: - options['xlim'] = [data['diffractogram'][0][options['x_vals']].min(), data['diffractogram'][0][options['x_vals']].max()] - - # GENERATE HEATMAP DATA - data['heatmap'], data['heatmap_xticks'], data['heatmap_xticklabels'], data['heatmap_yticks'], data['heatmap_yticklabels'] = generate_heatmap(data=data, options=options) - options['heatmap_loaded'] = True - - - if options['heatmap']: - xlim_start_frac, xlim_end_frac = options['xlim'][0] / data['diffractogram'][0][options['x_vals']].max(), options['xlim'][1] / data['diffractogram'][0][options['x_vals']].max() - options['xlim'] = [options['heatmap_xlim'][1]*xlim_start_frac, options['heatmap_xlim'][1]*xlim_end_frac] - - if options['heatmap_reverse']: - data['heatmap'] = data['heatmap'].iloc[::-1] - data['heatmap_yticklabels'] = data['heatmap_yticklabels'][::-1] - - # If data was already loaded, only do a check to see if the data is in a list or not, and if not, put it in one. This is because it will be looped over later. - else: - if not isinstance(data['diffractogram'], list): - data['diffractogram'] = [data['diffractogram']] - data['wavelength'] = [data['wavelength']] - - ############################################################################################################################################################ - ##### INTERACTIVE SESSION ################################################################################################################################## - ############################################################################################################################################################ - - - - # START INTERACTIVE SESSION - # Start inteactive session with ipywidgets. Disables options['interactive'] in order for the interactive loop to not recursively start new interactive sessions - if options['interactive']: - options['interactive'] = False - options['interactive_session_active'] = True - plot_diffractogram_interactive(data=data, options=options) - return - - - # If interactive mode is already enabled, update the offsets. - if options['interactive_session_active']: - if options['offset']: - if (options['offset_x'] != options['current_offset_x']) or (options['offset_y'] != options['current_offset_y']): - for i, (diff, wl) in enumerate(zip(data['diffractogram'], data['wavelength'])): - xrd.io.adjust_intensities(diff, wl, i, options) - - - - - ############################################################################################################################################################ - ##### PREPARE THE PLOT AND COLOURS ######################################################################################################################### - ############################################################################################################################################################ - - # CREATE AND ASSIGN AXES - - # Makes a list out of reflections_data if it only passed as a dict, as it will be looped through later - if options['reflections_data']: - if not isinstance(options['reflections_data'], list): - options['reflections_data'] = [options['reflections_data']] - - - # Determine the grid layout based on how many sets of reflections data has been passed - if options['reflections_data'] and len(options['reflections_data']) >= 1: - options = determine_grid_layout(options=options) - - # Create the Figure and Axes objects - fig, ax = btp.prepare_plot(options=options) - - # Assign the correct axes to the indicies, reflections and figure itself - if options['reflections_plot'] or options['reflections_indices']: - - if options['reflections_indices']: - indices_ax = ax[0] - - if options['reflections_plot']: - ref_axes = [axx for axx in ax[range(1,len(options['reflections_data'])+1)]] - - else: - ref_axes = [axx for axx in ax[range(0,len(options['reflections_data']))]] - - ax = ax[-1] - - - # GENERATE COLOURS - - # Limit for when it is assumed that each diffractogram should have its own colour - after 8, the default colour palette is used up and starts a new. - # FIXME Should probably allow for more than 8 if wanted - not a priority now - if len(data['path']) <= 8: - if 'colours' in options.keys(): - colours = btp.generate_colours(options['colours'], kind='single') - - else: - colours = btp.generate_colours(options['palettes']) - - - # Generates the colours of a list of scans to highlight is passed. options['highlight'] and options['highlight_colour'] must be of equal length. Entries in highlight can either be a list or a single number, - # if the latter it will be turned into a list with the same number as element 1 and 2. - elif options['highlight']: - # Make sure that options['highlight'] is a list - if not isinstance(options['highlight'], list): - options['highlight'] = [[options['highlight'], options['highlight']]] - - # Make sure that options['highlight_colours] is a list - if not isinstance(options['highlight_colours'], list): - options['highlight_colours'] = [options['highlight_colours']] - - if options['heatmap_reverse']: - print(len(data['diffractogram'])) - options['highlight'] = [len(data['diffractogram'])-highlight for highlight in options['highlight']] - - colours = [] - - # Loop through each scan - assign the correct colour to each of the scan intervals in options['highlight'] - for i in range(len(data['path'])): - assigned = False - for j, highlight in enumerate(options['highlight']): - - # If one of the elements in options['highlight'] is a single number (i.e. only one scan should be highlighted), this is converted into the suitable format to be handled below - if not isinstance(highlight, list): - highlight = [highlight, highlight] - - # Assigns the j-th colour if scan number (i) is within the j-th highlight-interval - if i >= highlight[0] and i <= highlight[1]: - colours.append(options['highlight_colours'][j]) - assigned = True - - # Only assign black to i if not already been given a colour - if not assigned: - colours.append('black') - - # Reset the 'assigned' value for the next iteration - assigned = False - - # Make a itertools cycle out of the colours - colours = btp.generate_colours(colours, kind='single') - - - # If there are many scans and no highlight-options have been passed, all scans will be black - else: - colours = btp.generate_colours(['black'], kind='single') - - - - ############################################################################################################################################################ - ##### PLOT THE DATA ######################################################################################################################################## - ############################################################################################################################################################ - - - # PLOT HEATMAP - if options['heatmap']: - - options['x_tick_locators'] = None - - # Add locators for y-axis - otherwise it will tend to break (too many ticks) when switching between diffractograms and heatmap in interactive mode. These values will be updated later anyway, and is only - # to allow the initial call to Seaborn to have values that are sensible. - # FIXME A more elegant solution to this? - ax.yaxis.set_major_locator(MultipleLocator(100)) - ax.yaxis.set_minor_locator(MultipleLocator(50)) - - # Call Seaborn to plot the data - sns.heatmap(data['heatmap'], cmap=options['cmap'], cbar=False, ax=ax) - - # Set the ticks and ticklabels to match the data point number with 2th values - ax.set_xticks(data['heatmap_xticks'][options['x_vals']]) - ax.set_xticklabels(data['heatmap_xticklabels'][options['x_vals']]) - ax.set_yticks(data['heatmap_yticks']) - ax.set_yticklabels(data['heatmap_yticklabels']) - - # Set the labels to the relevant values for heatmap plot - if not options['ylabel'] or options['ylabel'] == options['diff.ylabel']: - options['ylabel'] = options['heatmap.ylabel'] - if not options['yunit'] or options['yunit'] == options['diff.yunit']: - options['yunit'] = options['heatmap.yunit'] - - - - ax.tick_params(axis='x', which='minor', bottom=False, top=False) - ax.tick_params(axis='y', which='minor', left=False, right=False) - - options['hide_y_ticklabels'] = False - options['hide_y_ticks'] = False - - - # Toggle on the frame around the heatmap - this makes it look better together with axes ticks - for _, spine in ax.spines.items(): - spine.set_visible(True) - - - if options['highlight']: - for i, highlight in enumerate(options['highlight']): - if i < len(options['highlight']) or len(options['highlight']) == 1: - ax.axhline(y=highlight, c=options['highlight_colours'][i], ls='--', lw=0.5) - - - # PLOT DIFFRACTOGRAM - else: - for diffractogram in data['diffractogram']: - - # Plot data as line plot - if options['line']: - diffractogram.plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=next(colours), zorder=1) - - # Plot data as scatter plot - if options['scatter']: - ax.scatter(x=diffractogram[options['x_vals']], y = diffractogram[options['y_vals']], c=[(1,1,1,0)], edgecolors=[next(colours)], linewidths=plt.rcParams['lines.markeredgewidth'], zorder=2) #, edgecolors=np.array([next(colours)])) - - - # Set the labels to the relevant values for diffractogram plot - if not options['ylabel'] or options['ylabel'] == options['heatmap.ylabel']: - options['ylabel'] = options['diff.ylabel'] - if not options['yunit'] or options['yunit'] == options['heatmap.yunit']: - options['yunit'] = options['diff.yunit'] - - - options['hide_y_ticklabels'] = True - options['hide_y_ticks'] = True - - - if options['plot_diff'] and len(data['path']) == 2: - diff = data['diffractogram'][0] - diff['I'] = diff['I'] - data['diffractogram'][1]['I'] - diff['I'] = diff['I'] - 0.75 - - diff.plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=next(colours)) - - - # Adjust the plot to make it prettier - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - - # PLOT REFLECTION TABLES - if options['reflections_plot'] and options['reflections_data']: - options['xlim'] = ax.get_xlim() - options['to_wavelength'] = data['wavelength'][0] # By default, the wavelength of the first diffractogram will be used for these. - - # Plot each reflection table in the relevant axis - for reflections_params, axis in zip(options['reflections_data'], ref_axes): - plot_reflection_table(data=data, reflections_params=reflections_params, ax=axis, options=options) - - # Print the reflection indices. - if options['reflections_indices'] and options['reflections_data']: - options['xlim'] = ax.get_xlim() - options['to_wavelength'] = data['wavelength'][0] # By default, the wavelength of the first diffractogram will be used for this. - - for reflections_params in options['reflections_data']: - plot_reflection_indices(data=data, reflections_params=reflections_params, ax=indices_ax, options=options) - - - ############################################################################################################################################################ - ##### UPDATE WIDGET ######################################################################################################################################## - ############################################################################################################################################################ - - if options['interactive_session_active']: - options['current_y_offset'] = options['widget'].kwargs['offset_y'] - update_widgets(data=data, options=options) - - - - return data['diffractogram'], fig, ax - - - -def generate_heatmap(data, options={}): - - required_options = ['x_tick_locators', 'heatmap_y_tick_locators', 'heatmap_normalise', 'normalisation_range', 'increase_contrast'] - - default_options = { - 'x_tick_locators': [0.5, 0.1], - 'heatmap_y_tick_locators': [10, 5], # Major ticks for every 10 scans, minor for every 5 - 'heatmap_normalise': False, - 'normalisation_range': None, - 'increase_contrast': False, - 'contrast_factor': 100 - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - twotheta = [] - intensities = [] - scans = [] - - for i, d in enumerate(data['diffractogram']): - - # Find normalisation factor - if options['heatmap_normalise'] and options['normalisation_range']: - mean_background = d['I'].loc[(d['2th'] > options['normalisation_range'][0]) & (d['2th'] < options['normalisation_range'][1])].mean() - - d['I'] = d['I'] / mean_background - - - if options['increase_contrast']: - - if d['I'].min() < 0: - d['I'] = d['I'] - d['I'].min() - - d['I'] =d['I']**(1/options['contrast_factor']) - - twotheta = np.append(twotheta, d['2th'].to_numpy()) - intensities = np.append(intensities, d['I'].to_numpy()) - scans = np.append(scans, np.full(len(d['2th'].to_numpy()), int(i))) - - - heatmap = pd.DataFrame({'2th': twotheta, 'scan': scans, 'I': intensities}) - xrd.io.translate_wavelengths(data=heatmap, wavelength=data['wavelength'][0]) - - min_dict = {'2th': heatmap['2th'].min(), '2th_cuka': heatmap['2th_cuka'].min(), '2th_moka': heatmap['2th_moka'].min(), - 'q': heatmap['q'].min(), 'q2': heatmap['q2'].min(), 'q4': heatmap['q4'].min(), '1/d': heatmap['1/d'].min()} - - max_dict = {'2th': heatmap['2th'].max(), '2th_cuka': heatmap['2th_cuka'].max(), '2th_moka': heatmap['2th_moka'].max(), - 'q': heatmap['q'].max(), 'q2': heatmap['q2'].max(), 'q4': heatmap['q4'].max(), '1/d': heatmap['1/d'].max()} - - - ndatapoints = len(data['diffractogram'][0]['2th']) - - xlims = [0, ndatapoints, 0, ndatapoints] # 0: xmin, 1: xmax, 2: xmin_start, 3: xmax_start - xticks = {} - xticklabels = {} - - for xval in min_dict.keys(): - - # Add xticks labels - label_max = aux.floor(max_dict[xval], roundto=options['x_tick_locators'][0]) - label_min = aux.ceil(min_dict[xval], roundto=options['x_tick_locators'][0]) - label_steps = (label_max - label_min)/options['x_tick_locators'][0] - - xticklabels[xval] = np.linspace(label_min, label_max, num=int(label_steps)+1) - - # Add xticks - xval_span = max_dict[xval] - min_dict[xval] - steps = xval_span / ndatapoints - - - xticks_xval = [] - - for tick in xticklabels[xval]: - xticks_xval.append((tick-min_dict[xval])/steps) - - xticks[xval] = xticks_xval - - - # FIXME COMMENTED OUT THIS LINE TO FIX SOMETHING - NOT SURE WHAT UNINTENDED CONSEQUENCES THAT MAY HAVE.... - #options['x_tick_locators'] = None - - heatmap = heatmap.reset_index().pivot_table(index='scan', columns='2th', values='I') - - options['heatmap_xlim'] = xlims - - - - # Get temperatures if HTXRD-scans - scan_numbers = [] - - temperatures = [] - - # FIXME This is a very bad check for whether it is HTXRD or not - it bascailly just excludes any files that has a .poni-file passed. Make more rigorous in future! - if not 'calibrant' in data.keys(): - for i, filename in enumerate(data['path']): - scan_numbers.append(i) - temperatures.append(xrd.io.read_metadata_from_xy(filename)['temperature']) - - yticks = scan_numbers[0::options['heatmap_y_tick_locators'][0]] - yticks.append(scan_numbers[-1]) - - if not temperatures[0]: - yticklabels = yticks - options['heatmap.ylabel'] = 'Scan number' - options['heatmap.yunit'] = None - - else: - yticklabels = temperatures[0::options['heatmap_y_tick_locators'][0]] - yticklabels.append(temperatures[-1]) - options['heatmap.ylabel'] = 'Temperature' - options['heatmap.yunit'] = '$^\circ$C' - - else: - yticks, yticklabels = None, None - - - - - return heatmap, xticks, xticklabels, yticks, yticklabels - - - - - - - -# #results = np.transpose(np.vstack([twotheta, scans, intensities])) - - -def determine_grid_layout(options): - - - #aspect_ratio = int(options['format_params']['aspect_ratio'].split(':')[0]) / int(options['format_params']['aspect_ratio'].split(':')[1]) - - nrows = 1 if not options['reflections_indices'] else 2 - - if options['reflections_plot']: - for reference in options['reflections_data']: - nrows += 1 - - options['format_params']['nrows'] = nrows - - if not 'grid_ratio_height' in options['format_params'].keys(): - options['format_params']['grid_ratio_height'] = [0.6 for i in range(nrows-1)]+[10] - - return options - - - - - -def plot_diffractogram_interactive(data, options): - - - # Format here is xminmax[0]: xmin, xminmax[1]: xmax, xminmax[2]: xmin_start, xminmax[3]: xmax_start, where "_start" denotes starting value of the slider - xminmax = { '2th': [None, None, None, None], '2th_cuka': [None, None, None, None], '2th_moka': [None, None, None, None], - 'd': [None, None, None, None], '1/d': [None, None, None, None], - 'q': [None, None, None, None], 'q2': [None, None, None, None], 'q4': [None, None, None, None], - 'heatmap': [None, None, None, None], 'start': [None, None, None, None]} - - yminmax = { 'diff': [None, None, None, None], 'heatmap': [None, None, None, None], 'start': [None, None, None, None]} - - update_xminmax(xminmax=xminmax, data=data, options=options) - update_yminmax(yminmax=yminmax, data=data, options=options) - - options['xminmax'], options['yminmax'] = xminmax, yminmax - - # Get start values for ylim slider based on choice (FIXME This can be impleneted into update_yminmax). Can also make a 'start' item that stores the start values, instead of having 4 items in 'diff' as it is now. - if options['heatmap']: - ymin = yminmax['heatmap'][0] - ymax = yminmax['heatmap'][1] - ymin_start = yminmax['heatmap'][0] - ymax_start = yminmax['heatmap'][1] - - elif not options['heatmap']: - ymin = yminmax['diff'][0] - ymax = yminmax['diff'][1] - ymin_start = yminmax['diff'][2] - ymax_start = yminmax['diff'][3] - - - # FIXME The start values for xlim should probably also be decided by initial value of x_vals, and can likewise be implemented in update_xminmax() - - - - options['widgets'] = { - 'xlim': { - 'w': widgets.FloatRangeSlider(value=[xminmax['start'][2], xminmax['start'][3]], min=xminmax['start'][0], max=xminmax['start'][1], step=0.5, layout=widgets.Layout(width='95%')), - 'state': options['x_vals'], - '2th_default': {'min': xminmax['2th'][0], 'max': xminmax['2th'][1], 'value': [xminmax['2th'][0], xminmax['2th'][1]], 'step': 0.5}, - '2th_cuka_default': {'min': xminmax['2th_cuka'][0], 'max': xminmax['2th_cuka'][1], 'value': [xminmax['2th_cuka'][0], xminmax['2th_cuka'][1]], 'step': 0.5}, - '2th_moka_default': {'min': xminmax['2th_moka'][0], 'max': xminmax['2th_moka'][1], 'value': [xminmax['2th_moka'][0], xminmax['2th_moka'][1]], 'step': 0.5}, - 'd_default': {'min': xminmax['d'][0], 'max': xminmax['d'][1], 'value': [xminmax['d'][0], xminmax['d'][1]], 'step': 0.5}, - '1/d_default': {'min': xminmax['1/d'][0], 'max': xminmax['1/d'][1], 'value': [xminmax['1/d'][0], xminmax['1/d'][1]], 'step': 0.5}, - 'q_default': {'min': xminmax['q'][0], 'max': xminmax['q'][1], 'value': [xminmax['q'][0], xminmax['q'][1]], 'step': 0.5}, - 'q2_default': {'min': xminmax['q2'][0], 'max': xminmax['q2'][1], 'value': [xminmax['q2'][0], xminmax['q2'][1]], 'step': 0.5}, - 'q4_default': {'min': xminmax['q4'][0], 'max': xminmax['q4'][1], 'value': [xminmax['q4'][0], xminmax['q4'][1]], 'step': 0.5}, - 'heatmap_default': {'min': xminmax['heatmap'][0], 'max': xminmax['heatmap'][1], 'value': [xminmax['heatmap'][0], xminmax['heatmap'][1]], 'step': 10} - }, - 'ylim': { - 'w': widgets.FloatRangeSlider(value=[yminmax['start'][2], yminmax['start'][3]], min=yminmax['start'][0], max=yminmax['start'][1], step=0.01, layout=widgets.Layout(width='95%')), - 'state': 'heatmap' if options['heatmap'] else 'diff', - 'diff_default': {'min': yminmax['diff'][0], 'max': yminmax['diff'][1], 'value': [yminmax['diff'][2], yminmax['diff'][3]], 'step': 0.01}, - 'heatmap_default': {'min': yminmax['heatmap'][0], 'max': yminmax['heatmap'][1], 'value': [yminmax['heatmap'][0], yminmax['heatmap'][1]], 'step': 0.01} - } - } - - if options['reflections_data']: - w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_diffractogram), data=widgets.fixed(data), options=widgets.fixed(options), - x_vals=widgets.Dropdown(options=['2th', 'd', '1/d', 'q', 'q2', 'q4', '2th_cuka', '2th_moka'], value='2th', description='X-values'), - scatter=widgets.ToggleButton(value=False), - line=widgets.ToggleButton(value=True), - reflections_plot=widgets.ToggleButton(value=True), - reflections_indices=widgets.ToggleButton(value=False), - heatmap=widgets.ToggleButton(value=options['heatmap']), - xlim=options['widgets']['xlim']['w'], - ylim=options['widgets']['ylim']['w'], - offset_y=widgets.BoundedFloatText(value=options['offset_y'], min=-5, max=5, step=0.01, description='offset_y'), - offset_x=widgets.BoundedFloatText(value=options['offset_x'], min=-1, max=1, step=0.01, description='offset_x') - ) - - else: - w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_diffractogram), data=widgets.fixed(data), options=widgets.fixed(options), - x_vals=widgets.Dropdown(options=['2th', 'd', '1/d', 'q', 'q2', 'q4', '2th_cuka', '2th_moka'], value='2th', description='X-values'), - scatter=widgets.ToggleButton(value=False), - line=widgets.ToggleButton(value=True), - heatmap=widgets.ToggleButton(value=options['heatmap']), - xlim=options['widgets']['xlim']['w'], - ylim=options['widgets']['ylim']['w'], - offset_y=widgets.BoundedFloatText(value=options['offset_y'], min=-5, max=5, step=0.01, description='offset_y'), - offset_x=widgets.BoundedFloatText(value=options['offset_x'], min=-1, max=1, step=0.01, description='offset_x')) - - - options['widget'] = w - - display(w) - - -def update_xminmax(xminmax, data, options={}): - ''' Finds minimum and maximum values of each column and updates the minmax dictionary to contain the correct values. - - Input: - minmax (dict): contains ''' - - xminmax['2th'] = [None, None, None, None] - for index, diffractogram in enumerate(data['diffractogram']): - - if not xminmax['2th'][0] or diffractogram['2th'].min() < xminmax['2th'][0]: - xminmax['2th'][0] = diffractogram['2th'].min() - min_index = index - - if not xminmax['2th'][1] or diffractogram['2th'].max() > xminmax['2th'][1]: - xminmax['2th'][1] = diffractogram['2th'].max() - max_index = index - - - xminmax['2th'][2], xminmax['2th'][3] = xminmax['2th'][0], xminmax['2th'][1] - - xminmax['2th_cuka'][0], xminmax['2th_cuka'][1] = data['diffractogram'][min_index]['2th_cuka'].min(), data['diffractogram'][max_index]['2th_cuka'].max() - xminmax['2th_cuka'][2], xminmax['2th_cuka'][3] = xminmax['2th_cuka'][0], xminmax['2th_cuka'][1] - - xminmax['2th_moka'][0], xminmax['2th_moka'][1] = data['diffractogram'][min_index]['2th_moka'].min(), data['diffractogram'][max_index]['2th_moka'].max() - xminmax['2th_moka'][2], xminmax['2th_moka'][3] = xminmax['2th_moka'][0], xminmax['2th_moka'][1] - - xminmax['d'][0], xminmax['d'][1] = data['diffractogram'][max_index]['d'].min(), data['diffractogram'][min_index]['d'].max() # swapped, intended - xminmax['d'][2], xminmax['d'][3] = xminmax['d'][0], xminmax['d'][1] - - xminmax['1/d'][0], xminmax['1/d'][1] = data['diffractogram'][min_index]['1/d'].min(), data['diffractogram'][max_index]['1/d'].max() - xminmax['1/d'][2], xminmax['1/d'][3] = xminmax['1/d'][0], xminmax['1/d'][1] - - xminmax['q'][0], xminmax['q'][1] = data['diffractogram'][min_index]['q'].min(), data['diffractogram'][max_index]['q'].max() - xminmax['q'][2], xminmax['q'][3] = xminmax['q'][0], xminmax['q'][1] - - xminmax['q2'][0], xminmax['q2'][1] = data['diffractogram'][min_index]['q2'].min(), data['diffractogram'][max_index]['q2'].max() - xminmax['q2'][2], xminmax['q2'][3] = xminmax['q2'][0], xminmax['q2'][1] - - xminmax['q4'][0], xminmax['q4'][1] = data['diffractogram'][min_index]['q4'].min(), data['diffractogram'][max_index]['q4'].max() - xminmax['q4'][2], xminmax['q4'][3] = xminmax['q4'][0], xminmax['q4'][1] - - - xminmax['heatmap'] = options['heatmap_xlim'] # This value is set in the generate_heatmap()-function - - - xminmax['start'][0], xminmax['start'][1] = xminmax[options['x_vals']][0], xminmax[options['x_vals']][1] - xminmax['start'][2], xminmax['start'][3] = xminmax[options['x_vals']][2], xminmax[options['x_vals']][3] - - -def update_yminmax(yminmax: dict, data: dict, options={}) -> None: - - yminmax['diff'] = [None, None, None, None] - # Go through diffractograms and find the minimum and maximum intensity values - for diffractogram in data['diffractogram']: - if not yminmax['diff'][0] or (yminmax['diff'][0] > (diffractogram['I'].min())): - yminmax['diff'][0] = diffractogram['I'].min() - - if not yminmax['diff'][1] or (yminmax['diff'][1] < (diffractogram['I'].max())): - yminmax['diff'][1] = diffractogram['I'].max() - - - # Set start values of ymin and ymax to be slightly below lowest data points and slightly above highest data points to give some whitespace around the plot - yminmax['diff'][2] = yminmax['diff'][0] - 0.1*yminmax['diff'][1] - yminmax['diff'][3] = yminmax['diff'][1] + 0.2*yminmax['diff'][1] - - # Allow for adjustment up to five times ymax above and below data - yminmax['diff'][0] = yminmax['diff'][0] - 5*yminmax['diff'][1] - yminmax['diff'][1] = yminmax['diff'][1]*5 - - - # Set start values to the edges of the dataset - yminmax['heatmap'][0], yminmax['heatmap'][1] = 0, data['heatmap'].shape[0] - yminmax['heatmap'][2], yminmax['heatmap'][3] = yminmax['heatmap'][0], yminmax['heatmap'][1] - - - if options['heatmap']: - yminmax['start'][0], yminmax['start'][1] = yminmax['heatmap'][0], yminmax['heatmap'][1] - yminmax['start'][2], yminmax['start'][3] = yminmax['heatmap'][0], yminmax['heatmap'][1] - - else: - # The third and fourth index are different here to not be zoomed completely out to begin with. - yminmax['start'][0], yminmax['start'][1] = yminmax['diff'][0], yminmax['diff'][1] - yminmax['start'][2], yminmax['start'][3] = yminmax['diff'][2], yminmax['diff'][3] - - -def update_defaults(widget: dict, minmax: dict) -> None: - ''' Updates the default x- or y-limits of a given widget. Refer to plot_diffractogram_interactive() to see the form of the widget that is passed in. An update of the min/max-values is done just prior to calling this function. - Changes dictionaries in place. - - Input: - widget (dict): A dictionary containing the widget itself (widget['w']) and all its default-values (e.g. widget['2th_default']) - minmax (dict): A dictionary containing min and max values, as well as min_start and max_start values. (e.g. minmax['2th'] is a list with four elements: [xmin, xmax, xmin_start, xmax_start]) - - Output: - None.''' - - for name, attr in widget.items(): - if name.endswith('default'): - attr['min'] = minmax[name.replace('_default', '')][0] - attr['max'] = minmax[name.replace('_default', '')][1] - attr['value'] = [minmax[name.replace('_default', '')][2], minmax[name.replace('_default', '')][3]] - - -def update_widgets(data, options): - - - for widget_name, widget in options['widgets'].items(): - - # Make changes to xlim-widget - if widget_name == 'xlim': - # First update the min and max values - update_xminmax(xminmax=options['xminmax'], data=data, options=options) - update_defaults(widget=widget, minmax=options['xminmax']) - - - if options['heatmap'] and (widget['state'] != 'heatmap'): - - - setattr(widget['w'], 'min', widget['heatmap_default']['min']) - setattr(widget['w'], 'max', widget['heatmap_default']['max']) - setattr(widget['w'], 'value', widget['heatmap_default']['value']) - setattr(widget['w'], 'step', widget['heatmap_default']['step']) - - widget['state'] = 'heatmap' - - elif not options['heatmap'] and (widget['state'] != options['x_vals']): - # Then loop through all attributes in the widget and change to current mode. - for arg in widget[f'{options["x_vals"]}_default']: - - # If new min value is larger than previous max, or new max value is smaller than previous min, set the opposite first - if arg == 'min': - if widget[f'{options["x_vals"]}_default']['min'] > getattr(widget['w'], 'max'): - setattr(widget['w'], 'max', widget[f'{options["x_vals"]}_default']['max']) - - elif arg == 'max': - if widget[f'{options["x_vals"]}_default']['max'] < getattr(widget['w'], 'min'): - setattr(widget['w'], 'min', widget[f'{options["x_vals"]}_default']['min']) - - - setattr(widget['w'], arg, widget[f'{options["x_vals"]}_default'][arg]) - - - widget['state'] = options['x_vals'] - - # Make changes to ylim-widget - elif widget_name == 'ylim': - update_yminmax(yminmax=options['yminmax'], data=data, options=options) - update_defaults(widget=widget, minmax=options['yminmax']) - - state = 'heatmap' if options['heatmap'] else 'diff' - - if widget['state'] != state or options['offset_change']: - - for arg in widget[f'{state}_default']: - # If new min value is larger than previous max, or new max value is smaller than previous min, set the opposite first - if arg == 'min': - if widget[f'{state}_default']['min'] > getattr(widget['w'], 'max'): - setattr(widget['w'], 'max', widget[f'{state}_default']['max']) - - elif arg == 'max': - if widget[f'{state}_default']['max'] < getattr(widget['w'], 'min'): - setattr(widget['w'], 'min', widget[f'{state}_default']['min']) - - - setattr(widget['w'], arg, widget[f'{state}_default'][arg]) - - options['offset_change'] = False - widget['state'] = state - - - - -def plot_reflection_indices(data, reflections_params, ax, options={}): - ''' Print reflection indices from output generated by VESTA. - - Required contents of data: - path (str): relative path to reflection table file''' - - required_options = ['reflection_indices', 'text_colour', 'hide_indices'] - - default_options = { - 'reflection_indices': 3, # Number of reflection indices to plot, from highest intensity and working its way down - 'text_colour': 'black', - 'hide_indices': False - } - - reflections_params = aux.update_options(options=reflections_params, required_options=required_options, default_options=default_options) - - if not reflections_params['hide_indices']: - reflection_table = xrd.io.load_reflection_table(data=data, reflections_params=reflections_params, options=options) - - if reflections_params['reflection_indices'] > 0: - - # Get the data['reflection_indices'] number of highest reflections within the subrange options['xlim'] - x_vals = 'heatmap' if options['heatmap'] else options['x_vals'] - reflection_indices = reflection_table.loc[(reflection_table[x_vals] > options['xlim'][0]) & (reflection_table[x_vals] < options['xlim'][1])].nlargest(options['reflection_indices'], 'I') - - # Plot the indices - for i in range(reflections_params['reflection_indices']): - if reflection_indices.shape[0] > i: - ax.text(s=f'({reflection_indices["h"].iloc[i]} {reflection_indices["k"].iloc[i]} {reflection_indices["l"].iloc[i]})', x=reflection_indices[x_vals].iloc[i], y=0, fontsize=2.5, rotation=90, va='bottom', ha='center', c=reflections_params['text_colour']) - - - if options['xlim']: - ax.set_xlim(options['xlim']) - - ax.axis('off') - - - return - -def plot_reflection_table(data, reflections_params, ax=None, options={}): - ''' Plots a reflection table from output generated by VESTA. - - Required contents of data: - path (str): relative path to reflection table file''' - - required_options = ['reflection_indices', 'reflections_colour', 'min_alpha', 'wavelength', 'format_params', 'rc_params', 'label'] - - default_options = { - 'reflection_indices': 0, # Number of indices to print - 'reflections_colour': [0,0,0], - 'min_alpha': 0, - 'wavelength': 1.54059, # CuKalpha, [Å] - 'format_params': {}, - 'rc_params': {}, - 'label': None, - 'heatmap': False - } - - - if 'colour' in reflections_params.keys(): - options['reflections_colour'] = reflections_params['colour'] - if 'min_alpha' in reflections_params.keys(): - options['min_alpha'] = reflections_params['min_alpha'] - if 'reflection_indices' in reflections_params.keys(): - options['reflection_indices'] = reflections_params['reflection_indices'] - if 'label' in reflections_params.keys(): - options['label'] = reflections_params['label'] - if 'wavelength' in reflections_params.keys(): - options['wavelength'] = reflections_params['wavelength'] - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if not ax: - _, ax = btp.prepare_plot(options) - - x_vals = 'heatmap' if options['heatmap'] else options['x_vals'] - - reflection_table = xrd.io.load_reflection_table(data=data, reflections_params=reflections_params, options=options) - reflections, intensities = reflection_table[x_vals], reflection_table['I'] - - - - colours = [] - - for ref, intensity in zip(reflections, intensities): - - colour = list(options['reflections_colour']) - rel_intensity = (intensity / intensities.max())*(1-options['min_alpha']) + options['min_alpha'] - colour.append(rel_intensity) - colours.append(colour) - - - - ax.vlines(x=reflections, ymin=-1, ymax=1, colors=colours, lw=0.5) - ax.set_ylim([-0.5,0.5]) - - - ax.tick_params(which='both', bottom=False, labelbottom=False, right=False, labelright=False, left=False, labelleft=False, top=False, labeltop=False) - - if options['xlim']: - ax.set_xlim(options['xlim']) - - - if options['label']: - xlim_range = ax.get_xlim()[1] - ax.get_xlim()[0] - ylim_avg = (ax.get_ylim()[0]+ax.get_ylim()[1])/2 - - ax.text(s=reflections_params['label'], x=(ax.get_xlim()[0]-0.01*xlim_range), y=ylim_avg, ha = 'right', va = 'center') - - - - - -def prettify_labels(label): - - labels_dict = { - '2th': '2$\\theta$', - 'I': 'Intensity' - } - - return labels_dict[label] - - - -def reverse_diffractograms(diffractograms): - - rev_diffractograms = [] - - for i in len(diffractograms): - rev_diffractograms.append(diffractograms.pop()) - - return rev_diffractograms - - - -def make_animation(data: dict, options={}): - - default_options = { - 'cmap': 'inferno', - 'contrast': False, - 'contrast_factor': 1/3, - 'save_path': 'diff_animation.gif', - 'fps': 5 - } - - options = aux.update_options(options=options, default_options=default_options, required_options=default_options.keys()) - - if not isinstance(data['path'], list): - data['path'] = aux.get_filenames(data['path'], ext='dat') - - - if not os.path.isdir('tmp'): - os.makedirs('tmp') - - # Scale image to make GIF smaller - # - options['format_params']['width'] = 5 - options['format_params']['height'] = 5 - - options['format_params']['dpi'] = 200 - - for i, scan in enumerate(data['path']): - - giffig, gifax = btp.prepare_plot(options=options) - - img = xrd.io.get_image_array(scan) - - if options['contrast']: - img[img < 0] = 0.00000001 - img = np.log(img) - img[img < 0] = 0 - - gifax.imshow(img, cmap=options['cmap']) - - plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png')) - plt.close() - - - img_paths = aux.get_filenames('tmp', ext='png') - - frames = [] - for path in img_paths: - frame = Image.open(path) - frames.append(frame) - - frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0) - - shutil.rmtree('tmp') - - - -def plot_refinement(data, options={}): - - - required_options = ['diff_offset', 'index', 'title', 'xlim', 'r_wp', 'r_exp', 'wp'] - - default_options = { - 'diff_offset': .10, - 'index': -1, - 'title': None, - 'xlim': None, - 'r_wp': True, - 'r_exp': False, - 'wp': False, - 'wavelength': None, - 'xlabel': '2$\\theta$', 'xunit': '$^{\circ}$', - 'ylabel': 'Intensity', 'yunit': 'arb. u.', - 'text': [], - 'text_pos': [0.7, 0.9], - 'text_pos_increments': [0, -0.1], - 'reflections_plot': False, # whether to plot reflections as a plot - 'reflections_indices': False, # whether to plot the reflection indices - 'reflections_data': None, # Should be passed as a list of dictionaries on the form {path: rel_path, reflection_indices: number of indices, colour: [r,g,b], min_alpha: 0-1] - - } - - options = aux.update_options(options=options, default_options=default_options, required_options=required_options) - - df = pd.read_csv(data['path'], delim_whitespace=True, header=None) - df.columns = ['2th', 'Yobs', 'Ycalc', 'diff'] - df['diff'] = df['diff'] - options['diff_offset']*(df['Yobs'].max() - df['Yobs'].min()) - - - if not isinstance(data['results'], list): - data['results'] = [data['results']] - - results = { - 'vol': [], - 'mass': [], - 'wp': [], - 'a': [], - 'b': [], - 'c': [], - 'alpha': [], - 'beta': [], - 'gamma': [] - } - - for result in data['results']: - result = xrd.refinement.read_results(path=result) - - r_wp = result['r_wp'].iloc[options['index']] - r_exp = result['r_exp'].iloc[options['index']] - - for attr in results.keys(): - results[attr].append(result[attr].iloc[options['index']]) - - # CREATE AND ASSIGN AXES - - # Makes a list out of reflections_data if it only passed as a dict, as it will be looped through later - if options['reflections_data']: - if not isinstance(options['reflections_data'], list): - options['reflections_data'] = [options['reflections_data']] - - - # Determine the grid layout based on how many sets of reflections data has been passed - if options['reflections_data'] and len(options['reflections_data']) >= 1: - options = determine_grid_layout(options=options) - - # Create the Figure and Axes objects - fig, ax = btp.prepare_plot(options=options) - - # Assign the correct axes to the indicies, reflections and figure itself - if options['reflections_plot'] or options['reflections_indices']: - - if options['reflections_indices']: - indices_ax = ax[0] - - if options['reflections_plot']: - ref_axes = [axx for axx in ax[range(1,len(options['reflections_data'])+1)]] - - else: - ref_axes = [axx for axx in ax[range(0,len(options['reflections_data']))]] - - ax = ax[-1] - - df.plot.scatter(x='2th', y='Yobs', ax=ax, c='black', marker='$\u25EF$', s=plt.rcParams['lines.markersize']*10) - df.plot(x='2th', y='Ycalc', ax=ax, c='red') - df.plot(x='2th', y='diff', ax=ax) - - - - - if options['sample']: - options['text'].append([options['sample'], [options['text_pos'][0]*df['2th'].max(), options['text_pos'][1]*df['Yobs'].max()]]) - options['text_pos'][0] += options['text_pos_increments'][0] - options['text_pos'][1] += options['text_pos_increments'][1] - - if options['wavelength']: - options['text'].append([f'$\lambda$ = {options["wavelength"]} Å', [options['text_pos'][0]*df['2th'].max(), options['text_pos'][1]*df['Yobs'].max()]]) - options['text_pos'][0] += options['text_pos_increments'][0] - options['text_pos'][1] += options['text_pos_increments'][1] - - if options['wp']: - for i, (result, label) in enumerate(zip(data['results'], options['labels'])): - options['text'].append([f'{label}: {np.round(float(results["wp"][i]), 1)}%', [options['text_pos'][0]*df['2th'].max(), options['text_pos'][1]*df['Yobs'].max()]]) - - - #ax.text(x=0.7*df['2th'].max(), y=ypos*df['Yobs'].max(), s=f'{label}: {np.round(float(results["wp"][i]), 2)}%', fontsize=20) - options['text_pos'][0] += options['text_pos_increments'][0] - options['text_pos'][1] += options['text_pos_increments'][1] - - if options['r_wp']: - options['text'].append(['R$_{wp}$ = '+f'{np.round(r_wp, 2)}', [options['text_pos'][0]*df['2th'].max(), options['text_pos'][1]*df['Yobs'].max()]]) - options['text_pos'][0] += options['text_pos_increments'][0] - options['text_pos'][1] += options['text_pos_increments'][1] - #ax.text(x=0.7*df['2th'].max(), y=0.7*df['Yobs'].max(), s='R$_{wp}$ = '+f'{r_wp}') - - if options['r_exp']: - options['text'].append(['R$_{exp}$ = '+f'{np.round(r_exp, 2)}', [options['text_pos'][0]*df['2th'].max(), options['text_pos'][1]*df['Yobs'].max()]]) - options['text_pos'][0] += options['text_pos_increments'][0] - options['text_pos'][1] += options['text_pos_increments'][1] - #ax.text(x=0.70*df['2th'].max(), y=0.60*df['Yobs'].max(), s='R$_{exp}$ = '+f'{r_exp}') - - - if 'xlim' not in options.keys() or options['xlim'] == None: - options['xlim'] = [df['2th'].min(), df['2th'].max()] - - - fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options) - - # PLOT REFLECTION TABLES - if options['reflections_plot'] and options['reflections_data']: - options['xlim'] = ax.get_xlim() - options['to_wavelength'] = options['wavelength'] # By default, the wavelength of the first diffractogram will be used for these. - - # Plot each reflection table in the relevant axis - for reflections_params, axis in zip(options['reflections_data'], ref_axes): - plot_reflection_table(data=data, reflections_params=reflections_params, ax=axis, options=options) - - - - diff --git a/nafuma/xrd/refinement.py b/nafuma/xrd/refinement.py deleted file mode 100644 index 8b0eb5b..0000000 --- a/nafuma/xrd/refinement.py +++ /dev/null @@ -1,1132 +0,0 @@ -import os -import shutil -import subprocess -import re -import numpy as np -import time -import datetime -import warnings -import json - -import pandas as pd - -import nafuma.auxillary as aux - - -def make_initial_inp(data: dict, options={}): - - # required_options = ['filename', 'overwrite', 'include', 'save_results', 'save_dir', 'instrument', 'topas_options', 'background', 'capillary', 'th2_offset', 'fit_peak_width', 'simple_axial_model', 'TCHZ_Peak_Type', 'start', 'finish', 'exclude', 'magnetic_atoms', 'radiation', 'magnetic_space_group', 'interval'] - - default_options = { - 'filename': 'start.inp', - 'overwrite': False, - 'save_results': False, - 'save_dir': 'results/', - 'instrument': None, - 'radiation': 'synchrotron', - 'magnetic_space_group': None, - 'magnetic_atoms': [], - 'include': [], # Any files that should be included with #include - 'topas_options': {}, - 'background': 7, - 'capillary': False, - 'th2_offset': False, - 'fit_peak_width': False, - 'simple_axial_model': False, - 'TCHZ_Peak_Type': False, - 'interval': [None, None], # Start and finish values that TOPAS should refine on. Overrides 'start' and 'finish' - 'start': None, # Start value only. Overridden by 'interval' if this is set. - 'finish': None, # Finish value only. Overridden by 'interval' if this is set. - 'exclude': [], # Excluded regions. List of lists. - 'manual_background': False #Upload a background-file - } - - options = aux.update_options(options=options, default_options=default_options) - options['topas_options'] = update_topas_options(options=options) - - - if not os.path.exists(options['filename']) or options['overwrite']: - with open(options['filename'], 'w') as fout: - write_headers(fout=fout, options=options) - - - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('\'START OF INP - XXXX') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('r_wp 0 r_exp 0 r_p 0 r_wp_dash 0 r_p_dash 0 r_exp_dash 0 weighted_Durbin_Watson 0 gof 0') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - - write_xdd(fout=fout, data=data, options=options) - - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('\'PARAMETER DEFINITONS') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - - for i, str in enumerate(data['str']): - fout.write('\n\n') - write_params(fout=fout, data=data, options=options, index=i) - - - - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('\'STR DEFINITIONS') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - - for i, str in enumerate(data['str']): - fout.write('\n\n') - write_str(fout=fout, data=data, options=options, index=i) - - - if 'peak' in data.keys(): - - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - fout.write('\'PEAK PHASES') - fout.write('\n\'------------------------------------------------------------------------------------------------------------------------------------------\n') - - write_peaks(fout=fout, data=data, options=options, index=i) - - - - with open(options['filename'], 'r') as fin: - - lines = [] - for statement in data['define_statements']: - lines.append(f'\'#define {statement}\n') - - lines.append('\n') - - lines += fin.readlines() - - - with open(options['filename'], 'w') as fout: - for line in lines: - fout.write(line) - -def make_general_background_in_q(data, options): - #Function where you get a txt.-file of q-values, based on the 2th values you have picked out manually from a data set. Idea is that the same background points can be used for several samples, measured at several wavelengths. - #The input is a list of 2th-values that the user has found by manually inspecting a data set - import numpy as np - import nafuma.xrd as xrd - import matplotlib.pyplot as plt - - default_options = { - 'save_dir': 'background', - 'filename': 'test' - } - - options = aux.update_options(options=options, default_options=default_options) - - list_of_2th_values = data['2th_values'] - - #x_background_points=list_of_2th_values - x_background_points_q=[] - for x in list_of_2th_values: - q=np.abs((4*np.pi/data['wavelength'])*np.sin(x/2 * np.pi/180)) - x_background_points_q.append(q) - - background_points=pd.DataFrame(x_background_points_q) - background_filename="C:/Users/halvorhv/OneDriveUiO/1_OrderPaper/analysis/exsitu/probing_ordering/refinements/"+options['filename']+"_background_points_in_q.txt" - background_points.to_csv(background_filename,index=None, header=None)#,index=None, header=None,sep=' ') - -def make_manual_background(data, options): - #FIXME generalize this so it works properly - #FIXME fix so that plotting is optional - import numpy as np - import nafuma.xrd as xrd - import matplotlib.pyplot as plt - - default_options = { - 'plot_background': True, - 'interval_length': 0.05, #picking out the region to be fitted for each background point, bigger interval-length means a bigger region for each point. - 'save_dir': 'background' - } - if "noheaders" in data['path'][0]: - filename = os.path.basename(data['path'][0]).split('_noheaders.')[0] - else: - filename = os.path.basename(data['path'][0]).split('.')[0] - - - options = aux.update_options(options=options, default_options=default_options) - - #data['background_in_q'] is a .txt-file with one column of x-values that are good starting points for background determination, as they should not include any peaks for that specific material - - #importing the pre-made background points in Q into a dataframe - df_backgroundpoints_q=pd.read_csv(data['background_in_q'],names=["background_q"]) - - diffractogram, wavelength = xrd.io.read_xy(data=data) - - #transferring q-range background to the respective wavelength - x_background_points=[] - for q in df_backgroundpoints_q["background_q"]: - twotheta=2*180/np.pi*np.arcsin(q*wavelength/(4*np.pi)) - x_background_points.append(twotheta) - - intervallength=options['interval_length'] - - background=pd.DataFrame() - - for i, x in enumerate(x_background_points): - test=diffractogram.loc[(diffractogram["2th"]>x-intervallength) & (diffractogram["2th"] str: - ''' Takes a template and creates an entry with xdd as path to file and with number num.''' - - temp_xdd = find_xdd_in_inp(options['template']) - - - num_str = f'{num}'.zfill(4) - - # Replace diffractogram-path - s = template.replace(temp_xdd, xdd).replace('XXXX', num_str) - s = s.replace(os.path.basename(temp_xdd).split('.')[0], os.path.basename(xdd).split('.')[0]) - - return s - - - -def find_xdd_in_inp(path: str) -> str: - ''' Finds the path to the .xy / .xye scan in a given .INP-file. Assumes only one occurence of xdd and will return the last one no matter what, but outputs a UserWarning if more than one xdd is found.''' - - with open(path, 'r') as f: - lines = f.readlines() - - - xdds = 0 - for line in lines: - if 'xdd' in line: - xdd = line.split()[-1].strip('"') - xdds += 1 - - if xdds > 1: - warnings.warn(f'More than one path was found in {path}. Returning last occurence - please make sure this is what you want!') - - return xdd - - -def refine(data: dict, options={}): - ''' Calls TOPAS from options['topas_path'], which should point to tc.exe in the TOPAS-folder. If not explicitly passed, will try to use what is in topas.conf.''' - - required_options = ['topas_path', 'topas_log', 'topas_logfile', 'log', 'logfile', 'overwrite'] - - default_options = { - 'topas_path': None, - 'topas_log': False, - 'topas_logfile': f'{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_topas.out', - 'log': False, - 'logfile': f'{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_generate_big_inp.log', - 'overwrite': False - } - - options = aux.update_options(options=options, required_options=required_options, default_options=default_options) - - if not options['topas_path']: - import nafuma.xrd as xrd - - # Open topas.conf in the nafuma/xrd - with open(os.path.join(os.path.dirname(xrd.__file__), 'topas.conf'), 'r') as f: - topas_base = f.read() - - options['topas_path'] = os.path.join(topas_base, 'tc.exe') - - - # Check to see if the executable exists - if not os.path.exists(options['topas_path']): - raise Exception('TOPAS executable not found! Please explicitly pass path to tc.exe directly in options["topas_path"] or change base folder in topas.conf to the correct one.') - - - # Create folders if they don't exist - - # FIXME Since the big INP files now have the same filename for all iterations, we need to adjust the code to only get unique values from the get_paths function - # FIXME get_headers() is also not working now. Needs to be adjusted to the new way of writing the Out-parameters - paths = get_paths(data['inp']) - paths = aux.get_unique(paths) - - - - for path in paths: - headers = get_headers(data['inp'], path) - - dirname = os.path.dirname(path) - - if dirname and not os.path.isdir(dirname): - os.makedirs(dirname) - - if not os.path.exists(path) or options['overwrite']: - with open(path, 'w') as results: - for header in headers: - results.write(header+'\t') - - results.write('\n') - else: - raise Exception(f'Results file already exists: {path}') - - - # Create shell command - command = ' '.join([options['topas_path'], data['inp']]) - - # Append output if logging is enabled - if options['topas_log']: - command = ' '.join([command, f'>{options["topas_logfile"]}']) - - if os.path.dirname(options['topas_logfile']) and not os.path.isdir(os.path.dirname(options['topas_logfile'])): - os.makedirs(os.path.dirname(options['topas_logfile'])) - - - subprocess.call(command, shell=True) - - - - - -def read_results(path, options={}): - - - default_options = { - 'errors': True - } - - options = aux.update_options(options=options, default_options=default_options) - - results = pd.read_csv(path, delim_whitespace=True, index_col=0, header=None) - - if options['errors']: - atoms = int((results.shape[1] - 24) / 10) - - headers = [ - 'r_wp', 'r_exp', 'r_p', 'r_p_dash', 'r_exp_dash', 'gof', - 'vol', 'vol_err', 'mass', 'mass_err', 'wp', 'wp_err', - 'a', 'a_err', 'b', 'b_err', 'c', 'c_err', 'alpha', 'alpha_err', 'beta', 'beta_err', 'gamma', 'gamma_err', - ] - - else: - atoms = int((results.shape[1] - 15) / 5) - - headers = [ - 'r_wp', 'r_exp', 'r_p', 'r_p_dash', 'r_exp_dash', 'gof', - 'vol', 'mass', 'wp', - 'a', 'b', 'c', 'alpha', 'beta', 'gamma', - ] - - - labels = ['x', 'y', 'z', 'occ', 'beq'] - for i in range(atoms): - for label in labels: - headers.append(f'atom_{i+1}_{label}') - - if options['errors']: - headers.append(f'atom_{i+1}_{label}_err') - - - results.columns = headers - - return results - - - - -def fix_magnetic_cif(path): - with open(path, 'r') as f: - lines = f.readlines() - - for i, line in enumerate(lines): - if '_magnetic_space_group_symop_operation_timereversal' in line: # last line before symmetry operations - j = 1 - line = lines[i + j] - while len(line) > 1: - lines[i+j] = f'{j} ' + line - j += 1 - line = lines[i+j] - - - - with open(path, 'w') as f: - for line in lines: - f.write(line) - - - -def get_magnetic_moment(path): - with open(path, 'r') as f: - lines = f.readlines() - - for i, line in enumerate(lines): - if '_magnetic_atom_site_moment_crystalaxis_mz' in line: # last line before magnetic moments - j = 1 - line = lines[i+j] - - magnetic_moments = {} - while line: - magnetic_moments[line.split()[0]] = (float(line.split()[1]), float(line.split()[2]), float(line.split()[3])) - - j += 1 - - if i+j < len(lines): - line = lines[i+j] - else: - break - - - - return magnetic_moments - - -def get_magnetic_moment_magnitude(magmom): - magnitudes = {} - for site in magmom.keys(): - magnitudes[site] = np.sqrt(magmom[site][0]**2+magmom[site][1]**2+magmom[site][2]**2) - - return magnitudes - - -def strip_labels_from_inp(path, save_path=None): - - if not save_path: - save_path = path - - with open(path, 'r') as fin: - lines = fin.readlines() - - for i, line in enumerate(lines): - lines[i] = line.replace('_XXXX', '') - - with open(save_path, 'w') as fout: - for line in lines: - fout.write(line) diff --git a/nafuma/xrd/snippets.json b/nafuma/xrd/snippets.json deleted file mode 100644 index 60f169d..0000000 --- a/nafuma/xrd/snippets.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "calculation_step": "x_calculation_step = Yobs_dx_at(Xo); convolution_step {}", - "capillary": [ - "local !packing_density {} min 0.1 max 1.0 'typically 0.2 to 0.5", - "local !capdia {} 'capillary diameter in mm", - "local !linab = Get(mixture_MAC) Get(mixture_density_g_on_cm3);: 100 'in cm-1", - "local muR = (capdia/20)*linab*packing_density;", - "Cylindrical_I_Correction(muR)" - ], - "gauss_fwhm": "gauss_fwhm = Sqrt({} Cos(2 * Th)^4 + {} Cos(2 * Th)^2 + {});", - "lp_factor": "LP_Factor({}) 'change the LP correction or lh value if required", - "synchrotron": "lam ymin_on_ymax 0.0001 la 1.0 lo {} lh 0.1", - "neutron": [ - "lam ymin_on_ymax 0.0001 la 1.0 lo {} lh 0.5", - "neutron_data" - ], - "MoKa":[ - "lam ymin_on_ymax 0.0001", - "la 0.6533 lo 0.7093 lh 0.2695", - "la 0.3467 lo 0.713574 lh 0.2795" - ], - "RECX2": [ - "Rp 280", - "Rs 280" - ], - "zero_error": "Zero_Error(!zero, 0)", - "th2_offset": [ - "prm !zero\t\t\t= 0 ;: 0 \t\t\t\tmin = Max(Val - 20 Yobs_dx_at(X1), -100 Yobs_dx_at(X1)); max = Min(Val + 20 Yobs_dx_at(X2), 100 Yobs_dx_at(X2)); del = .01 Yobs_dx_at(X1); val_on_continue 0", - "prm !cos_shift\t\t= 0 ;: 0 \t\t\t\tmin = Val-.8; max = Val+.8; del 0.001", - "prm !sin_shift\t\t= 0 ;: 0 \t\t\t\tmin = Val-.8; max = Val+.8; del 0.001", - "th2_offset = (zero) + (cos_shift) Cos(Th) + (sin_shift) Sin(Th) ;" - ], - "fit_peak_width": "DC1( ad, 0, bd, 0, cd, 0)", - "TCHZ_Peak_Type": "TCHZ_Peak_Type(pku_1, 0, pkv_1, 0,pkw_1, 0, !pkx_1, 0.0000,pky_1, 0,!pkz_1, 0.0000)", - "Simple_Axial_Model": "Simple_Axial_Model( axial_1, 0)", - "magnetic_moment_str": "mlx = ml_x_{}_{}_XXXX ; \t mly = ml_y_{}_{}_XXXX ; \t mlz = ml_z_{}_{}_XXXX ; \t MM_CrystalAxis_Display( 0, 0, 0)", - "peak": [ - "xo_Is", - "xo @ {}", - "peak_type fp", - "LVol_FWHM_CS_G_L( 1, 0, 0.89, 0,,,@, 2)", - "I @ 35.35632`" - ] -} \ No newline at end of file diff --git a/nafuma/xrd/topas.conf b/nafuma/xrd/topas.conf deleted file mode 100644 index 3c6612a..0000000 --- a/nafuma/xrd/topas.conf +++ /dev/null @@ -1 +0,0 @@ -C:/TOPAS6/ \ No newline at end of file diff --git a/nafuma/xrd/topas_default.json b/nafuma/xrd/topas_default.json deleted file mode 100644 index c92e8a4..0000000 --- a/nafuma/xrd/topas_default.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "A_matrix_memory_allowed_in_Mbytes": null, - "approximate_A": false, - "bootstrap_errors": null, - "capdia": 0.5, - "chi2_convergence_criteria": 0.001, - "conserve_memory": false, - "continue_after_convergence": false, - "convolution_step": 1, - "do_errors": false, - "iters": 100000, - "lp_factor": 90, - "num_runs": null, - "packing_density": 0.5 -} \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 82b364d..0000000 --- a/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -from setuptools import setup, find_packages - -setup(name='nafuma', - version='0.4', - description='Analysis tools for inorganic materials chemistry at the NAFUMA-group at the University of Oslo', - url='https://github.com/rasmusthog/nafuma', - author='Rasmus Vester Thøgersen, Halvor Høen Hval', - author_email='code@rasmusthog.me', - license='MIT', - packages=find_packages(), - zip_safe=False) diff --git a/test.txt b/test.txt deleted file mode 100644 index e69de29..0000000