diff --git a/.gitignore b/.gitignore index 282803896..a764d756b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,44 @@ src/ess/_version.py .mypy_cache .virtual_documents __pycache__ + + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos + + diff --git a/docs/instruments/loki/nurf/fluo_demonstration.ipynb b/docs/instruments/loki/nurf/fluo_demonstration.ipynb new file mode 100644 index 000000000..584d6e0ab --- /dev/null +++ b/docs/instruments/loki/nurf/fluo_demonstration.ipynb @@ -0,0 +1,447 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ess.loki.nurf import utils, fluo, plot\n", + "from ess.loki.nurf import ill_auxilliary_funcs as ill \n", + "\n", + "# standard library imports\n", + "import itertools\n", + "import os\n", + "from typing import Optional, Type, Union\n", + "\n", + "# related third party imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "import matplotlib as mpl\n", + "import matplotlib.gridspec as gridspec\n", + "from IPython.display import display, HTML\n", + "from scipy.optimize import leastsq # needed for fitting of turbidity\n", + "\n", + "# local application imports\n", + "import scippnexus as snx\n", + "import scipp as sc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare for export to .dat for uv and fluo\n", + "\n", + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "# change to folder\n", + "os.chdir(process_folder)\n", + "\n", + "# export path for .dat files\n", + "path_output='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version/dat-files'\n", + "graphpath_output='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version/graphs'\n", + "\n", + "\n", + "# experimental data sets\n", + "exp5= [66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "\n", + "exp2= [65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "exp7= [66083, 66086, 66089, 66092, 66095, 66098, 66101, 66104, 66107, 66110, 66113]\n", + "exp8= [66116, 66119, 66122, 66125, 66128, 66131, 66134, 66137, 66140, 66143, 66146]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exp_meth='fluorescence'\n", + "name='066029.nxs'\n", + "\n", + "# load and calculate fluo spectra in one command\n", + "fluo_da=fluo.load_and_normalize_fluo(name)\n", + "\n", + "display(fluo_da)\n", + "\n", + "sc.show(fluo_da)\n", + "\n", + "svg_fluo_file=sc.make_svg(fluo_da)\n", + "\n", + "with open('/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/Post-doc_Studium_MAXIV/MIH_Zahnprojekt/UU_Institutsseminar/XFEL_Vortrag/fluo_da_exp.svg', 'w') as f:\n", + " f.write(svg_fluo_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exp_meth='fluorescence'\n", + "name='066029.nxs'\n", + "\n", + "# set legend props\n", + "legend_props = {\"show\": True, \"loc\": (1.04, 0)}\n", + "\n", + "# load and calculate fluo spectra in one command\n", + "fluo_da=fluo.load_and_normalize_fluo(name)\n", + "\n", + "#quick plot\n", + "!pwd\n", + "graph_name=f\"Fluo_spectra_{name}.pdf\"\n", + "graph_out=os.path.join(graphpath_output, graph_name)\n", + "\n", + "# prepare a good graph for a poster\n", + "cm = 1/2.54 # centimeters in inches\n", + "#figsize_b=6\n", + "#figsize_a=1.333*figsize_b\n", + "figsize_a=20\n", + "figsize_b=11.25\n", + "dpi=1200\n", + "\n", + "fig, ax1 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) )\n", + "\n", + "# load and calculate fluo spectra in two lines\n", + "fluo_dict=utils.load_nurfloki_file(name,'fluorescence') \n", + "final_fluo = fluo.normalize_fluo(**fluo_dict) \n", + "\n", + "\n", + "only_good_fspectra = {} # make empty dict\n", + "for i in range(1, fluo_dict[\"sample\"].sizes[\"spectrum\"], 2):\n", + " mwl = str(final_fluo.coords[\"monowavelengths\"][i].value) + \"nm\"\n", + " only_good_fspectra[f\"spectrum-{i}, {mwl}\"] = final_fluo[\"spectrum\", i]\n", + "\n", + "out4 = sc.plot(\n", + " only_good_fspectra,\n", + " linestyle=\"dashed\",\n", + " grid=True,\n", + " legend=legend_props,\n", + " marker='.',\n", + " title=f\"Fluo - {name}\",\n", + " ylabel='Intensity [counts]',\n", + " color=plot.line_colors(len(only_good_fspectra)),\n", + " figsize=(figsize_a*cm, figsize_b*cm),\n", + " ax=ax1\n", + ")\n", + "out4.ax.set_xlim([np.min(fluo_da.coords[\"wavelength\"].values)*0.98, np.max(fluo_da.coords[\"wavelength\"].values)*1.01])\n", + "out4.ax.set_ylim([np.nanmin(fluo_da.data.values), np.nanmax(fluo_da.data.values)])\n", + "\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(graph_out, dpi=dpi,bbox_inches='tight')\n", + "\n", + "######################################################\n", + "\n", + "graph_name=f\"Fluo_spectra_{name}_median.pdf\"\n", + "graph_out_filt=os.path.join(graphpath_output, graph_name)\n", + "\n", + "#apply median filter\n", + "kernel_size=15\n", + "final_fluo_filt=utils.nurf_median_filter(final_fluo, kernel_size=kernel_size)\n", + "\n", + "cm = 1/2.54 # centimeters in inches\n", + "#figsize_b=6\n", + "#figsize_a=1.333*figsize_b\n", + "figsize_a=20\n", + "figsize_b=11.25\n", + "dpi=1200\n", + "\n", + "fig2, ax2 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) )\n", + "\n", + "\n", + "only_good_fspectra_filt= {} # make empty dict\n", + "for i in range(1, fluo_dict[\"sample\"].sizes[\"spectrum\"], 2):\n", + " mwl = str(final_fluo.coords[\"monowavelengths\"][i].value) + \"nm\"\n", + " only_good_fspectra_filt[f\"spectrum-{i}, {mwl}\"] = final_fluo_filt[\"spectrum\", i]\n", + "\n", + "\n", + "\n", + "out5 = sc.plot(\n", + " only_good_fspectra_filt,\n", + " linestyle=\"dashed\",\n", + " grid=True,\n", + " marker='.',\n", + " legend=legend_props,\n", + " title=f\"Fluo - {name} - median filter size {kernel_size}\",\n", + " ylabel='Intensity [counts]',\n", + " color=plot.line_colors(len(only_good_fspectra_filt)),\n", + " figsize=(figsize_a*cm, figsize_b*cm),\n", + " ax=ax2\n", + ")\n", + "out5.ax.set_xlim([np.min(fluo_da.coords[\"wavelength\"].values)*0.98, np.max(fluo_da.coords[\"wavelength\"].values)*1.01])\n", + "out5.ax.set_ylim([np.nanmin(fluo_da.data.values), np.nanmax(fluo_da.data.values)])\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(graph_out_filt, dpi=dpi,bbox_inches='tight')\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# load and calculate fluo spectra in two lines\n", + "fluo_dict=utils.load_nurfloki_file(name,'fluorescence') \n", + "final_fluo = fluo.normalize_fluo(**fluo_dict) \n", + "\n", + "\n", + "#apply median filter\n", + "kernel_size=15\n", + "final_fluo_filt=utils.nurf_median_filter(final_fluo, kernel_size=kernel_size)\n", + "\n", + "fig3, ax3 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) )\n", + "\n", + "\n", + "only_good_fspectra_filt= {} # make empty dict\n", + "for i in range(1, fluo_dict[\"sample\"].sizes[\"spectrum\"], 2):\n", + " mwl = str(final_fluo.coords[\"monowavelengths\"][i].value) + \"nm\"\n", + " only_good_fspectra_filt[f\"spectrum-{i}, {mwl}\"] = final_fluo_filt[\"spectrum\", i]\n", + "\n", + "\n", + "\n", + "out6 = sc.plot(\n", + " only_good_fspectra_filt,\n", + " linestyle=\"dashed\",\n", + " grid=True,\n", + " marker='.',\n", + " legend=legend_props,\n", + " title=f\"Fluo - {name} - median filter size {kernel_size}\",\n", + " ylabel='Intensity [counts]',\n", + " color=plot.line_colors(len(only_good_fspectra_filt)),\n", + " figsize=(figsize_a*cm, figsize_b*cm),\n", + " ax=ax3\n", + ")\n", + "out6.ax.set_xlim([np.min(fluo_da.coords[\"wavelength\"].values)*0.98, np.max(fluo_da.coords[\"wavelength\"].values)*1.01])\n", + "out6.ax.set_ylim([np.nanmin(fluo_da.data.values), np.nanmax(fluo_da.data.values)])\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell below shows how to load fluo spectra from a Loki.nxs file" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# filename\n", + "name='066017.nxs'\n", + "# method\n", + "exp_meth='fluorescence'\n", + "\n", + "# some plotting options\n", + "legend_props = {\"show\": True, \"loc\": (1.04, 0)}\n", + "figure_size = (8, 4)\n", + "\n", + "# load a file and caluclate fluo spectra within a LoKI.nxs file\n", + "fluo_dict=utils.load_nurfloki_file(name,exp_meth)\n", + "fluo_da=fluo.normalize_fluo(**fluo_dict)\n", + "\n", + "print(\"This is the resulting dataarray\")\n", + "display(fluo_da)\n", + "\n", + "#if you want to view a quick plot\n", + "display(sc.plot(sc.collapse(fluo_da, keep=\"wavelength\"), legend=legend_props, linestyle=\"dashed\", color=plot.line_colors((fluo_da.sizes['spectrum'])),\n", + " grid=True,figsize=figure_size, title=f\"{name}\"))\n", + "\n", + "\n", + "# or in one line, preserves source attribute\n", + "fluo_da2=fluo.load_and_normalize_fluo(name)\n", + "\n", + "display(fluo_da2)\n", + "\n", + "#plot result\n", + "display(sc.plot(sc.collapse(fluo_da2, keep=\"wavelength\"), legend=legend_props, linestyle=\"dashed\", color=plot.line_colors((fluo_da2.sizes['spectrum'])),\n", + " grid=True,figsize=figure_size, title=f\"{name}\"))\n", + "\n", + "\n", + "# apply a median filter\n", + "# prepare kernel\n", + "kernel_size=15\n", + "\n", + "# apply median filter\n", + "fluo_da2_filt=utils.nurf_median_filter(fluo_da2, kernel_size=kernel_size)\n", + "\n", + "# plot result with median filter\n", + "display(sc.plot(sc.collapse(fluo_da2_filt, keep=\"wavelength\"), legend=legend_props, linestyle=\"dashed\", color=plot.line_colors((fluo_da2.sizes['spectrum'])),\n", + " grid=True,figsize=figure_size, title=f\"{name} - median filter size {kernel_size}\"))\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The cell below shows how to plot the fluo content of a Loki file. Currently, the code differentiates between good and bad spectra because during the measurements at ILL technical problems occured." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#How to plot fluo the fluo spectra contained in one LoKI.nxs file\n", + "plot.plot_fluo(name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#How to load and normalize fluo spectra of one file in one go.\n", + "fluo_da=fluo.load_and_normalize_fluo(name) \n", + "display(fluo_da)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How to apply a median filter. Let's use the new median_filter offered by scipp." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#How to load and normalize fluo spectra of one file in one go.\n", + "fluo_da=fluo.load_and_normalize_fluo(name) \n", + "\n", + "# prepare kernel\n", + "kernel_size=15\n", + "\n", + "# apply median filter\n", + "fluo_da_filt=utils.nurf_median_filter(fluo_da, kernel_size=kernel_size)\n", + "\n", + "#show effect of median_filter\n", + "legend_props = {\"show\": True, \"loc\": (1.04, 0)}\n", + "fig1=sc.plot(sc.collapse(fluo_da, keep='wavelength'), marker='.', title='before any median filter',legend=legend_props,grid=True)\n", + "fig2=sc.plot(sc.collapse(fluo_da_filt, keep='wavelength'), marker='.', title=f'after median filter, size={kernel_size} ',legend=legend_props, grid=True)\n", + "display(fig1,fig2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell belows how to extract from one fluo measurement the maximum intensity and the corresponding wavelength" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "wllim=sc.scalar(300, unit='nm')\n", + "wulim=sc.scalar(400, unit='nm')\n", + "\n", + "name='066017.nxs'\n", + "\n", + "fluo_da=fluo.load_and_normalize_fluo(name)\n", + "fluo_filt_max = fluo.fluo_peak_int(fluo_da,wllim=wllim,wulim=wulim,medfilter=True,kernel_size=15)\n", + "\n", + "print('This is the fluo_filt_max scipp array')\n", + "display(fluo_filt_max)\n", + "\n", + "plot.plot_fluo_peak_int(fluo_da, wllim=wllim,wulim=wulim,medfilter=True,kernel_size=15)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell belows shows how to extract max intensity and corresponding wavelength from many files. Results are plotted in two graphs for all input files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# experimental data sets\n", + "exp5= [66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "#exp5= [66017, 66046]\n", + "\n", + "filelist=ill.complete_fname(exp5)\n", + "\n", + "# We need a wavelength interval\n", + "wllim=sc.scalar(300, unit='nm')\n", + "wulim=sc.scalar(400, unit='nm')\n", + "\n", + "# Calculate and plot max intensity for each spectrum in each file and plot corresponding wavelength\n", + "fig=plot.plot_fluo_multiple_peak_int(filelist, wllim=wllim, wulim=wulim, medfilter=True, kernel_size=15)\n", + "\n", + "\n", + "#How to export the graphs\n", + "graph_name=f\"Fluo_peak_int_exp5.pdf\"\n", + "graph_out=os.path.join(graphpath_output, graph_name)\n", + "plt.savefig(graph_out,dpi=1200,bbox_inches='tight')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.13 ('dev')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4396f389b93e7269692bd3bea4c62813bbe379469bde939b058805f538feec11" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/instruments/loki/nurf/sans_demonstration.ipynb b/docs/instruments/loki/nurf/sans_demonstration.ipynb new file mode 100644 index 000000000..77043f216 --- /dev/null +++ b/docs/instruments/loki/nurf/sans_demonstration.ipynb @@ -0,0 +1,167 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ess.loki.nurf import ill_auxilliary_funcs as ill \n", + "\n", + "# standard library imports\n", + "import itertools\n", + "import os\n", + "from typing import Optional, Type, Union\n", + "\n", + "\n", + "# related third party imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "import matplotlib as mpl\n", + "import matplotlib.gridspec as gridspec\n", + "from IPython.display import display, HTML\n", + "\n", + "import pandas as pd\n", + "\n", + "\n", + "# local application imports\n", + "import scipp as sc" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#we have to simply life a bit. I copied all bg_substracted.txt into one folder\n", + "top_path='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/SANSData&Analysis/all_bg_subtracted/'\n", + "os.chdir(top_path)\n", + "#!ls\n", + "\n", + "def complete_SANS_fname(scan_numbers, ext='_bg_subtracted.txt'):\n", + " if isinstance(scan_numbers, int):\n", + " flist_num = f\"{str(scan_numbers).zfill(6)}{ext}\"\n", + "\n", + " if isinstance(scan_numbers, list):\n", + " # convert a list of input numbers to real filename\n", + " flist_num = [str(i).zfill(6) + ext for i in scan_numbers]\n", + "\n", + " return flist_num\n", + "\n", + "\n", + "def load_SANStxt(name, path):\n", + "\n", + " if (isinstance(name, list) and len(name)==1):\n", + " name=(lambda x: x)(*name)\n", + " elif isinstance(name, str):\n", + " pass\n", + "\n", + " else:\n", + " raise ValueError('Only one filename possible!')\n", + "\n", + " abs_file_path= os.path.join(path, name)\n", + "\n", + " # get the Pandas data frame\n", + " col_names=[\"c0\", \"c1\", \"c2\", \"c3\"]\n", + " df = pd.read_csv(abs_file_path, header = None, delim_whitespace=True, names=col_names) \n", + "\n", + " print(df)\n", + "\n", + "\n", + " # convert it to numpy array\n", + " #data=df.to_numpy()\n", + " data=sc.compat.from_pandas(df[\"c0\"])\n", + "\n", + " return data\n", + " #return df\n", + "\n", + "def load_multiple_SANStxt(filelist, path):\n", + "\n", + " data_dict={}\n", + "\n", + " if isinstance(filelist, list):\n", + " for fname in filelist:\n", + "\n", + " data=load_SANStxt(fname, path)\n", + "\n", + " data_dict[fname]=data\n", + "\n", + " return data_dict\n", + "\n", + "\n", + "exp=[ 65887]#, 65985]\n", + "\n", + "filelist_fullname=complete_SANS_fname(exp)\n", + "\n", + "df=load_SANStxt(filelist_fullname, top_path)\n", + "display(df)\n", + "\n", + "\n", + "\n", + "#file_dict=load_multiple_SANStxt(filelist_fullname, top_path)\n", + "#test=sc.from_dict(file_dict)\n", + "#print(test)\n", + "\n", + "#def plot_SANSfiles(file_dict):\n", + "\n", + "\n", + "\n", + " \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "abs_file_path=os.path.join(top_path, '065985_bg_subtracted.txt')\n", + "print(abs_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.13 ('dev')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4396f389b93e7269692bd3bea4c62813bbe379469bde939b058805f538feec11" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/instruments/loki/nurf/uv_demonstration.ipynb b/docs/instruments/loki/nurf/uv_demonstration.ipynb new file mode 100644 index 000000000..3e0c4d002 --- /dev/null +++ b/docs/instruments/loki/nurf/uv_demonstration.ipynb @@ -0,0 +1,477 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Demonstration of functions operating on UV spectra for NurF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ess.loki.nurf import utils, uv, plot\n", + "from ess.loki.nurf import ill_auxilliary_funcs as ill \n", + "\n", + "# standard library imports\n", + "import itertools\n", + "import os\n", + "from typing import Optional, Type, Union\n", + "\n", + "# related third party imports\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "import matplotlib.gridspec as gridspec\n", + "from IPython.display import display, HTML\n", + "from scipy.optimize import leastsq # needed for fitting of turbidity\n", + "\n", + "# local application imports\n", + "import scippnexus as snx\n", + "import scipp as sc\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare for export to .dat for uv and fluo\n", + "\n", + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "# change to folder\n", + "os.chdir(process_folder)\n", + "\n", + "# export path for .dat files\n", + "path_output='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version/dat-files'\n", + "graphpath_output='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version/graphs'\n", + "\n", + "# experimental data sets\n", + "exp5= [66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "\n", + "exp2= [65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "exp7= [66083, 66086, 66089, 66092, 66095, 66098, 66101, 66104, 66107, 66110, 66113]\n", + "exp8= [66116, 66119, 66122, 66125, 66128, 66131, 66134, 66137, 66140, 66143, 66146]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filesetlist=ill.complete_fname(exp5)\n", + "fig=plot.plot_multiple_uv_peak_int(filesetlist, wavelength= sc.scalar(280, unit='nm'))\n", + "\n", + "graph_name=f\"UV_peak_exp5.pdf\"\n", + "graph_out=os.path.join(graphpath_output, graph_name)\n", + "plt.savefig(graph_out,dpi=1200,bbox_inches='tight')\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#params = {\n", + "# 'font.size': 40,\n", + "#}\n", + "#plt.rcParams.update(params)\n", + "\n", + "ax0=fig.get_axes()[0]\n", + "extent = ax0.get_window_extent().transformed(fig.dpi_scale_trans.inverted())\n", + "graph_name=f\"UV_peak_exp5_subplot_0.pdf\"\n", + "graph_out_subplot=os.path.join(graphpath_output, graph_name)\n", + "\n", + "fig.savefig(graph_out_subplot, bbox_inches=extent.expanded(1.2, 1.38), dpi=1200)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ess.loki.nurf import ill_auxilliary_funcs\n", + "\n", + "name='066017.nxs'\n", + "name='066029.nxs'\n", + "exp_meth='uv'\n", + "\n", + "# load a file and caluclate corrected uv spectra\n", + "uv_dict=utils.load_nurfloki_file(name,exp_meth)\n", + "uv_da=uv.normalize_uv(**uv_dict)\n", + "\n", + "display(uv_da)\n", + "\n", + "#quick plot\n", + "!pwd\n", + "\n", + "graph_name=f\"UV_spectra_{name}.pdf\"\n", + "graph_out=os.path.join(graphpath_output, graph_name)\n", + "cm = 1/2.54 # centimeters in inches\n", + "figsize_b=8\n", + "figsize_a=1.333*figsize_b\n", + "dpi=300\n", + "\n", + "\n", + "fig, ax1 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) )\n", + "out1=sc.plot(sc.collapse(uv_da, keep=\"wavelength\"),\n", + " linestyle=\"dashed\",\n", + " grid=True,\n", + " marker='.',\n", + " title=f\"UV - {name}\",\n", + " ylabel='Abs [dimensionless]',\n", + " #filename=graph_out,\n", + " ax=ax1) \n", + "#out1.ax.set_xlim(300,400)\n", + "\n", + "#out1.ax.set_ylim([np.nanmin(uv_da.data.values), np.nanmax(np.nanmin(uv_da.data.values))])\n", + "out1.ax.set_xlim([np.min(uv_da.coords[\"wavelength\"].values)*0.98, np.max(uv_da.coords[\"wavelength\"].values)])\n", + "out1.ax.set_ylim([np.nanmin(uv_da.data.values)*0.9, np.nanmax(uv_da.data.values)*1.1])\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(graph_out, dpi=dpi,bbox_inches='tight')\n", + "\n", + "\n", + "sc.show(uv_da)\n", + "\n", + "\n", + "#better plot\n", + "plot.plot_uv(name)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cell belows shows how to plot all UV spectra in a single series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for i in exp2:\n", + " name=ill_auxilliary_funcs.complete_fname(i)\n", + " plot.plot_uv(name)\n", + "\n", + "for m in exp3:\n", + " name=ill_auxilliary_funcs.complete_fname(m)\n", + " plot.plot_uv(name)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# extract value for given wavelength\n", + "wavelength=sc.scalar(280,unit='nm')\n", + "tol= sc.scalar(0.5,unit='nm')\n", + "res=uv.uv_peak_int(uv_da, wavelength, tol=None)\n", + "#returns a dict, #TODO: do we want a Dataset here? \n", + "print(res)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How to average mutliple UV spectra in one Loki.nxs file?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "name='066017.nxs'\n", + "#How to average multiple UV spectra in one LoKI.nxs file?\n", + "\n", + "out1=uv.load_and_normalize_uv(name)\n", + "display(out1)\n", + "\n", + "# Apply uv.average_uv function to Loki.nxs\n", + "out2=uv.average_uv(name)\n", + "display(out2)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell below shows how to perform a turbidity correction on data from a single input file. If the user wishes to apply a median_filter beforehand, the user can apply this manually by adding one line of code, see next cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "name='066017.nxs'\n", + "name='066029.nxs'\n", + "\n", + "#How to perform a turbiity correction on one input file?\n", + "#uv_dict=utils.load_nurfloki_file(name,exp_meth)\n", + "#uv_da=uv.normalize_uv(**uv_dict)\n", + "\n", + "# Or load like this:\n", + "uv_da=uv.load_and_normalize_uv(name)\n", + "\n", + "uv_turb_corr=uv.uv_turbidity_fit(uv_da, fit_llim=None, fit_ulim=None, b_llim=None, b_ulim=None, m=None)\n", + "display(uv_turb_corr)\n", + "\n", + "\n", + "print(\"Turbidity\")\n", + "\n", + "graph_name_turbcorr=f\"UV_spectra_{name}_turbcorr.pdf\"\n", + "graph_out_turbcorr=os.path.join(graphpath_output, graph_name_turbcorr)\n", + "cm = 1/2.54 # centimeters in inches\n", + "figsize_b=8\n", + "figsize_a=1.333*figsize_b\n", + "dpi=1200\n", + "\n", + "\n", + "fig3, ax3 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) )\n", + "\n", + "turb_corr=sc.plot(sc.collapse(uv_turb_corr,keep=\"wavelength\"),\n", + " linestyle=\"dashed\",\n", + " grid=True,\n", + " marker='.',\n", + " title=f\"UV - {name} - turbidity corrected\",\n", + " ylabel='Abs [dimensionless]',\n", + " ax=ax3\n", + "\n", + ")\n", + "\n", + "turb_corr.ax.set_xlim([np.min(uv_turb_corr.coords[\"wavelength\"].values)*0.98, np.max(uv_turb_corr.coords[\"wavelength\"].values)])\n", + "turb_corr.ax.set_ylim([np.nanmin(uv_turb_corr.data.values)*0.9, np.nanmax(uv_turb_corr.data.values)*1.1])\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(graph_out_turbcorr, dpi=dpi,bbox_inches='tight')\n", + "plt.show()\n", + "\n", + "\n", + "\n", + "#How to plot the turbidity correction for visual inspection? \n", + "#plot.plot_uv_turbidity_fit(uv_da, fit_llim=None, fit_ulim=None, b_llim=None, b_ulim=None, m=None)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell below shows how to perform a turbidity correction on data from a single input file. Here is the example where a median filter is applied before hand." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "name='066017.nxs'\n", + "\n", + "# Or load like this:\n", + "uv_da=uv.load_and_normalize_uv(name)\n", + "\n", + "# How to apply a median filter?\n", + "kernel_size=10\n", + "#uv_da=utils.nurf_median_filter(uv_da, kernel_size=kernel_size)\n", + "\n", + "#uv_turb_corr_mfilt=uv.uv_turbidity_fit(uv_da, fit_llim=None, fit_ulim=None, b_llim=None, b_ulim=None, m=None)\n", + "#display(uv_turb_corr_mfilt)\n", + "\n", + "# Median filter in one line with turbdity_fit\n", + "uv_turb_corr_mfilt=uv.uv_turbidity_fit(utils.nurf_median_filter(uv_da, kernel_size=kernel_size), fit_llim=None, fit_ulim=None, b_llim=None, b_ulim=None, m=None)\n", + "\n", + "\n", + "#How to plot the turbidity correction for visual inspection? \n", + "plot.plot_uv_turbidity_fit(uv_turb_corr_mfilt, fit_llim=None, fit_ulim=None, b_llim=None, b_ulim=None, m=None)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell below shows how to perform a turbidity fit on multiple input files **without** previous applying a median filter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "scanlist = [66017, 66020]\n", + "filelist=ill.complete_fname(scanlist)\n", + "\n", + "# apply turbidity correction to multiple files\n", + "# create first a dict of sc.DataArray, where each array corresponds to a Loki.file\n", + "dict_da={name:uv.load_and_normalize_uv(name) for name in [name for name in filelist]} \n", + "# alternative that works, but maybe not so nice :-)\n", + "#d={name:uv.load_and_normalize_uv(name) for name in [name for name in ill.complete_fname(scanlist)]} \n", + "\n", + "\n", + "# apply turbidity fit to all spectra inside each Loki.file and to multiple files at once\n", + "res_da=uv.uv_multi_turbidity_fit(dict_da)\n", + "\n", + "display(res_da)\n", + "\n", + "# plot turbidity corrected files\n", + "plot.plot_uv_multi_turbidity_fit(dict_da)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Cell below shows how to perform a turbidity fit on multiple input files **with** applying a median filter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scanlist = [66017, 66020]\n", + "filelist=ill.complete_fname(scanlist)\n", + "\n", + "kernel_size=15\n", + "\n", + "# apply turbidity correction to multiple files\n", + "# create first a dict of sc.DataArray, where each array corresponds to a Loki.file\n", + "# for each sc.DataArray the nurf_median_filter is applied with above kernel_size\n", + "dict_da={name:utils.nurf_median_filter(uv.load_and_normalize_uv(name), kernel_size=kernel_size) for name in [name for name in filelist]} \n", + "\n", + "\n", + "# apply turbidity fit to all spectra inside each Loki.file and to multiple files at once\n", + "res_da=uv.uv_multi_turbidity_fit(dict_da)\n", + "\n", + "display(res_da)\n", + "\n", + "# plot turbidity corrected files\n", + "plot.plot_uv_multi_turbidity_fit(dict_da)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "How to gather the spectra of multiple files in one sc.DataArray or in one sc.Dataset?\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#How to gather the spectra of multiple files in one sc.DataArray or in one sc.Dataset?\n", + "filesetlist=ill.complete_fname(exp5)\n", + "uv_da=uv.gather_uv_set(filesetlist)\n", + "display(uv_da)\n", + "\n", + "#if all files contain the same number of UV spectra, a sc.DataSet is returned \n", + "filesetlist_3files=ill.complete_fname([66017, 66020, 66023])\n", + "uv_ds=uv.gather_uv_set(filesetlist_3files)\n", + "display(uv_ds)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "name='066017.nxs'\n", + "da = uv.load_and_normalize_uv(name)\n", + "\n", + "#How to apply the scipp.ndimage.median_filter to an sc.DataArray? \n", + "#Current NUrF has non-equaled spaced data, only int is accepeted.\n", + "#Otherwise: kernel_size=sc.scalar(2.5, units='nm') as example should work.\n", + "kernel_size=15 \n", + "da_filt=utils.nurf_median_filter(da, kernel_size=kernel_size)\n", + "\n", + "#plot all contributions\n", + "legend_props = {\"show\": True, \"loc\": 1}\n", + "fig1=sc.plot(sc.collapse(da, keep='wavelength'),title='before any median filter',legend=legend_props, marker='.')\n", + "fig2=sc.plot(sc.collapse(da_filt, keep='wavelength'), marker='.', title=f'after median filter - scipp, size={kernel_size} ',legend=legend_props)\n", + "\n", + "display(fig1,fig2)\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filesetlist=ill.complete_fname(exp5)\n", + "plot.plot_multiple_uv_peak_int(filesetlist, wavelength= sc.scalar(280, unit='nm'))\n", + "graph_name=f\"UV_peak_exp5.pdf\"\n", + "graph_out=os.path.join(graphpath_output, graph_name)\n", + "plt.savefig(graph_out,dpi=1200,bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.9.13 ('dev')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4396f389b93e7269692bd3bea4c62813bbe379469bde939b058805f538feec11" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/ess/loki/nurf/Convert_ILL2LoKI.ipynb b/src/ess/loki/nurf/Convert_ILL2LoKI.ipynb new file mode 100644 index 000000000..fa1f6fdf6 --- /dev/null +++ b/src/ess/loki/nurf/Convert_ILL2LoKI.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import h5py\n", + "import os \n", + "import numpy as np\n", + "import copy\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import cm\n", + "\n", + "import os\n", + "import errno\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# This cell contains function to convert ILL raw data to the forseen Loki file format\n", + "\n", + "def loki_file_creator(loki_path, loki_filename):\n", + " \"\"\"\n", + " This function creates a dummy loki_basic.nxs that has the expected file structure for ESS Loki.\n", + " Afterwards, I will append the nurf data and rename the file.\n", + " \"\"\"\n", + " with h5py.File(os.path.join(loki_path,loki_filename), 'w') as hf:\n", + " nxentry = hf.create_group(\"entry\")\n", + " nxentry.attrs['NX_class'] = 'NXentry'\n", + " nxinstrument=nxentry.create_group(\"instrument\") \n", + " nxinstrument.attrs['NX_class'] = 'NXinstrument'\n", + "\n", + "\n", + "def load_one_spectro_file(file_handle, path_rawdata):\n", + " \"\"\"\n", + " This function loads one .nxs file containing spectroscopy data (fluo, uv). Data is stored in multiple np.ndarrays.\n", + "\n", + " In:\n", + " file_handle: file_handle is a file number, one element is expected otherwise an error is raised\n", + " type: list\n", + "\n", + " path_rawdata: Path to the raw data\n", + " type: str\n", + "\n", + " Out:\n", + " data: contains all relevant HDF5 entries and their content for the Nurf project (keys and values)\n", + " type: dict\n", + "\n", + " \"\"\"\n", + "\n", + " # create path to file, convert file_number to string\n", + " file_path_spectro = os.path.join(path_rawdata, file_handle + '.nxs')\n", + "\n", + " # check if file exists\n", + " if not os.path.isfile(file_path_spectro):\n", + " raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT),\n", + " file_path_spectro)\n", + "\n", + " # we are ready to load the data set\n", + " # open the .nxs file and read the values\n", + " with h5py.File(file_path_spectro, \"r\") as f:\n", + " # access nurf sub-group\n", + " nurf_group = '/entry0/D22/nurf/'\n", + "\n", + " # access keys in sub-group\n", + " nurf_keys = list(f[nurf_group].keys())\n", + " # how many keys exist\n", + " len_nurf_keys = len(nurf_keys)\n", + "\n", + " # print(nurf_keys)\n", + " # print(len_nurf_keys)\n", + "\n", + " # this returns a list with HDF5datasets (I think)\n", + " # data_spectro_file=list(f[nurf_group].values())\n", + "\n", + " # data_spectro_file=f[nurf_group].values()\n", + " # data_spectro_file=f[nurf_group]\n", + "\n", + " # extract all data of the nurf subgroup and store it in a new dict\n", + "\n", + " # initialise an empty dict\n", + " data = {}\n", + "\n", + " for key in f[nurf_group].keys():\n", + " # print(key)\n", + " # this is how I get string giving the full path to this dataset\n", + " path_dataset = f[nurf_group][key].name\n", + " # print(path_dataset)\n", + " # print(f[nurf_group][key].name) #this prints the full path to the dataset\n", + " # print(type(f[path_dataset][:])) #this gives me access to the content of the data set, I could use : or () inside [], out: np.ndarray\n", + "\n", + " # This gives a dict with full path name as dict entry followed by value. No, I don't want this, but good to know.\n", + " # data[f[nurf_group][key].name]=f[path_dataset][:]\n", + "\n", + " # This gives a dict where the keys corresponds to the key names of the h5 file.\n", + " data[key] = f[path_dataset][:]\n", + "\n", + " # print(f[nurf_group].get('Fluo_spectra'))\n", + "\n", + " # print a hierachical view of the file (simple)\n", + " # like this is would only go through subgroups\n", + " # f[nurf_group].visititems(lambda x, y: print(x))\n", + "\n", + " # walk through the whole file and show the attributes (found on github as an explanation for this function)\n", + " # def print_attrs(name, obj):\n", + " # print(name)\n", + " # for key, val in obj.attrs.items():\n", + " # print(\"{0}: {1}\".format(key, val))\n", + " # f[nurf_group].visititems(print_attrs)\n", + "\n", + " # print(data_spectro_file)\n", + "\n", + " # file_handle is returned as np.ndarray and its an np.array, elements correspond to row indices\n", + " return data\n", + "\n", + "\n", + "def nurf_file_creator(loki_file, path_to_loki_file, data):\n", + " \"\"\"\n", + " Appends NUrF group to LOKI NeXus file for ESS\n", + "\n", + " Args:\n", + " loki_file (str): filename of NeXus file for Loki\n", + " path_to_loki_file (str): File path where the NeXus file for LOKI is stored\n", + " data (dict): Dictionary with dummy data for Nurf\n", + " \"\"\"\n", + "\n", + " # change directory where the loki.nxs is located\n", + " os.chdir(path_to_loki_file)\n", + "\n", + " # open the file and append\n", + " with h5py.File(loki_file, 'a') as hf:\n", + " \n", + " #comment on names\n", + " #UV/FL_Background is the dark\n", + " #UV/FL_Intensity0 is the reference\n", + " #UV/FL_Spectra is the sample\n", + " \n", + " # image_key: number of frames (nFrames) given indirectly as part of the shape of the arrays \n", + " # TODO: keep in mind what happens if multiple dark or reference frames are taken\n", + " \n", + " # remove axis=2 of length one from this array\n", + " data['UV_spectra']=np.squeeze(data['UV_spectra'], axis=2) #this removes the third axis in this array, TODO: needs later to be verified with real data from hardware\n", + " \n", + " # assemble all spectra in one variable\n", + " uv_all_data=np.row_stack((data['UV_spectra'],data['UV_background'], data['UV_intensity0']))\n", + " \n", + " dummy_vec=np.full(np.shape(uv_all_data)[0], False) \n", + "\n", + " #create boolean masks for data, dark, reference\n", + " uv_nb_spectra=np.shape(data['UV_spectra'])[0]\n", + "\n", + " # data mask, copy and replace first entries \n", + " uv_data_mask=copy.copy(dummy_vec)\n", + " uv_data_mask[0:uv_nb_spectra]=True\n", + "\n", + " # dark mask\n", + " # find out how many darks exist\n", + " # TODO: Is there always a background or do we need to catch this case if there isn't?\n", + " if data['UV_background'].ndim==1:\n", + " uv_nb_darks=1\n", + " else: \n", + " uv_nb_darks=np.shape(data['UV_background'])[1] #TODO: needs to be verified with real data from Judith's setup\n", + "\n", + " uv_dark_mask=copy.copy(dummy_vec)\n", + " uv_dark_mask[uv_nb_spectra:uv_nb_spectra+uv_nb_darks]=True\n", + "\n", + " # reference \n", + " # how many references where taken? \n", + " if data['UV_intensity0'].ndim==1:\n", + " uv_nb_ref=1\n", + " else:\n", + " uv_nb_ref=np.shape(data['UV_intensity0'])[1] #TODO: needs to be verified with real data from Judith's setup\n", + " \n", + " # reference mask, copy and replace first entries \n", + " uv_ref_mask=copy.copy(dummy_vec)\n", + " uv_ref_mask[uv_nb_spectra+uv_nb_darks:uv_nb_spectra+uv_nb_darks+uv_nb_ref]=True\n", + "\n", + " \n", + " \n", + " # UV subgroup\n", + " grp_uv = hf.create_group(\"/entry/instrument/uv\")\n", + " grp_uv.attrs[\"NX_class\"] = 'NXdata'\n", + " \n", + " # uv spectra\n", + " uv_signal_data=grp_uv.create_dataset('data', data=uv_all_data, dtype=np.float32)\n", + " uv_signal_data.attrs['long name']= 'all_data'\n", + " uv_signal_data.attrs['units']= 'counts'\n", + " grp_uv.attrs['signal']= 'data' #indicate that the main signal is data \n", + " grp_uv.attrs['axes']= [ \"spectrum\", \"wavelength\" ] #time is here the first axis, i.e axis=0, wavelength is axis=1\n", + " \n", + " # define the AXISNAME_indices\n", + " grp_uv.attrs['time_indices'] = 0\n", + " grp_uv.attrs['integration_time_indices'] = 0\n", + " grp_uv.attrs['wavelength_indices'] = 1\n", + "\n", + " # introducing a key that is interpretable for sample, dark, and reference \n", + " grp_uv.attrs['is_sample_indices'] = 0\n", + " grp_uv.attrs['is_dark_indices'] = 0\n", + " grp_uv.attrs['is_reference_indices'] = 0 \n", + "\n", + " grp_uv.create_dataset('is_sample', data=uv_data_mask, dtype=bool)\n", + " grp_uv.create_dataset('is_dark', data=uv_dark_mask, dtype=bool)\n", + " grp_uv.create_dataset('is_reference', data=uv_ref_mask, dtype=bool)\n", + "\n", + " # uv_time\n", + " # dummy timestamps for uv_time\n", + " # TODO: Codes will have to change later for the real hardware.\n", + " uv_time = np.empty(np.shape(uv_all_data)[0], dtype='datetime64[us]') \n", + " for i in range(0, np.shape(uv_time)[0]):\n", + " uv_time[i]=np.datetime64('now')\n", + " \n", + " # see https://stackoverflow.com/questions/23570632/store-datetimes-in-hdf5-with-h5py \n", + " # suggested work around because h5py does not support time types\n", + " uv_time_data=grp_uv.create_dataset('time', data=uv_time.view(' 066150.nxs 066150\n", + "065925 \n", + "065927 \n", + "065930 \n", + "065933 \n", + "065936 \n", + "065939 \n", + "065942 \n", + "065945 \n", + "065948 \n", + "065951 \n", + "065954 \n", + "065957 \n", + "065962 \n", + "065965 \n", + "065968 \n", + "065971 \n", + "065974 \n", + "065977 \n", + "065980 \n", + "065983 \n", + "065986 \n", + "065989 \n", + "065992 \n" + ] + } + ], + "source": [ + "# Location processed files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "\n", + "# Location for LOKI_basic.nxs\n", + "path_to_loki_file=process_folder\n", + "\n", + "# create LOKI_basic.nxs\n", + "loki_file='LOKI_basic.nxs'\n", + "loki_file_creator(path_to_loki_file, loki_file)\n", + "\n", + "# Location raw data files\n", + "path_rawdata='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/rawdata'\n", + "\n", + "\n", + "os.chdir(path_rawdata)\n", + "flist=os.listdir(path_rawdata)\n", + "print(type(flist), flist[0], os.path.splitext(flist[0])[0])\n", + "# get only filenumbers\n", + "flist_num=[os.path.splitext(fnumber)[0] for fnumber in flist]\n", + "\n", + "# file numbers for ILL experiment 947\n", + "#scan numbers in exp 5 \n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "#scan numbers in exp 6\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "#scan_numbers=[exp5, exp6]\n", + "\n", + "# scan number in exp 7 and exp8\n", + "exp7= [66083, 66086, 66089, 66092, 66095, 66098, 66101, 66104, 66107, 66110, 66113]\n", + "exp8= [66116, 66119, 66122, 66125, 66128, 66131, 66134, 66137, 66140, 66143, 66146]\n", + "\n", + "#scan_numbers= [exp7, exp8]\n", + "\n", + "#for i in scan_numbers:\n", + "# convert_ill2loki(i, path_to_loki_file, loki_file, path_rawdata, process_folder)\n", + "\n", + "#exp2 \n", + "exp2=[65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "scan_numbers= [exp2, exp3]\n", + "\n", + "for i in scan_numbers:\n", + " convert_ill2loki(i, path_to_loki_file, loki_file, path_rawdata, process_folder)\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.12 ('scippneutron')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "410bfb39d85b3112e66f48ab3e53bb74ad7e7b3fb364f756ca860b5a3cf79ca2" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/ess/loki/nurf/__init__.py b/src/ess/loki/nurf/__init__.py new file mode 100755 index 000000000..9f9161bf6 --- /dev/null +++ b/src/ess/loki/nurf/__init__.py @@ -0,0 +1 @@ +from . import utils \ No newline at end of file diff --git a/src/ess/loki/nurf/deprecated.py b/src/ess/loki/nurf/deprecated.py new file mode 100644 index 000000000..f2528b642 --- /dev/null +++ b/src/ess/loki/nurf/deprecated.py @@ -0,0 +1,138 @@ +# This file contains functions that will be deprecated. +# NUrf scipp graveyard + +def load_uv(name): + """Loads the UV data from the corresponding entry in the LoKI.nxs filename. + + Parameters + ---------- + name: str + Filename, e.g. 066017.nxs + + Returns: + ---------- + uv_dict: dict + Dictionary that contains UV data signal (data) from the sample, the reference, + and the dark measurement. + Keys: sample, reference, dark + + """ + + # load the nexus and extract the uv entry + with snx.File(name) as f: + uv = f["entry/instrument/uv"][()] + + # separation + uv_dict = split_sample_dark_reference(uv) + + return uv_dict + + +def load_fluo(name): + """Loads the data contained in the fluo entry of a LoKI.nxs file + + Parameters + ---------- + name: str + Filename, e.g. 066017.nxs + + Returns + ---------- + fluo_dict: dict + Dictionary of sc.DataArrays. Keys: data, reference, dark. Data contains the fluo signals of the sample. + + """ + + with snx.File(name) as f: + fluo = f["entry/instrument/fluorescence"][()] + + # separation + fluo_dict = split_sample_dark_reference(fluo) + + return fluo_dict + + +# possibilites for median filters, I did not benchmark, apparently median_filter +# could be the faster and medfilt2d is faster than medfilt +#from scipy.ndimage import median_filter +#from scipy.signal import medfilt +# This function will be replaced by scipp owns medilter. +def apply_medfilter( + da: sc.DataArray, kernel_size: Optional[int] = None +) -> sc.DataArray: + #TODO: Rewrite this function with median filters offered by scipp. + """Applies a median filter to a sc.DataArray that contains fluo or uv spectra to + remove spikes + Filter used: from scipy.ndimage import median_filter. This filter function is maybe + subject to change for another scipy function. + Caution: The scipy mean (median?) filter will just ignore errorbars on counts + according to Neil Vaytet. + + Parameters + ---------- + da: sc.DataArray + DataArray that contains already fluo or uv spectra in data. + + kernel_size: int + Scalar giving the size of the median filter window. Elements of kernel_size should be odd. Default size is 3 + + Returns + ---------- + da_mfilt: sc.DataArray + New sc.DataArray with median filtered data + + """ + if kernel_size is None: + kernel_size = 3 + else: + assert type(kernel_size) is int, "kernel_size must be an integer" + + # check if kernel_size is odd + if (kernel_size % 2) != 1: + raise ValueError("kernel_size should be odd.") + + # extract all spectreal values + data_spectrum = da.values + + # apply a median filter + # yes, we can apply a median filter to an array (vectorize it), but the kernel needs to be adapted to the ndim off the array + # no filterin in the first dimension (spectrum), but in the second (wavelength) + # magic suggested by Simon + # this will make [kernel_size] for 1d data, and [1, kernel_size] for 2d data + ksize = np.ones_like(da.shape) + ksize[-1] = kernel_size + data_spectrum_filt = median_filter(data_spectrum, size=ksize) + + # create a new sc.DataArray where the data is replaced by the median filtered data + # make a deep copy + da_mfilt = da.copy() + + # replace original data with filtered data + da_mfilt.values = data_spectrum_filt + + # graphical comparison + legend_props = {"show": True, "loc": (1.04, 0)} + figs, axs = plt.subplots(1, 2, figsize=(16, 5)) + # out1=sc.plot(da['spectrum',7], ax=axs[0]) + # out2=sc.plot(da_mfilt['spectrum',7], ax=axs[1]) + out1 = sc.plot( + sc.collapse(da, keep="wavelength"), + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + title=f"Before median filter", + ax=axs[0], + ) + out2 = sc.plot( + sc.collapse(da_mfilt, keep="wavelength"), + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + title=f"After median filter - kernel_size: {kernel_size}", + ax=axs[1], + ) + # display(figs) + + return da_mfilt diff --git a/src/ess/loki/nurf/fluo.py b/src/ess/loki/nurf/fluo.py new file mode 100644 index 000000000..c4ba1beea --- /dev/null +++ b/src/ess/loki/nurf/fluo.py @@ -0,0 +1,269 @@ +# standard library imports +import itertools +import os +from typing import Optional, Type, Union + +# related third party imports +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import cm +import matplotlib.gridspec as gridspec +from IPython.display import display, HTML +from scipy.optimize import leastsq # needed for fitting of turbidity + +# local application imports +import scippnexus as snx +import scipp as sc +from ess.loki.nurf import utils +from scipp.scipy.ndimage import median_filter + + + +def normalize_fluo( + *, sample: sc.DataArray, reference: sc.DataArray, dark: sc.DataArray +) -> sc.DataArray: + """Calculates the corrected fluo signal for each given fluo spectrum in a given sc.DataArray + + Parameters + ---------- + sample: sc.DataArray + DataArray containing sample fluo signal, one spectrum or multiple spectra. + reference: sc.DataArray + DataArray containing reference fluo signal, one spectrum expected. + dark: sc.DataArray + DataArray containing dark fluo signal, one spectrum expected. + + Returns + ---------- + final_fluo: sc.DataArray + DataArray that contains the calculated fluo signal, one spectrum or mulitple spectra. + + """ + + # all spectra in this file are converted to final_fluo + # More explanation on the dark here. + # We keep dark here for consistency reasons. + # The dark measurement is necessary for this type of detector. Sometimes, + # one can use the fluorescence emission without the reference. In that case + # having the dark is important. + # In most cases the reference should have no fluorescence emission, + # basically flat. In more complex solvent the reference may have some + # intrinsic fluorescence that would need to be substracted. + + + final_fluo = (sample - dark) - (reference - dark) + + return final_fluo + + +def load_and_normalize_fluo(name) -> sc.DataArray : + """Loads the fluo data from the corresponding entry in the LoKI.nxs filename and + calculates the final fluo spectrum of each spectrum. + + Parameters + ---------- + name: str + Filename, e.g. 066017.nxs + + Returns + ---------- + normalized: sc.DataArray + DataArray that contains the normalized fluo signal, one spectrum or mulitple spectra. + + """ + fluo_dict = utils.load_nurfloki_file(name, 'fluorescence') + normalized = normalize_fluo(**fluo_dict) + # provide source of each spectrum in the file + #normalized.attrs['source'] = sc.scalar(name).broadcast(['spectrum'], [normalized.sizes['spectrum']]) + + return normalized + + + +def fluo_maxint_max_wavelen( + flist_num: list, + wllim: Optional[sc.Variable] = None, + wulim: Optional[sc.Variable] = None, + medfilter=True, + kernel_size=15, +): + """For a given list of files this function extracts for each file the maximum intensity and the corresponding + wavelength of each fluo spectrum. + + Parameters + ---------- + flist_num: list + List of filename for a LoKI.nxs file containting Fluo entry. + wllim: sc.Variable + Lower wavelength limit where the search for the maximum should begin + wulim: sc.Variable + Upper wavelength limit where the search for the maximum should end + medfilter: bool + If medfilter=False, not medfilter is applied. Default: True + A medfilter is applied to the fluo spectra as fluo is often more noisy + kernel_size: int or sc.Variable + kernel for median_filter along the wavelength dimension. Default: 15 + + Returns + ---------- + fluo_int_dict: dict + A dictionary of nested dictionaries. For each found monowavelength, there are nested dictionaries + for each file containing the maximum intensity "intensity_max" and the corresponding wavelength "wavelength_max" + + """ + + from collections import defaultdict + + fluo_int_dict = defaultdict(dict) + + # set default parameter + if wllim is None: + wllim=sc.scalar(300, unit='nm') + if wulim is None: + wulim=sc.scalar(400, unit='nm') + if (wllim is not None and wulim is not None): + assert wllim.value < wulim.value, "wllim < wulim" + if not wllim.unit==wulim.unit: + raise ValueError("Use same unit for wavelength range.") + + + for name in flist_num: + fluo_dict = utils.load_nurfloki_file(name, 'fluorescence') + fluo_da = normalize_fluo(**fluo_dict) + # check for the unit + #if wl_unit is None: + # wl_unit = fluo_da.coords["wavelength"].unit + + print(f'Number of fluo spectra in {name}: {fluo_da.sizes["spectrum"]}') + print(f"This is the fluo dataarray for {name}.") + display(fluo_da) + + fluo_filt_max = fluo_peak_int( + fluo_da, + wllim=wllim, + wulim=wulim, + medfilter=medfilter, + kernel_size=kernel_size, + ) + print(f"This is the fluo filt max intensity and wavelength dataset for {name}.") + display(fluo_filt_max) + + # print(f'{name} Unique mwl:', np.unique(fluo_filt_max.coords['monowavelengths'].values)) + unique_monowavelen = np.unique(fluo_filt_max.coords["monowavelengths"].values) + unique_monowavelen_unit = np.unique(fluo_filt_max.coords["monowavelengths"].unit) + + # create entries into dict + for mwl in unique_monowavelen: + fluo_int_max = fluo_filt_max["intensity_max"][ + fluo_filt_max.coords["monowavelengths"] == mwl * unique_monowavelen_unit + ].values + fluo_wavelen_max = fluo_filt_max["wavelength_max"][ + fluo_filt_max.coords["monowavelengths"] == mwl * unique_monowavelen_unit + ].values + + # I collect the values in a dict with nested dicts, separated by wavelength + fluo_int_dict[f"{mwl}{unique_monowavelen_unit}"][f"{name}"] = { + "intensity_max": fluo_int_max, + "wavelength_max": fluo_wavelen_max, + } + + return fluo_int_dict + + +def fluo_peak_int( + fluo_da: sc.DataArray, + wllim: Optional[sc.Variable] = None, + wulim: Optional[sc.Variable] = None, + medfilter=True, + kernel_size=15, +) -> sc.Dataset: + """Main task: Extract for a given wavelength range [wllim, wulim] the maximum fluo intensity and its corresponding wavelength position. + A median filter is automatically applied to the fluo data and data is extracted after its application. + TODO: Check with Cedric if it is ok to use max intensity values after filtering + + Parameters + ---------- + fluo_da: sc.DataArray + DataArray containing uv spectra + wllim: sc.Variable + Wavelength range lower limit + wulim: sc.Variable + Wavelength range upper limit + medfilter: bool + If medfilter=False, not medfilter is applied. Default: True + A medfilter is applied to the fluo spectra as fluo is often more noisy + kernel_size: int or sc.Variable + kernel for median_filter along the wavelength dimension. Expected dims + 'spectrum' and 'wavelength' in sc.DataArray + Default kernel_size in wavelength direction: 15 + + Returns + ---------- + fluo_filt_max: sc.Dataset + A new dataset for each spectrum max. intensity value and corresponding wavelength position + + + """ + if not isinstance(fluo_da, sc.DataArray): + raise TypeError("fluo_da has to be an sc.DataArray!") + + # set default parameter + if wllim is None: + wllim=sc.scalar(300, unit='nm') + if wulim is None: + wulim=sc.scalar(400, unit='nm') + if (wllim is not None and wulim is not None): + assert wllim.value < wulim.value, "wllim < wulim, lower wavelength limit needs to be smaller than upper wavelength limit" + if not wllim.unit==wulim.unit: + raise ValueError("Use same unit for wavelength range.") + + + # apply nurf_median_filter with kernel_size along the wavelength dimension + if (medfilter is True and kernel_size is not None): + fluo_da=utils.nurf_median_filter(fluo_da, kernel_size=kernel_size) + elif (medfilter is True and kernel_size is None): + kernel_size=15 + fluo_da=utils.nurf_median_filter(fluo_da, kernel_size=kernel_size) + + + # let's go and filter + # filter spectrum values for the specified interval, filtered along the wavelength dimension + fluo_filt = fluo_da["wavelength", wllim : wulim ] + + # there is no function in scipp similar to xr.argmax + # access to data np.ndarray in fluo_filt + fluo_filt_data = fluo_filt.values + # max intensity values along the wavelength axis, here axis=1, but they have seen a median filter. TODO: check with Cedric, is it okay to continue with this intensity value? + fluo_filt_max_int = fluo_filt.data.max("wavelength").values + # corresponding indices + fluo_filt_max_idx = fluo_filt_data.argmax(axis=1) + # corresponding wavelength values for max intensity values in each spectrum + fluo_filt_max_wl = fluo_filt.coords["wavelength"].values[fluo_filt_max_idx] + + # new sc.Dataset + fluo_filt_max = sc.Dataset() + fluo_filt_max["intensity_max"] = sc.Variable( + dims=["spectrum"], values=fluo_filt_max_int + ) + fluo_filt_max["wavelength_max"] = sc.Variable( + dims=["spectrum"], values=fluo_filt_max_wl, unit=wllim.unit + ) + # adding source information to each data entry in the dataset + fluo_filt_max["intensity_max"].attrs['source']=fluo_da.attrs['source'].broadcast(['spectrum'], [fluo_da.sizes['spectrum']]) + fluo_filt_max["wavelength_max"].attrs['source']=fluo_da.attrs['source'].broadcast(['spectrum'], [fluo_da.sizes['spectrum']]) + + + # add previous information to this dataarray + # TODO: Can we copy multiple coordinates from one array to another? + # add information from previous fluo_da to the new dataarray + fluo_filt_max.coords["integration_time"] = fluo_da.coords["integration_time"] + fluo_filt_max.coords["is_dark"] = fluo_da.coords["is_dark"] + fluo_filt_max.coords["is_reference"] = fluo_da.coords["is_reference"] + fluo_filt_max.coords["is_data"] = fluo_da.coords["is_data"] + fluo_filt_max.coords["monowavelengths"] = fluo_da.coords["monowavelengths"] + fluo_filt_max.coords["time"] = fluo_da.coords["time"] + + return fluo_filt_max + + + diff --git a/src/ess/loki/nurf/ill_auxilliary_funcs.py b/src/ess/loki/nurf/ill_auxilliary_funcs.py new file mode 100644 index 000000000..31ee85a85 --- /dev/null +++ b/src/ess/loki/nurf/ill_auxilliary_funcs.py @@ -0,0 +1,438 @@ +import errno +import os +import h5py +import numpy as np +import copy + +def complete_fname(scan_numbers): + """Converts a list of input numbers to a filename uses at ILL. + + Parameters + ---------- + scan_numbers: list of int or a single int + List of filenumbers or one filenumnber. + + Returns: + ---------- + flist_num: list of str or one str + List of filenames following ILL style or string following ILL style. + + """ + if isinstance(scan_numbers, int): + flist_num = f"{str(scan_numbers).zfill(6)}.nxs" + + if isinstance(scan_numbers, list): + # convert a list of input numbers to real filename + flist_num = [str(i).zfill(6) + ".nxs" for i in scan_numbers] + + return flist_num + +# This cell contains function to convert ILL raw data to the forseen Loki file format + +def loki_file_creator(loki_path, loki_filename): + """ + This function creates a dummy loki_basic.nxs that has the expected file structure for ESS Loki. + Afterwards, I will append the nurf data and rename the file. + """ + with h5py.File(os.path.join(loki_path,loki_filename), 'w') as hf: + nxentry = hf.create_group("entry") + nxentry.attrs['NX_class'] = 'NXentry' + nxinstrument=nxentry.create_group("instrument") + nxinstrument.attrs['NX_class'] = 'NXinstrument' + + +def load_one_spectro_file(file_handle, path_rawdata): + """ + This function loads one .nxs file containing spectroscopy data (fluo, uv). Data is stored in multiple np.ndarrays. + + In: + file_handle: file_handle is a file number, one element is expected otherwise an error is raised + type: list + + path_rawdata: Path to the raw data + type: str + + Out: + data: contains all relevant HDF5 entries and their content for the Nurf project (keys and values) + type: dict + + """ + + # create path to file, convert file_number to string + file_path_spectro = os.path.join(path_rawdata, file_handle + '.nxs') + + # check if file exists + if not os.path.isfile(file_path_spectro): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), + file_path_spectro) + + # we are ready to load the data set + # open the .nxs file and read the values + with h5py.File(file_path_spectro, "r") as f: + # access nurf sub-group + nurf_group = '/entry0/D22/nurf/' + + # access keys in sub-group + nurf_keys = list(f[nurf_group].keys()) + # how many keys exist + len_nurf_keys = len(nurf_keys) + + # print(nurf_keys) + # print(len_nurf_keys) + + # this returns a list with HDF5datasets (I think) + # data_spectro_file=list(f[nurf_group].values()) + + # data_spectro_file=f[nurf_group].values() + # data_spectro_file=f[nurf_group] + + # extract all data of the nurf subgroup and store it in a new dict + + # initialise an empty dict + data = {} + + for key in f[nurf_group].keys(): + # print(key) + # this is how I get string giving the full path to this dataset + path_dataset = f[nurf_group][key].name + # print(path_dataset) + # print(f[nurf_group][key].name) #this prints the full path to the dataset + # print(type(f[path_dataset][:])) #this gives me access to the content of the data set, I could use : or () inside [], out: np.ndarray + + # This gives a dict with full path name as dict entry followed by value. No, I don't want this, but good to know. + # data[f[nurf_group][key].name]=f[path_dataset][:] + + # This gives a dict where the keys corresponds to the key names of the h5 file. + data[key] = f[path_dataset][:] + + # print(f[nurf_group].get('Fluo_spectra')) + + # print a hierachical view of the file (simple) + # like this is would only go through subgroups + # f[nurf_group].visititems(lambda x, y: print(x)) + + # walk through the whole file and show the attributes (found on github as an explanation for this function) + # def print_attrs(name, obj): + # print(name) + # for key, val in obj.attrs.items(): + # print("{0}: {1}".format(key, val)) + # f[nurf_group].visititems(print_attrs) + + # print(data_spectro_file) + + # file_handle is returned as np.ndarray and its an np.array, elements correspond to row indices + return data + + +def nurf_file_creator(loki_file, path_to_loki_file, data): + """ + Appends NUrF group to LOKI NeXus file for ESS + + Args: + loki_file (str): filename of NeXus file for Loki + path_to_loki_file (str): File path where the NeXus file for LOKI is stored + data (dict): Dictionary with dummy data for Nurf + """ + + # change directory where the loki.nxs is located + os.chdir(path_to_loki_file) + + # open the file and append + with h5py.File(loki_file, 'a') as hf: + + #comment on names + #UV/FL_Background is the dark + #UV/FL_Intensity0 is the reference + #UV/FL_Spectra is the sample + + # image_key: number of frames (nFrames) given indirectly as part of the shape of the arrays + # TODO: keep in mind what happens if multiple dark or reference frames are taken + + # remove axis=2 of length one from this array + data['UV_spectra']=np.squeeze(data['UV_spectra'], axis=2) #this removes the third axis in this array, TODO: needs later to be verified with real data from hardware + + # assemble all spectra in one variable + uv_all_data=np.row_stack((data['UV_spectra'],data['UV_background'], data['UV_intensity0'])) + + dummy_vec=np.full(np.shape(uv_all_data)[0], False) + + #create boolean masks for data, dark, reference + uv_nb_spectra=np.shape(data['UV_spectra'])[0] + + # data mask, copy and replace first entries + uv_data_mask=copy.copy(dummy_vec) + uv_data_mask[0:uv_nb_spectra]=True + + # dark mask + # find out how many darks exist + # TODO: Is there always a background or do we need to catch this case if there isn't? + if data['UV_background'].ndim==1: + uv_nb_darks=1 + else: + uv_nb_darks=np.shape(data['UV_background'])[1] #TODO: needs to be verified with real data from Judith's setup + + uv_dark_mask=copy.copy(dummy_vec) + uv_dark_mask[uv_nb_spectra:uv_nb_spectra+uv_nb_darks]=True + + # reference + # how many references where taken? + if data['UV_intensity0'].ndim==1: + uv_nb_ref=1 + else: + uv_nb_ref=np.shape(data['UV_intensity0'])[1] #TODO: needs to be verified with real data from Judith's setup + + # reference mask, copy and replace first entries + uv_ref_mask=copy.copy(dummy_vec) + uv_ref_mask[uv_nb_spectra+uv_nb_darks:uv_nb_spectra+uv_nb_darks+uv_nb_ref]=True + + + + # UV subgroup + grp_uv = hf.create_group("/entry/instrument/uv") + grp_uv.attrs["NX_class"] = 'NXdata' + + # uv spectra + uv_signal_data=grp_uv.create_dataset('data', data=uv_all_data, dtype=np.float32) + uv_signal_data.attrs['long name']= 'all_data' + uv_signal_data.attrs['units']= 'counts' + grp_uv.attrs['signal']= 'data' #indicate that the main signal is data + grp_uv.attrs['axes']= [ "spectrum", "wavelength" ] #time is here the first axis, i.e axis=0, wavelength is axis=1 + + # define the AXISNAME_indices + grp_uv.attrs['time_indices'] = 0 + grp_uv.attrs['integration_time_indices'] = 0 + grp_uv.attrs['wavelength_indices'] = 1 + + # introducing a key that is interpretable for sample, dark, and reference + grp_uv.attrs['is_data_indices'] = 0 + grp_uv.attrs['is_dark_indices'] = 0 + grp_uv.attrs['is_reference_indices'] = 0 + + grp_uv.create_dataset('is_data', data=uv_data_mask, dtype=bool) + grp_uv.create_dataset('is_dark', data=uv_dark_mask, dtype=bool) + grp_uv.create_dataset('is_reference', data=uv_ref_mask, dtype=bool) + + # uv_time + # dummy timestamps for uv_time + # TODO: Codes will have to change later for the real hardware. + uv_time = np.empty(np.shape(uv_all_data)[0], dtype='datetime64[us]') + for i in range(0, np.shape(uv_time)[0]): + uv_time[i]=np.datetime64('now') + + # see https://stackoverflow.com/questions/23570632/store-datetimes-in-hdf5-with-h5py + # suggested work around because h5py does not support time types + uv_time_data=grp_uv.create_dataset('time', data=uv_time.view(' I am not sure currently, I wait for Simon's advice + + out1 = sc.plot( + sc.collapse(uv_dict["sample"], keep="wavelength"), + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + title=f"{name} - raw data", + ax=axs[0], + ) + + # How to plot dark and reference? + #out4 = sc.plot( + # {"dark": uv_dict["dark"], "reference": uv_dict["reference"]}, + # linestyle="dashed", + # marker=".", + # grid=True, + # legend=legend_props, + # title=f"{name} - dark and reference", + # ax=axs[1], + #) + # How to plot dark and reference? + to_plot = {} + for group in ('dark', 'reference'): + for key, da in sc.collapse(uv_dict[group]["spectrum", :], keep="wavelength").items(): + to_plot[f'{group}-{key}'] = da + + out4= sc.plot(to_plot, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + #figsize=figure_size2, + title=f"{name}, dark and reference spectra - all", + ax=axs[1], + ) + #display(out4) + + + # How to plot individual calculated spectra from one .nxs file + out2 = sc.plot( + sc.collapse(normalized, keep="wavelength"), + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + title=f"{name} - calculated", + ax=axs[2], + ) + #display(out2) + + # How to plot averaged spectra (.mean and NOT .sum) + out3 = sc.plot( + sc.collapse(normalized.mean("spectrum"), keep="wavelength"), + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + title=f"{name} - averaged", + ax=axs[3], + ) + #display(out3) + #display(figs) + +def plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None): + """Plots a set of averaged UV spectra + + Parameters + ---------- + flist_num: list of int + List of filenames as numbers (ILL style) containing UV data + lambda_min: float + Minimum wavelength + lambda_max: float + Maximum wavelength + vmin: float + Minimum y-value + vmax: float + Maximum y-value + + Returns + ---------- + fig_all_spectra: scipp.plotting.objects.Plot + + """ + + # creates a dataset based on flist_num, plots all of them + uv_spectra_set = gather_uv_set(flist_num) + # How to plot multiple UV spectra and zoom in. + # set figure size and legend props + figure_size = (7, 4) + legend_props = {"show": True, "loc": (1.04, 0)} + fig_all_spectra = uv_spectra_set.plot( + linestyle="dashed", + marker=".", + grid=True, + legend=legend_props, + figsize=figure_size, + ) + if vmin is not None and vmax is not None: + fig_all_spectra.ax.set_ylim(vmin, vmax) + if lambda_min is not None and lambda_max is not None: + fig_all_spectra.ax.set_xlim(lambda_min, lambda_max) + display(fig_all_spectra) + return fig_all_spectra + +def plot_uv_peak_int( + uv_da: sc.DataArray, + name, + wavelength=None, + wl_unit=None, + tol=None, + medfilter=False, + kernel_size=None, +): + """Plotting of extracted uv peak intensity for a given wavelength [wl_unit] and a given interval [wl_unit] in file name + First version: interval around wavelength of width 2*tol, values are averaged and then we need interpolation to get value at requested wavelength??? not sure yet how to realise this + If no wavelength is given, 280 is chosen as value + If no wl_unit is given, the unit of the wavelength coordinate of the sc.DataArray is chosen, e.g. [nm]. Other option to generate a unit: wl_unit=sc.Unit('nm') + Kernel size determined the window for the medfilter + + Parameters + ---------- + uv_da: sc.DataArray + DataArray containing uv spectra + name: str + Filename of a file containting uv data + wavelength: float + Wavelength + wl_unit: sc.Unit + Unit of the wavelength + tol: float + Tolerance, 2*tol defines the interval around the given wavelength + medfilter: bool + If medfilter=False, not medfilter is applied. Default: True + A medfilter is applied to the fluo spectra as fluo is often more noisy + kernel_size: int or sc.Variable + kernel for median_filter along the wavelength dimension. Expected dims + 'spectrum' and 'wavelength' in sc.DataArray + + Returns + ---------- + uv_peak_int: dict + Dictionary that contains the peak intensity for the requested wavelength, the peak intensity averaged over the requested interval, the requested wavelength with its unit, and the tolerance + + """ + if not isinstance(uv_da, sc.DataArray): + raise TypeError + + if medfilter is not False: + # apply medfiler with kernel_size along the wavelength dimension + if ('spectrum' and 'wavelength') in fluo_da.dims: + kernel_size_sc={'spectrum':1, 'wavelength':kernel_size} + else: + raise ValueError('Dimensions spectrum and wavelength expected.') + uv_da=median_filter(uv_da, size=kernel_size_sc) + + + # process + uv_peak_int_dict = uv_peak_int( + uv_da, wavelength=wavelength, wl_unit=wl_unit, tol=tol + ) + # append name to dict + uv_peak_int_dict["filename"] = name + + print(uv_peak_int_dict["one_wavelength"]) + print(uv_peak_int_dict["wl_interval"]["spectrum", :]) + + # set figure size and legend props + # figs, axs = plt.subplots(1, 2, figsize=(10 ,3)) + fig = plt.figure(figsize=(12, 4)) + + gs = gridspec.GridSpec(nrows=1, ncols=2, width_ratios=[1, 1], wspace=0.4) + ax0 = fig.add_subplot(gs[0, 0]) + out1 = sc.plot( + uv_peak_int_dict["one_wavelength"]["spectrum", :].squeeze(), + linestyle="none", + grid=True, + title=f"UV peak intensity for {uv_peak_int_dict['wavelength']} {uv_peak_int_dict['unit']} in {name} ", + ax=ax0, + ) + ax0.set_ylabel("UV int") + ax1 = fig.add_subplot(gs[0, 1]) + out2 = sc.plot( + uv_peak_int_dict["wl_interval"]["spectrum", :], + linestyle="none", + grid=True, + title=f"UV peak intensity for intervall [{uv_peak_int_dict['wavelength']-uv_peak_int_dict['tol']} , {uv_peak_int_dict['wavelength']+uv_peak_int_dict['tol']}]{uv_peak_int_dict['unit']} in {name} ", + ax=ax1 + ) + ax1.set_ylabel("UV int") + +def uv_quick_data_check( + filelist, + wavelength=None, + wl_unit=None, + tol=None, + medfilter=False, + kernel_size=None, +): + """Plots uv peak intensity for a given wavelength with unit wl_unit for a given + filelist in order of items in filelist. Per recorded spectrum in the file a data + point is shown. + + """ + # Currently I cannot rely on having the same number of spectra in each file currently, sc.Dataset will not work + # The following does not work if the number of spectra in each Loki file is not the same :() + # out= sc.Dataset({name:load_and_normalize_uv(name) for name in filelist}) + + # Let's try dictionary comprehension ... yeap that works + uv_dict = {name: uv.load_and_normalize_uv(name) for name in filelist} + + # apply medianfilter + if medfilter is not False: + # apply medfilter with kernel_size along the wavelength dimension + if ('spectrum' and 'wavelength') in fluo_da.dims: + kernel_size_sc={'spectrum':1, 'wavelength':kernel_size} + else: + raise ValueError('Dimensions spectrum and wavelength expected.') + uv_dict = { + name: median_filter(uv_dict[name], size=kernel_size_sc) + for name in filelist + } + + # extract peak intensities + # function: uv_peak_int(uv_da: sc.DataArray, wavelength=wavelength, wl_unit=wl_unit, tol=tol) + # this process will return a dictionary of dictionaries + uv_peak_int_dict = { + name: uv_peak_int( + uv_dict[name], wavelength=wavelength, wl_unit=wl_unit, tol=tol + ) + for name in filelist + } + + # prepare plots + figure_size = (20, 10) + fig = plt.figure(figsize=figure_size) + gs = gridspec.GridSpec( + nrows=2, + ncols=3, + width_ratios=[1, 1, 1], + wspace=0.3, + height_ratios=[1, 1], + ) + ax0 = fig.add_subplot(gs[0, 0]) + ax1 = fig.add_subplot(gs[0, 1]) + + # Testing but so far unsuccessful + # print(uv_peak_int_dict['066017.nxs']['one_wavelength'].data) + # display(sc.plot(sc.collapse(uv_peak_int_dict['066017.nxs']['one_wavelength'].data,keep='spectrum'))) + + uv_peak_one_wl = {} # make empty dict + for name in filelist: + # uv_peak_one_wl[f'{name}']=uv_peak_int_dict[f'{name}']['one_wavelength']['spectrum',:] + # print(uv_peak_one_wl[f'{name}']) + + ax0.plot( + uv_peak_int_dict[f"{name}"]["one_wavelength"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax0.legend() + + ax1.plot( + uv_peak_int_dict[f"{name}"]["wl_interval"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax1.legend() + + ax0.grid(visible=True) + ax0.set_title( + f"UV peak intensity for {uv_peak_int_dict[filelist[0]]['wavelength']} {uv_peak_int_dict[filelist[0]]['unit']} " + ) + ax0.set_xlabel("spectrum") + + ax1.plot( + uv_peak_int_dict[filelist[0]]["wl_interval"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax1.legend() + ax1.grid(visible=True) + ax1.set_title( + f"UV peak intensity for intervall [{uv_peak_int_dict[filelist[0]]['wavelength']-uv_peak_int_dict[filelist[0]]['tol']} , {uv_peak_int_dict[filelist[0]]['wavelength']+uv_peak_int_dict[filelist[0]]['tol']}]{uv_peak_int_dict[filelist[0]]['unit']}" + ) + print( + uv_peak_int_dict[filelist[0]]["wl_interval"]["spectrum", :].values, + uv_peak_int_dict[filelist[0]]["wl_interval"]["spectrum", :].values, + ) + ax1.set_xlabel("spectrum") + + display(fig) + + +def plot_multiple_uv_peak_int( + filelist, + wavelength=None, + wl_unit=None, + tol=None, + medfilter=False, + kernel_size=None, +): + """Plots uv peak intensity for a given wavelength with unit wl_unit for a given filelist in order of items in filelist + Explorative state. I fall back to matplotlib because it is complicated. Try later with sc.plot. + Currently, I cannot rely on all files having the same number of spectra. I cannot create a sc.Dataset. I am unsure how to catch this with Python. + No averaging in uv spectra when calculating the uv peak intensity within one file. + Medfilter can be activated + + #TODO: Get rid of the for loop and and make filename an attribute when loading nexus files, filename as new dimension? + """ + + # currently I cannot rely on having the same number of spectra in each file currently, sc.Dataset will not work + fig = plt.figure(figsize=(24, 7)) + gs = gridspec.GridSpec( + nrows=1, ncols=2, width_ratios=[1, 1], wspace=0.2, height_ratios=[1], top=0.9, bottom=0.15, left=0.1, right=0.9 + ) + ax0 = fig.add_subplot(gs[0, 0]) + ax1 = fig.add_subplot(gs[0, 1]) + + + + for i, name in enumerate(filelist): + uv_dict= utils.load_nurfloki_file(name, 'uv') + uv_da = uv.normalize_uv(**uv_dict) + + # check for medfilter + if medfilter is not False: + # apply medianfilter + uv_da = utils.nurf_median_filter( uv_da,kernel_size=kernel_size ) + + # process + uv_peak_int_dict=uv.uv_peak_int(uv_da , wavelength=wavelength, tol=tol) + # append name to dict + uv_peak_int_dict["filename"] = name + + num_spec = uv_peak_int_dict["wl_interval"]["spectrum", :].values.size + + # prints number of spectra in each file and artifical x-value + # print(num_spec,i*np.ones(num_spec)) + + # to offset each spectrum on the x-axis, I create an artifical x-axis at the moment, TODO: could be later replaced by the pertubation parmeter (time, concentration) + ax0.plot( + i * np.ones(num_spec), + uv_peak_int_dict["one_wavelength"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax0.legend( loc="best") + ax0.set_ylabel("Abs") + ax0.grid(visible=True) + ax0.set_title( + f"UV peak intensity for {uv_peak_int_dict['wavelength'].value} {uv_peak_int_dict['wavelength'].unit} " + ) + + ax1.plot( + i * np.ones(num_spec), + uv_peak_int_dict["wl_interval"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax1.legend(loc="best") + ax1.set_ylabel("Abs") + ax1.grid(visible=True) + ax1.set_title( + f"UV peak intensity for intervall [{uv_peak_int_dict['wavelength'].value-uv_peak_int_dict['tol'].value} , {uv_peak_int_dict['wavelength'].value+uv_peak_int_dict['tol'].value}]{uv_peak_int_dict['wavelength'].unit}" + ) + + # overwrite xticks in ax0 + ax0.set_xticks( + np.arange(0, len(filelist), 1), + labels=[f"{name}" for name in filelist], + rotation=90, + ) + # overwrite xticks in ax1 + ax1.set_xticks( + np.arange(0, len(filelist), 1), + labels=[f"{name}" for name in filelist], + rotation=90, + ) + + + + # a quick hack for the poster + cm = 1/2.54 # centimeters in inches + #figsize_b=8 + #figsize_a=1.333*figsize_b + figsize_a=15 + figsize_b= figsize_a*0.75 + + fig3, ax0 = plt.subplots(1, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) ) + + + for i, name in enumerate(filelist): + uv_dict= utils.load_nurfloki_file(name, 'uv') + uv_da = uv.normalize_uv(**uv_dict) + + # check for medfilter + if medfilter is not False: + # apply medianfilter + uv_da = utils.nurf_median_filter( uv_da,kernel_size=kernel_size ) + + # process + uv_peak_int_dict=uv.uv_peak_int(uv_da , wavelength=wavelength, tol=tol) + # append name to dict + uv_peak_int_dict["filename"] = name + + num_spec = uv_peak_int_dict["wl_interval"]["spectrum", :].values.size + + # prints number of spectra in each file and artifical x-value + # print(num_spec,i*np.ones(num_spec)) + + # to offset each spectrum on the x-axis, I create an artifical x-axis at the moment, TODO: could be later replaced by the pertubation parmeter (time, concentration) + ax0.plot( + i * np.ones(num_spec), + uv_peak_int_dict["one_wavelength"]["spectrum", :].values, + "o", + label=f"{name}", + ) + ax0.legend( bbox_to_anchor=(1.05, 1.03), loc="upper left" ) + ax0.set_ylabel("Abs") + ax0.grid(visible=True) + ax0.set_title( + f"UV peak intensity for {uv_peak_int_dict['wavelength'].value} {uv_peak_int_dict['wavelength'].unit} " + ) + + + # overwrite xticks in ax0 + ax0.set_xticks( + np.arange(0, len(filelist), 1), + labels=[f"{name}" for name in filelist], + rotation=90, + ) + + + return fig3 + #display(fig) + + +def plot_fluo_peak_int( + fluo_da: sc.DataArray, + wllim: Optional[sc.Variable] = None, + wulim: Optional[sc.Variable] = None, + medfilter=True, + kernel_size=15, +): + """Plot max intensity value found in a given wavelength interval and + corresponding wavelength, both as function monowavelengths in one + file "name" + + """ + + # extract max int value and corresponding wavelength position + fluo_filt_max = fluo.fluo_peak_int( + fluo_da, + wllim=wllim, + wulim=wulim, + medfilter=medfilter, + kernel_size=kernel_size, + ) + # attach filename as attribute to dataarray + #fluo_da.attrs["name"] = sc.scalar(name) #not necessary anymore + #print('This is the fluo_da scipp array received as input.') + #display(fluo_da) + + source = np.unique(fluo_da.attrs['source'].values) + name = (lambda x: x)(*source) #https://stackoverflow.com/questions/33161448/getting-only-element-from-a-single-element-list-in-python + + + fig = plt.figure(figsize=(22, 7)) + gs = gridspec.GridSpec( + nrows=1, + ncols=2, + width_ratios=[1, 1], + wspace=0.3, + height_ratios=[1], + ) + ax0 = fig.add_subplot(gs[0, 0]) + ax1 = fig.add_subplot(gs[0, 1]) + + out0 = sc.plot( + fluo_filt_max["intensity_max"]["spectrum", :], + grid=True, + labels={"spectrum": "monowavelengths"}, + title=f"Sample: {name}", + ax=ax0, + ) + + x = np.arange(len(fluo_filt_max.coords['monowavelengths'].values)) # the label locations + out0.ax.set_xticks(x) + # Set ticks labels for x-axis + out0.ax.set_xticklabels(fluo_filt_max.coords['monowavelengths'].values) + + out1 = sc.plot( + fluo_filt_max["wavelength_max"]["spectrum", :], + grid=True, + labels={"spectrum": "monowavelengths"}, + title=f"Sample: {name}", + ax=ax1, + ) + out1.ax.set_xticks(x) + out1.ax.set_xticklabels(fluo_filt_max.coords['monowavelengths'].values) + #display(fig) + + +def plot_fluo_multiple_peak_int( + filelist: list, + wllim: Optional[sc.Variable] = None, + wulim: Optional[sc.Variable] = None, + medfilter=True, + kernel_size: Optional[int] = None, +): + """Plot multiple max peak intensities for given wavelength range and corresponding + position of found maximum for a series of fluo measurements. + + Parameters + ---------- + filelist: list + List of complete filenames for LoKI.nxs containing fluo spectra + wllim: float + Wavelength range lower limit + wulim: float + Wavelength range upper limit + medfilter: boolean + If medfilter=False, no medfilter is applied + kernel_size: int + kernel for medianfilter + + Returns + ---------- + + """ + + # setting the scene for the markers + marker = itertools.cycle(markers(15)) + + print(filelist) + + #mpr,a; + figure_size = (17, 5) + fig, ax = plt.subplots( + nrows=1, ncols=2, figsize=figure_size, constrained_layout=True + ) + + #poster hack + #cm = 1/2.54 # centimeters in inches + #figsize_b=8 + #figsize_a=1.333*figsize_b + #figsize_a=13 + #figsize_b= figsize_a*1.6 + + #fig, ax = plt.subplots(2, 1, constrained_layout=True, figsize=(figsize_a*cm, figsize_b*cm) ) + + + unique_mwl = [] + ds_list = [] + for name in filelist: + fluo_dict=utils.load_nurfloki_file(name,'fluorescence') + fluo_da = fluo.normalize_fluo(**fluo_dict) + # extract max int value and corresponding wavelength position, median filter is applied + fluo_filt_max = fluo.fluo_peak_int( + fluo_da, + wllim=wllim, + wulim=wulim, + medfilter=medfilter, + kernel_size=kernel_size, + ) + # attach filename as attribute to dataset, TODO: should this happen in fluo_peak_int ? + #fluo_filt_max.attrs["name"] = sc.scalar(name) + # display(fluo_filt_max) + ds_list.append(fluo_filt_max) + unique_mwl.append(np.unique(fluo_filt_max.coords["monowavelengths"].values)) + # print(fluo_filt_max) + + # same marker for both plots for the same file + markerchoice = next(marker) + + ax[0].plot( + fluo_filt_max.coords["monowavelengths"].values, + fluo_filt_max["intensity_max"].values, + label=f"{name}", + linestyle="None", + marker=markerchoice, + markersize=10, + ) + ax[0].set_ylabel("Max. Intensity") + #ax[0].set_title("Fluo - max. intensity") + ax[0].set_title(r'Fluo - $\mathrm{Int}_{\mathrm{max}}$') + + ax[1].plot( + fluo_filt_max.coords["monowavelengths"].values, + fluo_filt_max["wavelength_max"].values, + label=f"{name}", + linestyle="None", + marker=markerchoice, + markersize=10, + ) + unit_str = str(fluo_filt_max["wavelength_max"].unit) + + ax[1].set_ylabel(f"Wavelength [{unit_str}]") + #ax[1].set_title("Fluo - corresponding wavelength") + ax[1].set_title(r'Fluo - $\lambda_{\mathrm{max}}$') + + # show the lowest monowavelength as lower boundary on the y-axis + ax[1].set_ylim(bottom=0.9 * np.min(fluo_filt_max.coords["monowavelengths"].values)) + + # plot the found monowavelengths as additional visual information on the y-axis + for mwl in np.unique(unique_mwl): + ax[1].plot( + np.unique(unique_mwl), + np.full(np.shape(np.unique(unique_mwl)), mwl), + "--", + label=f"{mwl}{sc.Unit('nm')}", + ) + # ax[1].legend(loc='upper right', bbox_to_anchor=(1.05, 1.05)) + + for axes in ax: + #axes.legend(loc='upper right', bbox_to_anchor=(1.1, 1.00)) + axes.legend(loc='best')#, bbox_to_anchor=(1.06, 1.03)) + axes.grid(True) + axes.set_xlabel("Monowavelengths [nm]") + + + #return fig + #display(fig) + + +def plot_fluo_spectrum_selection( + flist_num: list, + spectral_idx: int, + kernel_size: Optional[int] = None, + wllim: Optional[float] = None, + wulim: Optional[float] = None, + wl_unit: Optional[sc.Unit] = None, +) -> dict: + """This function extracts a specific fluo spectrum from all given files. Ideally, the user provides the number index. + A median filter can be applied to the input data. A lower and upper wavelength range will be used for a zoomed-in image. + Selected spectra are all plotted in one graph. + + Parameters + ---------- + flist_num: list + List of LoKI.nxs file containting fluo entry. + spectral_idx: int + Index of spectrum for selection + kernel_size: int + Scalar giving the size of the median filter window. Elements of kernel_size should be odd. Default size is 3 + wllim: float + Lower wavelength limit + wulim: float + Upper wavelength limit + wl_unit: sc.Unit + Wavelength unit of a fluo spectum + + Returns + ---------- + fluo_spec_idx_ds: sc.Dataset + sc.Dataset containing selected spectra for each input LoKI.nxs + + """ + # This is maybe not necessary, because scipp catches it later, but I can catch it earlier. + if not isinstance(spectral_idx, int): + raise TypeError("Spectral index should be of type int.") + + if wllim is None: + wllim = 300 + if wulim is None: + wulim = 400 + + # obtain unit of wavelength, I extract it from the first element in the flist_num, + # but I have to load it + fluo_dict=utils.load_nurfloki_file(flist_num[0],'fluorescence') + final_fluo = fluo.normalize_fluo(**fluo_dict) + if wl_unit is None: + wl_unit = final_fluo.coords["wavelength"].unit + else: + if not isinstance(wl_unit, sc.Unit): + raise TypeError("wl_unit should be of type sc.Unit.") + assert ( + wl_unit == final_fluo.coords["wavelength"].unit + ) # we check that the given unit corresponds to the unit for the wavelength + + # prepare data + fluo_spec_idx_ds = sc.Dataset() + for name in flist_num: + fluo_dict=utils.load_nurfloki_file(name,'fluorescence') + fluo_normalized = fluo.normalize_fluo(**fluo_dict) + final_fluo = apply_medfilter( + fluo_normalized, kernel_size=kernel_size + ) # apply medfilter + fluo_spec_idx_ds[name] = final_fluo[ + "spectrum", spectral_idx + ] # append data array to dataset + + # Plotting, TODO: should it be decoupled form the loading? + legend_props = {"show": True, "loc": (1.04, 0)} + fig = plt.figure(figsize=(17, 5)) + gs = gridspec.GridSpec(nrows=1, ncols=2, width_ratios=[1, 1], wspace=0.5) + ax0 = fig.add_subplot(gs[0, 0]) + ax1 = fig.add_subplot(gs[0, 1]) + out0 = sc.plot( + fluo_spec_idx_ds, + grid=True, + linestyle="dashed", + title=f"Normalized fluo spectra, selected spectrum #{spectral_idx}", + legend=legend_props, + ax=ax0, + ) # would also work with fluo_spec_idx_dict + out1 = sc.plot( + fluo_spec_idx_ds["wavelength", wllim * wl_unit : wulim * wl_unit], + grid=True, + linestyle="dashed", + title=f"Normalized fluo spectra, selected spectrum #{spectral_idx}", + legend=legend_props, + ax=ax1, + ) + display(fig) + + return fluo_spec_idx_ds + + + +def plot_uv_turbidity_fit(uv_da: sc.DataArray, + fit_llim: Optional[sc.Variable] = None, + fit_ulim: Optional[sc.Variable] = None, + b_llim: Optional[sc.Variable] = None, + b_ulim: Optional[sc.Variable] = None, + m=None): + + # carry out the fit + uv_da_turbcorr= uv.uv_turbidity_fit( + uv_da, + fit_llim=fit_llim, + fit_ulim=fit_ulim, + b_llim=b_llim, + b_ulim=b_ulim, + m=m + ) + # get the values for the fit ranges + fit_llim, fit_ulim=uv_da_turbcorr.attrs["turbidity_fit_range"] + b_llim, b_ulim= uv_da_turbcorr.attrs["b_fit_range"] + + # select the UV wavelength range for fitting the turbidity + uv_da_filt = uv_da["wavelength", fit_llim : fit_ulim ] + + # How many spectra are in the file? + num_spec = uv_da_filt.sizes["spectrum"] + + + # Plotting results as sc.plot + fig2, ax2 = plt.subplots(ncols=2, figsize=(12, 5)) + out0 = sc.plot( + sc.collapse(uv_da, keep="wavelength"), + grid=True, + title="before correction", + ax=ax2[0], + ) + out1 = sc.plot( + sc.collapse(uv_da_turbcorr, keep="wavelength"), + grid=True, + title="after correction", + ax=ax2[1], + ) + + # Plotting intermediate steps + fig, ax = plt.subplots(ncols=num_spec + 1, figsize=(18, 5)) + out3 = sc.plot( + sc.collapse(uv_da_filt, keep="wavelength"), + grid=True, + title="Selection for turbidity fit", + ax=ax[-1], + ) + + for i in range(num_spec): + # collect the fitting parameters for each spectrum to avoid new fitting + popt = [ + uv_da_turbcorr.attrs["fit-offset_b"]["spectrum", i].values, + uv_da_turbcorr.attrs["fit-slope_m"]["spectrum", i].values, + ] + + ax[i].plot( + uv_da.coords["wavelength"].values, + uv_da["spectrum", i].values, + "s", + label=f"Full UV raw data {i}", + ) + ax[i].plot( + uv_da.coords["wavelength"].values, + uv_da["spectrum", i].values + - uv.turbidity(uv_da.coords["wavelength"].values, popt[0], popt[1]), + "x", + label=f"Whole UV spectrum, fitted turbidity subtracted {i}", + ) + ax[i].plot( + uv_da.coords["wavelength"].values, + uv.turbidity(uv_da.coords["wavelength"].values, popt[0], popt[1]), + "^", + label=f"Full turbidity {i}", + ) + + ax[i].plot( + uv_da_filt.coords["wavelength"].values, + uv.turbidity(uv_da_filt.coords["wavelength"].values, popt[0], popt[1]), + ".", + label=f"Fitted turbidity {i}, b={popt[0]:.3f}, m={popt[1]:.3f}, [{fit_llim.value}:{fit_ulim.value}]{fit_llim.unit}", + ) + + # ax[i].plot(uv_da['wavelength', b_llim*wl_unit: b_ulim*wl_unit]['spectrum',i].coords['wavelength'].values, uv_da['wavelength', + # b_llim*wl_unit: b_ulim*wl_unit]['spectrum',i].values,'v', label=f'Selection for b0 {i}') + # No need to slice in the spectrum dimension for the x values + ax[i].plot( + uv_da["wavelength", b_llim : b_ulim ] + .coords["wavelength"] + .values, + uv_da["wavelength", b_llim:b_ulim]["spectrum", i].values, + "v", + label=f"Selection for b0 {i}: [{b_llim.value}:{b_ulim.value}]{b_llim.unit}", + ) + + ax[i].grid(True) + ax[i].set_xlabel("Wavelength [nm]") + ax[i].set_ylabel("Absorbance") + ax[i].legend() + ax[i].set_title(f"Spectrum {str(i)}") + # set limits, np.isfinite filters out inf (and nan) values + ax[i].set_ylim( + [ + -0.5, + 1.1 + * uv_da["spectrum", i] + .values[np.isfinite(uv_da["spectrum", i].values)] + .max(), + ] + ) + + display(fig2) + +def plot_uv_multi_turbidity_fit( + filelist, + fit_llim: Optional[sc.Variable] = None, + fit_ulim: Optional[sc.Variable] = None, + b_llim: Optional[sc.Variable] = None, + b_ulim: Optional[sc.Variable] = None, + m=None +): + + multi_uv_turb_corr_da=uv.uv_multi_turbidity_fit(filelist, + fit_llim=fit_llim, + fit_ulim=fit_ulim, + b_llim=b_llim, + b_ulim=b_ulim, + m=m ) + + fig, ax = plt.subplots(ncols=3, figsize=(21, 7)) + legend_props = {"show": True, "loc": 1} + num_spectra = multi_uv_turb_corr_da.sizes["spectrum"] + + out = sc.plot( + sc.collapse(multi_uv_turb_corr_da, keep="wavelength"), + grid=True, + ax=ax[0], + legend=legend_props, + title=f"All turbidity corrected UV spectra for {num_spectra} spectra", + ) + ax[0].set_ylim( + [ + -1, + 1.2 + * multi_uv_turb_corr_da.data.values[ + np.isfinite(multi_uv_turb_corr_da.data.values) + ].max(), + ] + ) + + out2 = sc.plot( + sc.collapse(multi_uv_turb_corr_da.attrs["fit-offset_b"], keep="spectrum"), + title=f"All fit-offset b for {num_spectra} spectra", + ax=ax[1], + grid=True, + ) + + # ax0.set_xticks(np.arange(0,len(filelist),1), labels=[f'{name}' for name in filelist], rotation=90) + secx = ax[1].secondary_xaxis(-0.2) + secx.set_xticks( + np.arange(0, num_spectra, 1), + labels=[f"{name}" for name in multi_uv_turb_corr_da.attrs["source"].values], + rotation=90, + ) + out3 = sc.plot( + sc.collapse(multi_uv_turb_corr_da.attrs["fit-slope_m"], keep="spectrum"), + title=f"All fit-slope m for {num_spectra} spectra", + ax=ax[2], + grid=True, + ) + secx2 = ax[2].secondary_xaxis(-0.2) + secx2.set_xticks( + np.arange(0, num_spectra, 1), + labels=[f"{name}" for name in multi_uv_turb_corr_da.attrs["source"].values], + rotation=90, + ) + + + + + +################################################ +# +# Fluo plot functions +# +################################################ + + +def plot_fluo(name: str): + """Plots all fluo spectra contained in a LoKI.nxs + Currently, we separate between good and bad fluo spectra collected during a ILL experiment. This differentiation can later go for LoKI. + First graph: Plot of all raw fluo spectra + Second graph: Plot dark and reference for the fluo path + Third graph: All bad fluo spectra recorded during the ILL experiment + Fourth graph: All good fluo spectra recorded during the ILL experiment + Fifth graph: All good fluo spectra for wavelength range 250nm - 600nm + + + Parameters + ---------- + name: str + Filename of a LoKI.nxs file + + """ + + fluo_dict=utils.load_nurfloki_file(name,'fluorescence') + final_fluo = fluo.normalize_fluo(**fluo_dict) + + # set figure size and legend props + figure_size = (8, 4) + legend_props = {"show": True, "loc": (1.04, 0)} + + all_fluo_raw_spectra={} + #should this be more consistent? + #possible via fluo_dict["sample"] or final_fluo + for i in range(1, fluo_dict["sample"].sizes["spectrum"]): + mwl = str(final_fluo.coords["monowavelengths"][i].value) + "nm" + all_fluo_raw_spectra[f"spectrum:{i}, {mwl}"] = final_fluo["spectrum", i] + + # plot all fluo raw spectra + out1 = sc.plot( + #sc.collapse(fluo_dict["sample"]["spectrum", :], keep="wavelength"), + all_fluo_raw_spectra, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size, + title=f"{name}, raw fluo spectrum - all", + ) + display(out1) + + # plot raw and dark spectra for fluo part + figure_size2 = (10, 6) + legend_props = {"show": True, "loc": (1.04, 0)} + + to_plot = {} + for group in ('dark', 'reference'): + for key, da in sc.collapse(fluo_dict[group]["spectrum", :], keep="wavelength").items(): + to_plot[f'{group}-{key}'] = da + out2= sc.plot(to_plot, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size2, + title=f"{name}, dark and reference spectra - all" + ) + display(out2) + + # specific for ILL data, every second sppectrum good, pay attention to range() where + # the selection takes place + only_bad_spectra = {} # make empty dict + for i in range(0, fluo_dict["sample"].sizes["spectrum"], 2): + only_bad_spectra[f"spectrum-{i}, {mwl}"] = fluo_dict["sample"]["spectrum", i] + out3 = sc.plot( + only_bad_spectra, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size, + title=f"{name} - all bad raw spectra", + ) + display(out3) + + only_good_spectra = {} # make empty dict + for i in range(1, fluo_dict["sample"].sizes["spectrum"], 2): + only_good_spectra[f"spectrum-{i}, {mwl}"] = fluo_dict["sample"]["spectrum", i] + out4 = sc.plot( + only_good_spectra, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size, + title=f"{name} - all good raw spectra", + ) + display(out4) + + # plot only good spectra with wavelength in legend name + only_good_fspectra = {} # make empty dict + for i in range(1, fluo_dict["sample"].sizes["spectrum"], 2): + mwl = str(final_fluo.coords["monowavelengths"][i].value) + "nm" + only_good_fspectra[f"spectrum-{i}, {mwl}"] = final_fluo["spectrum", i] + out5 = sc.plot( + only_good_fspectra, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size, + title=f"{name} - all good final spectra", + ) + display(out5) + + # zoom in final spectra selection + out6 = sc.plot( + only_good_fspectra, + linestyle="dashed", + grid=True, + marker='.', + legend=legend_props, + figsize=figure_size, + title=f"{name} - all good final spectra", + ) + lambda_min = 250 + lambda_max = 600 + out6.ax.set_xlim(lambda_min, lambda_max) + display(out6) + + fig_handles = [out1, out2, out3, out4, out5, out5, out6] + #fig_handles=[out3] + modify_plt_app(fig_handles) + + +def fluo_plot_maxint_max_wavelen(fluo_int_dict: dict): + """Plots for fluorescence maximum intensity and corressponding wavelength as function of pertubation. + Pertubation parameter: currently filename as unique identifier + + Parameters + ---------- + fluo_int_dict: dict + Contains per found monowavelength and per measurement the value of the max fluo intensity for a selected wavelength range and the corresponding maximum wavelength. + + Returns + ---------- + + """ + + # separate information + max_int_dict = {} + max_int_wavelen_dict = {} + + monowavelen = fluo_int_dict.keys() + + # prepare the plots + monowavelen = len(fluo_int_dict.keys()) + print("Number of keys", monowavelen) + + fig, ax = plt.subplots(nrows=monowavelen, ncols=2, figsize=(10, 7)) + plt.subplots_adjust( + left=None, bottom=0.05, right=None, top=0.95, wspace=0.2, hspace=0.5 + ) + + for j, mwl in enumerate(fluo_int_dict): + + for name in fluo_int_dict[mwl]: + max_int_dict[name] = fluo_int_dict[mwl][name]["intensity_max"] + max_int_wavelen_dict[name] = fluo_int_dict[mwl][name]["wavelength_max"] + + for i, fname in enumerate(max_int_dict): + + x_val = [i] * len(max_int_dict[fname]) + ax[j, 0].set_title(f"monowavelength: {mwl}: max. intensity") + ax[j, 0].plot(x_val, max_int_dict[fname], marker="o", linestyle="None") + ax[j, 0].grid(True) + ax[j, 0].set_ylabel("Max. peak intensity [counts]") + print(mwl, fname, "max int", max_int_dict[fname]) + + ax[j, 1].set_title(f"monowavelength: {mwl}: wavelength") + ax[j, 1].plot( + x_val, max_int_wavelen_dict[fname], marker="o", linestyle="None" + ) + ax[j, 1].set_ylabel("Peak wavelength [nm]") + ax[j, 1].grid(True) + # ax[1].set_ylabel(f'Wavelength [{unit_str}]') + # ax[j,1].set_ylim(bottom=0.95*int(mwl[:-4])) + + ax[j, 1].plot( + np.linspace( + 0, + len(max_int_wavelen_dict.keys()) - 1, + num=len(max_int_wavelen_dict.keys()), + ), + [int(mwl[:-4])] * len(max_int_wavelen_dict.keys()), + "--", + ) + # overwrite xticks in ax0 + ax[j, 0].set_xticks( + np.arange(0, len(max_int_dict.keys()), 1), + labels=[f"{name}" for name in max_int_dict], + rotation=90, + ) + # overwrite xticks in ax1 + ax[j, 1].set_xticks( + np.arange(0, len(max_int_wavelen_dict.keys()), 1), + labels=[f"{name}" for name in max_int_wavelen_dict], + rotation=90, + ) + + diff --git a/src/ess/loki/nurf/scipp_947.ipynb b/src/ess/loki/nurf/scipp_947.ipynb new file mode 100644 index 000000000..6970753af --- /dev/null +++ b/src/ess/loki/nurf/scipp_947.ipynb @@ -0,0 +1,1378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Demonstration of NUrF function module\n", + "\n", + "The Nurf Python module contains functions to extract UV and fluo data from a LoKI.nxs file.
\n", + "It is capable of correcting the measured spectra for reference and dark current. It calculates the final fluo and UV spectra and plots them. \n", + "A median filter can be applied to the spectra.
\n", + "For UV the following parameters can be extracted:
\n", + "- Absorbance at 280nm\n", + "- Turbidity and m, b factor\n", + "\n", + "For fluo the following parameters can be extracted:
\n", + "- Maximum peak intensity\n", + "- Peak position corresponding to the maximum peak intensity\n", + "\n", + "Besides standard Python libraries like matplotlib, os, scipy, and numpy, it is relies on scipp, scippnexus, and scippneutron. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Prepare the stage and load the required tools." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from nurf import *\n", + "from utils import *\n", + "from ill_auxilliary_funcs import *\n", + "from scipp.signal import butter, sosfiltfilt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The code below uses UV and fluo data that was colleced in a multi-modal SANS, UV, Fluo experiment at ILL. One experiment is composed of a series of measurement. In each measurement, UV and fluo spectra were recorded. Alongside the sample, dark and reference measurements were recorded. Here, exp5 and exp6, exp2 and exp3, exp7 and exp8 form complementary datasets. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Provide information on where the relevant files are and which files belong to which experimental datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Prepare for export to .dat for uv and fluo\n", + "\n", + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "# change to folder\n", + "os.chdir(process_folder)\n", + "\n", + "# export path for .dat files\n", + "path_output='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version/dat-files'\n", + "\n", + "# experimental data sets\n", + "exp5= [66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "\n", + "exp2= [65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "exp7= [66083, 66086, 66089, 66092, 66095, 66098, 66101, 66104, 66107, 66110, 66113]\n", + "exp8= [66116, 66119, 66122, 66125, 66128, 66131, 66134, 66137, 66140, 66143, 66146]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The cell below shows how to export from LoKI.nxs to a generic Ascii file .dat if the users would like to use .dat files." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None,\n", + " None]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List comprehensions for export to .dat\n", + "all_data=[exp2, exp3, exp5, exp6, exp7, exp8]\n", + "\n", + "# Create one list only with all entries of the exp sets\n", + "all_data_flat = sum(all_data, [])\n", + "\n", + "# convert numbers to file names\n", + "all_file_names = [complete_fname(i) for i in all_data_flat]\n", + "\n", + "# export uv data for all_data to .dat\n", + "[export_uv(m, path_output) for m in all_file_names]\n", + "\n", + "#export fluo data for all_data to .dat\n", + "[export_fluo(m, path_output) for m in all_file_names]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting of UV spectra for exp5 and exp6 " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a8089f1325b94ce0be9a48784c59eb51", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(VBox(children=(Button(icon='home', layout=Layout(padding='0px 0px 0px 0px', widt…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "18cafe2c81d94a998b34d334e4aa41ed", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(VBox(children=(Button(icon='home', layout=Layout(padding='0px 0px 0px 0px', widt…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5172eb0a8a8d4548a21072be9481cd9a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(VBox(children=(Button(icon='home', layout=Layout(padding='0px 0px 0px 0px', widt…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4ae6270c93cd4f5c9936aef130ce3b82", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(VBox(children=(Button(icon='home', layout=Layout(padding='0px 0px 0px 0px', widt…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "406388fb6dbb43859e11c234c0c5d9f6", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot two uv sets\n", + "\n", + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "# exp 5 and exp 6\n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "\n", + "# Create filenames \n", + "flist_num=complete_fname(exp5)\n", + "\n", + "# Plotting exp5\n", + "main_title='Exp 5'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-1, vmax=6)\n", + "pl2.ax.set_title(main_title)\n", + "#modify plots\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "\n", + "#Example to plot one UV file. \n", + "plot_uv('066017.nxs')\n", + "\n", + "# We can gather all normalized uv data in a sc.Dataset\n", + "uv_exp5_set=gather_uv_set(flist_num)\n", + "\n", + "\n", + "# Repeating all steps above for exp6.\n", + "flist_num=complete_fname(exp6)\n", + "main_title='Exp 6'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "plt.tight_layout()\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-1, vmax=6)\n", + "pl2.ax.set_title(main_title)\n", + "plt.tight_layout()\n", + "#modify plots\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "uv_exp6_set=gather_uv_set(flist_num)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Back to some basic code functionality \n", + "This cell shows how to load a Loki.nxs files, what is returned and how to separate the components in the .nxs file (sample, darf, reference)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "name='066017.nxs'\n", + "with snx.File(name) as f:\n", + " uv = f[\"entry/instrument/uv\"][()]\n", + "\n", + "print('Output of .nxs is of type ', type(uv))\n", + "display(uv)\n", + "da=uv\n", + "dark = da[da.coords[\"is_dark\"]].squeeze()\n", + "ref = da[da.coords[\"is_reference\"]].squeeze()\n", + "sample = da[da.coords[\"is_data\"]] \n", + "\n", + "\n", + "print('Sample is', type(sample))\n", + "display(sample)\n", + "print('Dark is ', type(dark))\n", + "display(dark)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot fluo data for two single measurements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "# Plot two fluo examples\n", + "name='066017.nxs' #exp5\n", + "#name='066050.nxs' #exp6\n", + "#name='066053.nxs' #exp6\n", + "#name='066083.nxs' #exp7\n", + "#name='066116.nxs' #exp8 \n", + "#name='065925.nxs' #exp2\n", + "#name='065962.nxs' #exp3\n", + "plot_fluo(name)\n", + "\n", + "exp2=[65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "# converts numbers to full filenames\n", + "#flist_num=complete_fname(exp2)\n", + "\n", + "name='065925.nxs'\n", + "plot_fluo(name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot UV data for exp7 and exp8" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "exp7= [66083, 66086, 66089, 66092, 66095, 66098, 66101, 66104, 66107, 66110, 66113]\n", + "exp8= [66116, 66119, 66122, 66125, 66128, 66131, 66134, 66137, 66140, 66143, 66146]\n", + "\n", + "\n", + "flist_num=complete_fname(exp7)\n", + "main_title='Exp 7'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-1, vmax=7)\n", + "pl2.ax.set_title(main_title)\n", + "#modify plots,\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "\n", + "\n", + "uv_exp7_set=gather_uv_set(flist_num)\n", + "\n", + "flist_num=complete_fname(exp8)\n", + "main_title='Exp 8'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-1, vmax=10)\n", + "pl2.ax.set_title(main_title)\n", + "#modify plots\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "\n", + "\n", + "uv_exp8_set=gather_uv_set(flist_num)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot UV data for exp2 and exp3. In addition UV for two single measurements are shown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exp2=[65925, 65927, 65930, 65933, 65936, 65939, 65942, 65945, 65948, 65951, 65954, 65957]\n", + "exp3= [65962, 65965, 65968, 65971, 65974, 65977, 65980, 65983, 65986, 65989, 65992]\n", + "\n", + "\n", + "flist_num=complete_fname(exp2)\n", + "main_title='Exp 2'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-2, vmax=4)\n", + "pl2.ax.set_title(main_title)\n", + "#modify plots\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "\n", + "\n", + "uv_exp2_set=gather_uv_set(flist_num)\n", + "\n", + "name_s='065957.nxs'\n", + "plot_uv(name_s)\n", + "name_s='065925.nxs'\n", + "plot_uv(name_s)\n", + "\n", + "\n", + "flist_num=complete_fname(exp3)\n", + "main_title='Exp 3'\n", + "pl1=plot_uv_set(flist_num, lambda_min=None, lambda_max=None, vmin=None, vmax=None)\n", + "pl1.ax.set_title(main_title)\n", + "\n", + "\n", + "pl2=plot_uv_set(flist_num, lambda_min=200,lambda_max=400, vmin=-2, vmax=5)\n", + "pl2.ax.set_title(main_title)\n", + "#modify plots\n", + "fig_handles=[pl1, pl2]\n", + "modify_plt_app(fig_handles)\n", + "\n", + "\n", + "uv_exp3_set=gather_uv_set(flist_num)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Exploring filter possibilities\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Explore median filter\n", + "\n", + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "name='066017.nxs' #exp5\n", + "#name='066050.nxs' #exp6\n", + "#name='066053.nxs' #exp6\n", + "#name='066083.nxs' #exp7\n", + "#name='066116.nxs' #exp8 \n", + "#name='065925.nxs' #exp2\n", + "#name='065962.nxs' #exp3\n", + "\n", + "#plot_fluo(name)\n", + "\n", + "\n", + "fluo_dict=load_fluo(name)\n", + "fluo_da=normalize_fluo(**fluo_dict)\n", + "\n", + "display(fluo_da)\n", + "\n", + "spectra_number=7 \n", + "\n", + "def explore_medfilt(name, spectra_number=0, lmin=250, lmax=500):\n", + "\n", + " #load fluo data\n", + " fluo_dict=load_fluo(name)\n", + " fluo_da=normalize_fluo(**fluo_dict)\n", + "\n", + " # prepare a curve\n", + " yfluo=fluo_da['spectrum',spectra_number].values\n", + " xfluo=fluo_da.coords['wavelength'].values\n", + " \n", + " #slicing values\n", + " # lidx\n", + " lidx=np.where(np.logical_and((xfluo>=lmin), (xfluo<=lmax)))\n", + " xfluo_filt=xfluo[lidx]\n", + " yfluo_filt=yfluo[lidx]\n", + "\n", + " nsp=3\n", + " fig, ax = plt.subplots(1,nsp, figsize=(30,7))\n", + "\n", + "\n", + " ax[0].plot(xfluo,yfluo)\n", + " #yes, we want the zero line\n", + " ax[1].plot(xfluo,yfluo-yfluo)\n", + " ax[2].plot(xfluo_filt, yfluo_filt)\n", + " #example medfilt\n", + " kernel=[]\n", + " kernel_range=range(3,20,2)\n", + "\n", + "\n", + " for i in kernel_range:\n", + "\n", + " yfluomed=medfilt(yfluo, i)\n", + " ax[0].plot(xfluo, yfluomed)\n", + " ax[1].plot(xfluo,yfluo-yfluomed)\n", + " kernel.append(str(i))\n", + "\n", + " # zommed in\n", + " yfluomed_filt=medfilt(yfluo_filt, i)\n", + " ax[2].plot(xfluo_filt, yfluomed_filt)\n", + "\n", + " \n", + " colors=line_colors(len(kernel_range)+1)\n", + " #print('Colors contains,' ,len(colors))\n", + " ax[0].set_ylabel('Fluo intensity')\n", + " ax[1].set_ylabel('Difference between medfilter and signal')\n", + " ax[2].set_ylabel('Fluo intensity')\n", + "\n", + " title_string=f\"Influence of a median filter, {name}, spectrum {str(spectra_number)}\"\n", + "\n", + " for m in range(0,nsp):\n", + " ax[m].legend(['None', *kernel])\n", + " ax[m].set_xlabel('Wavelength [nm]')\n", + " ax[m].set_title(title_string)\n", + " ax[m].grid()\n", + " [ax[m].get_lines()[i].set_color(colors[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_legend().legendHandles[i].set_color(colors[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_marker(markers()[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_markersize(2) for i in range(0,len(kernel_range)+1)]\n", + " #[ax[m].get_lines()[i].set_markevery(5) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_legend().legendHandles[i].set_marker(markers()[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_linewidth(1) for i in range(0,len(kernel_range)+1)]\n", + "\n", + "\n", + "explore_medfilt(name, spectra_number=7, lmin=300, lmax=400)\n", + "\n", + "\n", + "# scipy.signal.medfilt not yet supported by scipp\n", + "# Simon suggests to check out\n", + "#https://scipp.github.io/generated/modules/scipp.signal.html\n", + "\n", + "\n", + "\n", + "# Check if fluo wavelength is regular. But it is not regular. \n", + "# Calculate the difference between two adjacent wavelength entries \n", + "xw=fluo_da.coords['wavelength'].values\n", + "xdiff = [xw[n]-xw[n-1] for n in range(1,len(xw))]\n", + "#print(xdiff)\n", + "\n", + "\n", + "def explore_butter(name, spectra_number=0, lmin=250, lmax=500):\n", + " from scipp.signal import butter, sosfiltfilt\n", + "\n", + " #load fluo data\n", + " fluo_dict=load_fluo(name)\n", + " fluo_da=normalize_fluo(**fluo_dict)\n", + "\n", + " # prepare a curve\n", + " yfluo=fluo_da['spectrum',spectra_number].values\n", + " x = fluo_da.coords['wavelength']\n", + " # conversion to float64 due to a bug\n", + " fluo_da.coords['wavelength'] = sc.linspace(x.dim, x.values[0], x.values[-1], num=len(x), unit=x.unit, dtype='float64')\n", + " #out=butter(fluo_da.coords['wavelength'], N=12, Wn=0.04/ x.unit).filtfilt(fluo_da.data, 'wavelength') #this applies the butter filter to the whole fluo dataarray\n", + " #out=butter(fluo_da['spectrum',spectra_number].coords['wavelength'], N=12, Wn=0.04/ x.unit).filtfilt(fluo_da['spectrum',spectra_number].data, 'wavelength') # I want to apply the butter filter only to one spectrum\n", + "\n", + " #slicing values\n", + " # lidx\n", + " lidx=np.where(np.logical_and((fluo_da.coords['wavelength'].values>=lmin), (fluo_da.coords['wavelength'].values<=lmax)))\n", + " xfluo_filt=x.values[lidx]\n", + " yfluo_filt=yfluo[lidx]\n", + "\n", + "\n", + " \n", + " # butter parameters\n", + " Wn_range=[0.5,0.3,0.04]\n", + " N_range=[12,4]\n", + " \n", + " nsp=3\n", + " fig, ax = plt.subplots(1,nsp, figsize=(30,7))\n", + " ax[0].plot(x.values,fluo_da['spectrum',spectra_number].values)\n", + " ax[1].plot(x.values, fluo_da['spectrum',spectra_number].values-fluo_da['spectrum',spectra_number].values)\n", + " ax[2].plot(xfluo_filt,yfluo_filt)\n", + "\n", + " ax[0].set_ylabel('Fluo intensity')\n", + " ax[2].set_ylabel('Fluo intensity')\n", + " ax[1].set_ylabel('Difference between butter and signal')\n", + "\n", + " para_list=[]\n", + " for i in Wn_range:\n", + " for m in N_range:\n", + " out=butter(fluo_da['spectrum',spectra_number].coords['wavelength'], N=m, Wn=i/ x.unit).filtfilt(fluo_da['spectrum',spectra_number].data, 'wavelength') \n", + " ax[0].plot(x.values, out.values)\n", + " para_out=[m,i]\n", + " para_list.append(para_out)\n", + " ax[1].plot(x.values,fluo_da['spectrum',spectra_number].values-out.values)\n", + " ax[2].plot(xfluo_filt,out.values[lidx])\n", + "\n", + " kernel_length=len(Wn_range)*len(N_range)\n", + " kernel_range=para_list\n", + "\n", + " #ax[0].plot(x.values, out['spectrum',spectra_number].values)\n", + " #ax[0].plot(x.values, out.values)\n", + "\n", + " title_string=f\"Influence of a butter filter, {name}, spectrum {str(spectra_number)}\"\n", + "\n", + "\n", + " colors=line_colors(len(kernel_range)+1)\n", + " for m in range(0,nsp):\n", + " ax[m].legend(['None', *kernel_range])\n", + " ax[m].set_xlabel('Wavelength [nm]')\n", + " ax[m].set_title(title_string)\n", + " ax[m].grid()\n", + " [ax[m].get_lines()[i].set_color(colors[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_legend().legendHandles[i].set_color(colors[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_marker(markers()[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_markersize(2) for i in range(0,len(kernel_range)+1)]\n", + " #[ax[m].get_lines()[i].set_markevery(5) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_legend().legendHandles[i].set_marker(markers()[i]) for i in range(0,len(kernel_range)+1)]\n", + " [ax[m].get_lines()[i].set_linewidth(1) for i in range(0,len(kernel_range)+1)]\n", + "\n", + "\n", + "\n", + "explore_butter(name, spectra_number=7, lmin=300, lmax=400)\n", + "\n", + "\n", + "# there is a bug, hence conversion to float64\n", + "#fluo_da.coords['wavelength'] = fluo_da.coords.pop('wavelength').to(dtype='float64')\n", + "#out = butter(fluo_da.coords['wavelength'], N=4, Wn=20 / fluo_da.coords['wavelength'].unit).filtfilt(fluo_da,'wavelength')\n", + "# Above line fails currently, because the coords for wavelength are not regular\n", + "\n", + "#Simon suggestion\n", + "\n", + "#x = fluo_da.coords['wavelength']\n", + "#fluo_da.coords['wavelength'] = sc.linspace(x.dim, x.values[0], x.values[-1], num=len(x), unit=x.unit, dtype='float64')\n", + "#out=butter(fluo_da.coords['wavelength'], N=12, Wn=0.04/ x.unit).filtfilt(fluo_da.data, 'wavelength')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Developing functions for a data analysis pipeline for UV, fluo spectroscopy\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "name='065992.nxs'\n", + "#load uv data\n", + "uv_dict=load_uv(name)\n", + "#display(uv_dict['data'],uv_dict['reference'],uv_dict['dark'])\n", + "uv_da=normalize_uv(**uv_dict) #returns sc.DataArray with all uv spectra present in file\n", + "#display(uv_da)\n", + "\n", + "plot_uv(name)\n", + "uv_turbidity_fit(uv_da, wl_unit=sc.Unit('nm'), fit_llim=500, fit_ulim=850, b_llim=300, b_ulim=400,m=0.1, plot_corrections=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### UV pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "\"\"\" What functions are available for uv data? Overview on only the function names\n", + "#How to apply a medilter to fluo or uv data? \n", + "apply_medfilter\n", + "\n", + "#This is for uv\n", + "#Extract uv peak intensity\n", + "uv_peak_int\n", + "\n", + "#Plot uv peak intensity\n", + "plot_uv_peak_int\n", + "\n", + "#Quickly check uv data\n", + "uv_quick_data_check\n", + "\n", + "#plot mulitple uv peak int\n", + "plot_multiple_uv_peak_int\n", + "\n", + "#turbidity\n", + "turbidity\n", + "\n", + "#residuals\n", + "residual\n", + "\n", + "#UV turbidity fit\n", + "uv_turbidity_fit\n", + "\n", + "#multi turbidity fit\n", + "multi_uv_turbidity_fit\n", + "\"\"\"\n", + "\n", + "#%%%%%%%%%%%%%%%%%%%%%%%%%%% Code examples %%%%%%%%%%%%%%%%%%%%%%%%%%\n", + "\n", + "name='066017.nxs'\n", + "#load uv data\n", + "uv_dict=load_uv(name)\n", + "#display(uv_dict['data'],uv_dict['reference'],uv_dict['dark'])\n", + "uv_da=normalize_uv(**uv_dict) #returns sc.DataArray with all uv spectra present in a LoKI.nxs file\n", + "#display(uv_da)\n", + "\n", + "#How to apply a medfilter to a uv dataarray\n", + "#apply_medfilter(uv_da,kernel_size=9) #applies median filter to multiple spectra in uv_da\n", + "#apply_medfilter(process_uv(name),kernel_size=9) #applies median filter to normalised spectra resulting from process_uv\n", + "\n", + "# Plots uv peak intensity for a given nexus file\n", + "uv_280_result=uv_peak_int(uv_da, wavelength=None, wl_unit=sc.Unit('nm'), tol=None)\n", + "plot_uv_peak_int(uv_da, name, wavelength=None, wl_unit=sc.Unit('nm'), tol=1)\n", + "\n", + "# exp 5 and exp 6\n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "#exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "\n", + "# Create complete filenames \n", + "#flist_num=complete_fname(exp5)\n", + "#flist_num=complete_fname(exp6)\n", + "\n", + "# a quic visualisation check if all files have equivalent number of spectra\n", + "#uv_quick_data_check(flist_num, wavelength=None, wl_unit=None, tol=None, medfilter=False, kernel_size=None)\n", + "\n", + "# select peak intensities at 280nm and plot them\n", + "plot_multiple_uv_peak_int(flist_num, wavelength=280, wl_unit=None, tol=None, medfilter=True, kernel_size=None)\n", + "\n", + "# Plot the spectra in a single LoKI.nxs file\n", + "#plot_uv('066020.nxs')\n", + "\n", + "# Plot multiple UV spectra for a given set of files\n", + "#plot_uv_set(flist_num)\n", + "\n", + "# Example for tubidity\n", + "#uv_turbidity_fit(uv_da, wl_unit=sc.Unit('nm'), fit_llim=300, fit_ulim=850, b_llim=450, b_ulim=700,m=0.1, plot_corrections=True)\n", + "\n", + "# Performs for a given set of files turbidity correction\n", + "uv_da_multi=uv_multi_turbidity_fit(flist_num, wl_unit=sc.Unit('nm'), fit_llim=300, fit_ulim=850, b_llim=450, b_ulim=700,m=0.1, plot_corrections=True)\n", + "\n", + "\n", + "\n", + "\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Fluo pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# path to LOKI-like files\n", + "process_folder='/Users/gudlo523/Library/CloudStorage/OneDrive-LundUniversity/UU/ILL_947_June_2021/212/d22/exp_9-13-947/processed/ess_version'\n", + "os.chdir(process_folder)\n", + "\n", + "\"\"\" What functions are available for fluo data? Overview on only the function names\n", + "#How to apply a medilter to fluo or uv data? \n", + "apply_medfilter\n", + "\n", + "#Fluo peak intensity\n", + "fluo_peak_int\n", + "\n", + "#Plot fluo peak intensity\n", + "plot_fluo_peak_int\n", + "\n", + "#Plot multiple peak intensities\n", + "plot_fluo_multiple_peak_int\n", + "\"\"\"\n", + "\n", + "#Example of how to load one nexusfile and extract the fluo dataarray\n", + "#name='066017.nxs' #exp5\n", + "#fluo_dict=load_fluo(name)\n", + "#fluo_da=normalize_fluo(**fluo_dict)\n", + "#display(fluo_da) \n", + "\n", + "\n", + "# Example of how to apply the med filter to a fluo dataarray fluo_da\n", + "#apply_medfilter(fluo_da,kernel_size=9)\n", + "#apply_medfilter(fluo_da['spectrum', 7:9], kernel_size=9)\n", + "#apply_medfilter(fluo_da['spectrum', 7], kernel_size=9) \n", + "#plot_fluo(name)\n", + "#fluo_filt_max=fluo_peak_int(fluo_da, wllim=None, wulim=None, wl_unit=None, medfilter=True, kernel_size=15)\n", + "#display(fluo_filt_max)\n", + "#plot_fluo_peak_int(fluo_da,name, wllim=300, wulim=400, wl_unit=None, medfilter=True, kernel_size=15)\n", + "\n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "flist_num5=complete_fname(exp5)\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "flist_num6=complete_fname(exp6)\n", + "\n", + "\n", + "def plot_fluo_multiple_peak_int(filelist, wllim=None, wulim=None, wl_unit=None, medfilter=True, kernel_size=None):\n", + " \"\"\" Plots multiple peak intensities for fluo spectr\n", + " \n", + " \"\"\"\n", + " import itertools\n", + " marker = itertools.cycle(markers()) \n", + "\n", + "\n", + " print(filelist)\n", + "\n", + " figure_size=(15,5)\n", + " fig, ax=plt.subplots(nrows=1,ncols=2,figsize=figure_size, constrained_layout=True)\n", + "\n", + " \n", + " unique_mwl=[]\n", + " ds_list=[]\n", + " for name in filelist:\n", + " fluo_dict=load_fluo(name)\n", + " fluo_da=normalize_fluo(**fluo_dict)\n", + " #extract max int value and corresponding wavelength position, median filter is applied\n", + " fluo_filt_max=fluo_peak_int(fluo_da, wllim=wllim, wulim=wulim, wl_unit=wl_unit, medfilter=medfilter, kernel_size=kernel_size) \n", + " # attach filename as attribute to dataarray\n", + " #fluo_filt_max.attrs['filename'] = sc.scalar(name)\n", + " #display(fluo_filt_max)\n", + " ds_list.append(fluo_filt_max)\n", + " unique_mwl.append(np.unique(fluo_filt_max.coords['monowavelengths'].values))\n", + " #print(fluo_filt_max)\n", + "\n", + " #same marker for both plots for the same file\n", + " markerchoice=next(marker)\n", + "\n", + " ax[0].plot(fluo_filt_max.coords['monowavelengths'].values, fluo_filt_max['intensity_max'].values, label=f'{name}', linestyle=\"None\", marker=markerchoice, markersize=10)\n", + " ax[0].set_ylabel('Max. Intensity')\n", + " ax[0].set_title('Fluo - max. intensity')\n", + "\n", + " ax[1].plot(fluo_filt_max.coords['monowavelengths'].values, fluo_filt_max['wavelength_max'].values, label=f'{name}', linestyle=\"None\", marker=markerchoice, markersize=10)\n", + " unit_str=str(fluo_filt_max['wavelength_max'].unit)\n", + "\n", + " ax[1].set_ylabel(f'Wavelength [{unit_str}]')\n", + " ax[1].set_title(f'Fluo - corresponding wavelength')\n", + "\n", + " # show the lowest monowavelength as lower boundary on the y-axis\n", + " ax[1].set_ylim(bottom=0.9*np.min(fluo_filt_max.coords['monowavelengths'].values))\n", + "\n", + " # plot the found monowavelengths as additional visual information on the y-axis \n", + " for mwl in np.unique(unique_mwl):\n", + " ax[1].plot(np.unique(unique_mwl), np.full(np.shape(np.unique(unique_mwl)), mwl) , '--', label=f\"{mwl}{sc.Unit('nm')}\")\n", + " #ax[1].legend(loc='upper right', bbox_to_anchor=(1.05, 1.05)) \n", + " \n", + " for axes in ax:\n", + " #axes.legend(loc='upper right', bbox_to_anchor=(1.1, 1.00)) \n", + " axes.legend( bbox_to_anchor=(1.04, 1)) \n", + " axes.grid(True)\n", + " axes.set_xlabel('Monowavelengths')\n", + " \n", + " \n", + " display(fig)\n", + "\n", + "plot_fluo_multiple_peak_int(flist_num5,wllim=300, wulim=400, wl_unit=None, medfilter=True, kernel_size=15)\n", + "plot_fluo_multiple_peak_int(flist_num6,wllim=300, wulim=400, wl_unit=None, medfilter=True, kernel_size=15)\n", + "\n", + "\n", + "#If I understood scipp correctly, I can only have a Dataset, where each DataArray is the same. So if one DataArray has 2 spectra, one DataArray only one, it should not be possible to put them together in a Dataset.\n", + "#Correct. But you could have an 1-D data array and a 2-D data array in the same dataset, if they have the same length along the shared dimension.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#How to access for a given filename the fluo dataarray.\n", + "name='066017.nxs'\n", + "fluo_dict=load_fluo(name)\n", + "fluo_da=normalize_fluo(**fluo_dict)\n", + "display(fluo_da)\n", + "print(f'Number of fluo spectra in {name}: {fluo_da.sizes[\"spectrum\"]}' )\n", + "\n", + "#apply medfilter to fluo sc.DataArray\n", + "fluo_da_filt=apply_medfilter(fluo_da, kernel_size=15)\n", + "\n", + "#showcasing scipp dataarray graphical presentation\n", + "sc.show(fluo_da_filt)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#### Extract from fluorescence measurements the maximum intensity and the corresponding wavelength.\n", + "\n", + "#Fluo Max int, max wavelength\n", + "\n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "flist_num=complete_fname(exp5)\n", + "\n", + "fluo_int_dict=fluo_maxint_max_wavelen(flist_num,wllim=300, wulim=400, wl_unit=None, medfilter=True, kernel_size=15)\n", + "\n", + "fluo_plot_maxint_max_wavelen(fluo_int_dict) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot a selected fluo spectrum across a series of measurements" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Here we plot a certain fluo spectrum for all measurements in a whole experimental series. Here spectrum #1 corresponds to the white light spectrum.\n", + "\n", + "exp5=[66017, 66020, 66023, 66026, 66029, 66032, 66034, 66037, 66040, 66043, 66046]\n", + "flist_num5=complete_fname(exp5)\n", + "exp6= [66050, 66053, 66056, 66059, 66062, 66065, 66068, 66071, 66074, 66077, 66080]\n", + "flist_num6=complete_fname(exp6)\n", + "\n", + "fwls=plot_fluo_spectrum_selection(flist_num5, spectral_idx=1, kernel_size=15, wllim=400, wulim=500)\n", + "fwls=plot_fluo_spectrum_selection(flist_num6, spectral_idx=1, kernel_size=15, wllim=400, wulim=500)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Testing ground" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (42.78 KB)
    • spectrum: 2
    • wavelength: 3648
    • integration_time
      (spectrum)
      int32
      µs
      4000, 4000
      Values:
      array([4000, 4000], dtype=int32)
    • is_dark
      (spectrum)
      bool
      False, False
      Values:
      array([False, False])
    • is_data
      (spectrum)
      bool
      True, True
      Values:
      array([ True, True])
    • is_reference
      (spectrum)
      bool
      False, False
      Values:
      array([False, False])
    • time
      (spectrum)
      int64
      1655992257000000, 1655992257000000
      Values:
      array([1655992257000000, 1655992257000000])
    • wavelength
      (wavelength)
      float32
      nm
      195.67415, 195.93758, ..., 1047.7314, 1047.9298
      Values:
      array([ 195.67415, 195.93758, 196.20097, ..., 1047.5331 , 1047.7314 ,\n", + " 1047.9298 ], dtype=float32)
    • (spectrum, wavelength)
      float32
      counts
      0.82835215, 0.8393088, ..., 0.8570749, 0.8570749
      Values:
      array([[0.82835215, 0.8393088 , 0.82886416, ..., 0.84888303, 0.84888303,\n", + " 0.84888303],\n", + " [0.83761925, 0.83572483, 0.83613443, ..., 0.8570749 , 0.8570749 ,\n", + " 0.8570749 ]], dtype=float32)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[spectrum:2, wavelength:3648, ]\n", + "Coordinates:\n", + " integration_time int32 [µs] (spectrum) [4000, 4000]\n", + " is_dark bool (spectrum) [False, False]\n", + " is_data bool (spectrum) [True, True]\n", + " is_reference bool (spectrum) [False, False]\n", + " time int64 (spectrum) [1655992257000000, 1655992257000000]\n", + " wavelength float32 [nm] (wavelength) [195.674, 195.938, ..., 1047.73, 1047.93]\n", + "Data:\n", + " float32 [counts] (spectrum, wavelength) [0.828352, 0.839309, ..., 0.857075, 0.857075]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (28.51 KB)
    • wavelength: 3648
    • wavelength
      (wavelength)
      float32
      nm
      195.67415, 195.93758, ..., 1047.7314, 1047.9298
      Values:
      array([ 195.67415, 195.93758, 196.20097, ..., 1047.5331 , 1047.7314 ,\n", + " 1047.9298 ], dtype=float32)
    • (wavelength)
      float32
      counts
      0.82891536, 0.82763535, ..., 0.83552, 0.83552
      Values:
      array([0.82891536, 0.82763535, 0.8308609 , ..., 0.83552 , 0.83552 ,\n", + " 0.83552 ], dtype=float32)
    • integration_time
      ()
      int32
      µs
      4000
      Values:
      array(4000, dtype=int32)
    • is_dark
      ()
      bool
      True
      Values:
      array(True)
    • is_data
      ()
      bool
      False
      Values:
      array(False)
    • is_reference
      ()
      bool
      False
      Values:
      array(False)
    • time
      ()
      int64
      1655992257000000
      Values:
      array(1655992257000000)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[wavelength:3648, ]\n", + "Coordinates:\n", + " wavelength float32 [nm] (wavelength) [195.674, 195.938, ..., 1047.73, 1047.93]\n", + "Data:\n", + " float32 [counts] (wavelength) [0.828915, 0.827635, ..., 0.83552, 0.83552]\n", + "Attributes:\n", + " integration_time int32 [µs] () [4000]\n", + " is_dark bool () [True]\n", + " is_data bool () [False]\n", + " is_reference bool () [False]\n", + " time int64 () [1655992257000000]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (28.51 KB)
    • wavelength: 3648
    • wavelength
      (wavelength)
      float32
      nm
      195.67415, 195.93758, ..., 1047.7314, 1047.9298
      Values:
      array([ 195.67415, 195.93758, 196.20097, ..., 1047.5331 , 1047.7314 ,\n", + " 1047.9298 ], dtype=float32)
    • (wavelength)
      float32
      counts
      0.8299905, 0.8312705, ..., 0.84944624, 0.84944624
      Values:
      array([0.8299905 , 0.8312705 , 0.82753295, ..., 0.84944624, 0.84944624,\n", + " 0.84944624], dtype=float32)
    • integration_time
      ()
      int32
      µs
      4000
      Values:
      array(4000, dtype=int32)
    • is_dark
      ()
      bool
      False
      Values:
      array(False)
    • is_data
      ()
      bool
      False
      Values:
      array(False)
    • is_reference
      ()
      bool
      True
      Values:
      array(True)
    • time
      ()
      int64
      1655992257000000
      Values:
      array(1655992257000000)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[wavelength:3648, ]\n", + "Coordinates:\n", + " wavelength float32 [nm] (wavelength) [195.674, 195.938, ..., 1047.73, 1047.93]\n", + "Data:\n", + " float32 [counts] (wavelength) [0.829991, 0.831271, ..., 0.849446, 0.849446]\n", + "Attributes:\n", + " integration_time int32 [µs] () [4000]\n", + " is_dark bool () [False]\n", + " is_data bool () [False]\n", + " is_reference bool () [True]\n", + " time int64 () [1655992257000000]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (53.24 KB)
    • spectrum: 12
    • wavelength: 1044
    • integration_time
      (spectrum)
      float32
      µs
      50000000.0, 50000000.0, ..., 50000000.0, 50000000.0
      Values:
      array([50000000., 50000000., 50000000., 50000000., 50000000., 50000000.,\n", + " 50000000., 50000000., 50000000., 50000000., 50000000., 50000000.],\n", + " dtype=float32)
    • is_dark
      (spectrum)
      bool
      False, False, ..., False, False
      Values:
      array([False, False, False, False, False, False, False, False, False,\n", + " False, False, False])
    • is_data
      (spectrum)
      bool
      True, True, ..., True, True
      Values:
      array([ True, True, True, True, True, True, True, True, True,\n", + " True, True, True])
    • is_reference
      (spectrum)
      bool
      False, False, ..., False, False
      Values:
      array([False, False, False, False, False, False, False, False, False,\n", + " False, False, False])
    • monowavelengths
      (spectrum)
      float32
      nm
      295.0, 295.0, ..., 320.0, 320.0
      Values:
      array([295., 295., 295., 295., 320., 320., 320., 320., 295., 295., 320.,\n", + " 320.], dtype=float32)
    • time
      (spectrum)
      int64
      µs
      1655992257000000, 1655992257000000, ..., 1655992257000000, 1655992257000000
      Values:
      array([1655992257000000, 1655992257000000, 1655992257000000,\n", + " 1655992257000000, 1655992257000000, 1655992257000000,\n", + " 1655992257000000, 1655992257000000, 1655992257000000,\n", + " 1655992257000000, 1655992257000000, 1655992257000000])
    • wavelength
      (wavelength)
      float32
      nm
      248.68408, 249.48499, ..., 1042.3112, 1043.0312
      Values:
      array([ 248.68408, 249.48499, 250.28581, ..., 1041.591 , 1042.3112 ,\n", + " 1043.0312 ], dtype=float32)
    • (spectrum, wavelength)
      float32
      counts
      3.218e-05, 3.212e-05, ..., 3.202e-05, 3.196e-05
      Values:
      array([[3.218e-05, 3.212e-05, 3.196e-05, ..., 3.200e-05, 3.196e-05,\n", + " 3.194e-05],\n", + " [3.224e-05, 3.210e-05, 3.200e-05, ..., 3.194e-05, 3.198e-05,\n", + " 3.188e-05],\n", + " [3.216e-05, 3.206e-05, 3.186e-05, ..., 3.198e-05, 3.198e-05,\n", + " 3.190e-05],\n", + " ...,\n", + " [3.222e-05, 3.210e-05, 3.198e-05, ..., 3.200e-05, 3.188e-05,\n", + " 3.196e-05],\n", + " [3.216e-05, 3.202e-05, 3.200e-05, ..., 3.194e-05, 3.196e-05,\n", + " 3.200e-05],\n", + " [3.228e-05, 3.202e-05, 3.192e-05, ..., 3.196e-05, 3.202e-05,\n", + " 3.196e-05]], dtype=float32)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[spectrum:12, wavelength:1044, ]\n", + "Coordinates:\n", + " integration_time float32 [µs] (spectrum) [5e+07, 5e+07, ..., 5e+07, 5e+07]\n", + " is_dark bool (spectrum) [False, False, ..., False, False]\n", + " is_data bool (spectrum) [True, True, ..., True, True]\n", + " is_reference bool (spectrum) [False, False, ..., False, False]\n", + " monowavelengths float32 [nm] (spectrum) [295, 295, ..., 320, 320]\n", + " time int64 [µs] (spectrum) [1655992257000000, 1655992257000000, ..., 1655992257000000, 1655992257000000]\n", + " wavelength float32 [nm] (wavelength) [248.684, 249.485, ..., 1042.31, 1043.03]\n", + "Data:\n", + " float32 [counts] (spectrum, wavelength) [3.218e-05, 3.212e-05, ..., 3.202e-05, 3.196e-05]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (8.17 KB)
    • wavelength: 1044
    • wavelength
      (wavelength)
      float32
      nm
      248.68408, 249.48499, ..., 1042.3112, 1043.0312
      Values:
      array([ 248.68408, 249.48499, 250.28581, ..., 1041.591 , 1042.3112 ,\n", + " 1043.0312 ], dtype=float32)
    • (wavelength)
      float32
      counts
      3.226e-05, 3.202e-05, ..., 3.198e-05, 3.196e-05
      Values:
      array([3.226e-05, 3.202e-05, 3.200e-05, ..., 3.196e-05, 3.198e-05,\n", + " 3.196e-05], dtype=float32)
    • integration_time
      ()
      float32
      µs
      50000000.0
      Values:
      array(50000000., dtype=float32)
    • is_dark
      ()
      bool
      True
      Values:
      array(True)
    • is_data
      ()
      bool
      False
      Values:
      array(False)
    • is_reference
      ()
      bool
      False
      Values:
      array(False)
    • monowavelengths
      ()
      float32
      nm
      nan
      Values:
      array(nan, dtype=float32)
    • time
      ()
      int64
      µs
      1655992257000000
      Values:
      array(1655992257000000)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[wavelength:1044, ]\n", + "Coordinates:\n", + " wavelength float32 [nm] (wavelength) [248.684, 249.485, ..., 1042.31, 1043.03]\n", + "Data:\n", + " float32 [counts] (wavelength) [3.226e-05, 3.202e-05, ..., 3.198e-05, 3.196e-05]\n", + "Attributes:\n", + " integration_time float32 [µs] () [5e+07]\n", + " is_dark bool () [True]\n", + " is_data bool () [False]\n", + " is_reference bool () [False]\n", + " monowavelengths float32 [nm] () [nan]\n", + " time int64 [µs] () [1655992257000000]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide data repr\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "Show/Hide attributes\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
scipp.DataArray (8.17 KB)
    • wavelength: 1044
    • wavelength
      (wavelength)
      float32
      nm
      248.68408, 249.48499, ..., 1042.3112, 1043.0312
      Values:
      array([ 248.68408, 249.48499, 250.28581, ..., 1041.591 , 1042.3112 ,\n", + " 1043.0312 ], dtype=float32)
    • (wavelength)
      float32
      counts
      3.212e-05, 3.2e-05, ..., 3.19e-05, 3.188e-05
      Values:
      array([3.212e-05, 3.200e-05, 3.196e-05, ..., 3.190e-05, 3.190e-05,\n", + " 3.188e-05], dtype=float32)
    • integration_time
      ()
      float32
      µs
      50000000.0
      Values:
      array(50000000., dtype=float32)
    • is_dark
      ()
      bool
      False
      Values:
      array(False)
    • is_data
      ()
      bool
      False
      Values:
      array(False)
    • is_reference
      ()
      bool
      True
      Values:
      array(True)
    • monowavelengths
      ()
      float32
      nm
      nan
      Values:
      array(nan, dtype=float32)
    • time
      ()
      int64
      µs
      1655992257000000
      Values:
      array(1655992257000000)
" + ], + "text/plain": [ + "\n", + "Dimensions: Sizes[wavelength:1044, ]\n", + "Coordinates:\n", + " wavelength float32 [nm] (wavelength) [248.684, 249.485, ..., 1042.31, 1043.03]\n", + "Data:\n", + " float32 [counts] (wavelength) [3.212e-05, 3.2e-05, ..., 3.19e-05, 3.188e-05]\n", + "Attributes:\n", + " integration_time float32 [µs] () [5e+07]\n", + " is_dark bool () [False]\n", + " is_data bool () [False]\n", + " is_reference bool () [True]\n", + " monowavelengths float32 [nm] () [nan]\n", + " time int64 [µs] () [1655992257000000]\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "ename": "ValueError", + "evalue": "Wrong string. That methods does not exist for NurF at LoKi.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/zs/1bljp9y15h9bxppw44pk88gh0000gn/T/ipykernel_16658/2202802327.py\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 16\u001b[0;31m \u001b[0mra_dict\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mload_nurfloki_file\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m'ramen'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Library/CloudStorage/OneDrive-LundUniversity/UU/ESS-scipp/ess/docs/instruments/loki/nurf/utils.py\u001b[0m in \u001b[0;36mload_nurfloki_file\u001b[0;34m(name, exp_meth)\u001b[0m\n\u001b[1;32m 74\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 75\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mexp_meth\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mnurf_meth\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 76\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Wrong string. That methods does not exist for NurF at LoKi.'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 77\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 78\u001b[0m \u001b[0mpath_to_group\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34mf\"entry/instrument/{exp_meth}\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: Wrong string. That methods does not exist for NurF at LoKi." + ] + } + ], + "source": [ + "name='066017.nxs'\n", + "\n", + "uv_dict=load_nurfloki_file(name,'uv')\n", + "display(uv_dict['sample'])\n", + "display(uv_dict['dark'])\n", + "display(uv_dict['reference'])\n", + "\n", + "print(type(uv_dict))\n", + "\n", + "fluo_dict=load_nurfloki_file(name,'fluorescence')\n", + "display(fluo_dict['sample'])\n", + "display(fluo_dict['dark'])\n", + "display(fluo_dict['reference'])\n", + "\n", + "\n", + "ra_dict=load_nurfloki_file(name,'ramen')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "interpreter": { + "hash": "410bfb39d85b3112e66f48ab3e53bb74ad7e7b3fb364f756ca860b5a3cf79ca2" + }, + "kernelspec": { + "display_name": "Python 3.8.12 ('scippneutron')", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/ess/loki/nurf/utils.py b/src/ess/loki/nurf/utils.py new file mode 100755 index 000000000..7cd6a85f6 --- /dev/null +++ b/src/ess/loki/nurf/utils.py @@ -0,0 +1,286 @@ +# standard library imports +import itertools +import os +from typing import Optional, Type + +# related third party imports +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import cm +import matplotlib.gridspec as gridspec +from IPython.display import display, HTML +from scipy.optimize import leastsq # needed for fitting of turbidity + +# local application imports +import scippneutron as scn +import scippnexus as snx +import scipp as sc +from scipp.scipy.ndimage import median_filter + +# For scipp docstring convention, TODO: remove later +# https://scipp.github.io/reference/developer/coding-conventions.html#docstrings + + + + +def split_sample_dark_reference(da): + """Separate incoming dataarray into the three contributions: sample, dark, reference. + + Parameters + ---------- + da: scipp.DataArray + sc.DataArray that contains spectroscopy contributions sample, dark, + reference + + Returns: + ---------- + da_dict: dict + Dictionary that contains spectroscopy data signal (data) from the sample, + the reference, and the dark measurement. + Keys: sample, reference, dark + + """ + assert isinstance(da, sc.DataArray) + + dark = da[da.coords["is_dark"]] #spectrum: 1, wavelength: 1044 + ref = da[da.coords["is_reference"]] #spectrum: 1, wavelength: 1044 + sample = da[da.coords["is_data"]] #spectrum: 12, wavelength: 1044 (example) + + # Current suggestion to keep meta data along the calculation. + # Indirect assumption dark and reference are from the same Loki.nxs file + dark=dark.squeeze().broadcast(sizes=sample.sizes) + dark.attrs['source']=dark.attrs['source'].broadcast(['spectrum'], [sample.sizes['spectrum']]) + ref=ref.squeeze().broadcast(sizes=sample.sizes) + ref.attrs['source']=ref.attrs['source'].broadcast(['spectrum'], [sample.sizes['spectrum']]) + + + #TODO Instead of a dict a sc.Dataset? + return {"sample": sample, "reference": ref, "dark": dark} + + +def load_nurfloki_file(name: str, exp_meth: str ): + """ Loads data of a specified experimental method from the corresponding entry in a + NUrF-Loki.nxs file. + + + Parameters + ---------- + name: str + Filename, e.g. 066017.nxs + exp_meth: str + Experimental method available with the NUrF exp. configuration. + Current default values: uv, fluorescence. TODO in the future: raman + + Returns + ---------- + exp_meth_dict: dict + Dictionary of sc.DataArrays. Keys: data, reference, dark. + Data contains all relevant signals of the sample. + + """ + nurf_meth=['uv', 'fluorescence'] + + if not isinstance(exp_meth, str): + raise TypeError('exp_math needs to be of type str.') + + if not exp_meth in nurf_meth: + raise ValueError('Wrong string. This method does not exist for NurF at LoKi.') + + path_to_group=f"entry/instrument/{exp_meth}" + + with snx.File(name) as fnl: + meth = fnl[path_to_group][()] + meth.attrs['source'] = sc.scalar(name).broadcast(['spectrum'], [meth.sizes['spectrum']]) + + # separation + exp_meth_dict = split_sample_dark_reference(meth) + + return exp_meth_dict + +def nurf_median_filter( da:sc.DataArray, kernel_size: Optional[int] = None )-> sc.DataArray: + """ A simple wrapper for an universal median filter for the NurF project. Median filter method originates from + scipp.ndimage + This function filters only along the wavelength direction, not along the spectrum direction. + Kernel_size could be given as sc.scalar(value:float, unit='nm'), but only if data is equally spaced. + Default kernel_size in wavelength direction: 3 + #TODO: Take care of this option, when hardware is ready for integration. + If not, and currently this is the case for the spectrometer, kernel_size has to be int, odd or even. + I don't check for int because scipp does it.y + """ + if not {'spectrum', 'wavelength'}.issubset(da.dims): + raise ValueError('Dimensions spectrum and wavelength expected.') + + # set a default value + if kernel_size is None: + kernel_size=3 + + # create the kernel + # no filtering along the spectrum direction, but in wavelength direction + kernel_size_scipp={'spectrum':1, 'wavelength':kernel_size} + + # apply kernel + da_filt=median_filter(da, size=kernel_size_scipp) + + return da_filt + + + + +def export_uv(name, path_output): + """Export normalized all uv data and an averaged uv spectrum in a LoKI.nxs file to .dat file + + Attention: Current output format follows custom format for an individual user, not + any other software. + + Parameters + ---------- + name: str + Filename of LoKI.nxs file that contains the UV data for export + + path_output: str + Absolute path to output folder + + Returns + ---------- + Tab-limited .dat file with columns wavelength, dark, reference, (multiple) raw uv spectra, (multiple) corrected uv spectra, one averaged uv spectrum + + """ + + uv_dict = load_nurfloki_file(name, 'uv') + normalized = normalize_uv( + **uv_dict + ) + + normalized_avg = normalized.mean("spectrum") + + # prepare for export as .dat files + output_filename = f"{name}_uv.dat" + + # puzzle the header together + l = "".join( + ["dark_{0}\t".format(i) for i in range(uv_dict['dark'].ndim) + ] + ) + m="".join( + ["reference_{0}\t".format(i) for i in range(uv_dict['reference'].ndim) + ] + ) + + n="".join( + [ + "uv_raw_spectra_{0}\t".format(i) + for i, x in enumerate(range(uv_dict['sample'].sizes["spectrum"])) + ] + ) + o="".join( + [ + "uv_norm_spectra_{0}\t".format(i) + for i, x in enumerate(range(normalized.sizes["spectrum"])) + ] + ) + + p = "uv_spectra_avg\t" + + + hdrtxt = "wavelength [nm]\t" + final_header = hdrtxt + l + m + n + o + p + + + data_to_save = np.column_stack( + ( + normalized.coords["wavelength"].values.transpose(), + + # raw data + uv_dict['dark'].values.transpose(), + uv_dict['reference'].values.transpose(), + uv_dict['sample'].values.transpose(), + + # reduced data + normalized.values.transpose(), + normalized_avg.values.transpose(), + + ) + ) + path_to_save = os.path.join(path_output, output_filename) + + # dump the content + with open(path_to_save, "w") as f: + np.savetxt(f, data_to_save, fmt="%.10f", delimiter="\t", header=final_header) + + + +def export_fluo(name, path_output): + """Export corrected fluo data contained in a LoKI.nxs file to .dat file + + Attention: Current output format follows custom format for an individual user, not + any other software. + + Parameters + ---------- + name: str + Filename of LoKI.nxs file that contains the fluo data for export + + path_output: str + Absolute path to output folder + + Returns + ---------- + Tab-limited .dat file with columns wavelength, dark, reference, multiple raw fluo + spectra, and multiple normalized fluo spectra. + Header of each fluo spectrum column contains the incident excitation energy. + + """ + # export of all calculated fluo data in a LOKI.nxs name to .dat + # input: filename of LOKI.nxs: name, str, path_output: absolut path to output folder, str + + fluo_dict = load_nurfloki_file(name, 'fluorescence') + final_fluo = normalize_fluo(**fluo_dict) + + # prepare for export as .dat files + output_filename = f"{name}_fluo.dat" + path_to_save = os.path.join(path_output, output_filename) + + + + l = "".join( + ["dark_{0}\t".format(i) for i in range(fluo_dict['dark'].ndim) + ] + ) + m = "".join( + ["reference_{0}\t".format(i) for i in range(fluo_dict['reference'].ndim) + ] + ) + + n= "".join([f"raw_{i}nm\t" for i in final_fluo.coords["monowavelengths"].values]) + + o= "".join([f"norm_{i}nm\t" for i in final_fluo.coords["monowavelengths"].values]) + + + + hdrtxt = "wavelength [nm]\t" + final_header = hdrtxt + l + m + n + o + + data_to_save = np.column_stack( + ( + final_fluo.coords["wavelength"].values.transpose(), + # dark + fluo_dict['dark'].values.transpose(), + # reference + fluo_dict['reference'].values.transpose(), + # sample + fluo_dict['sample'].values.transpose(), + # final fluo spectra + final_fluo.data["spectrum", :].values.transpose(), + ) + ) + + # dump the content + with open(path_to_save, "w") as f: + np.savetxt( + f, + data_to_save, + fmt="".join(["%.5f\t"] + ["%.5e\t"] * (fluo_dict['dark'].ndim + + fluo_dict['reference'].ndim + 2*final_fluo.sizes["spectrum"])), + delimiter="\t", + header=final_header, + ) diff --git a/src/ess/loki/nurf/uv.py b/src/ess/loki/nurf/uv.py new file mode 100644 index 000000000..1ddf2bbed --- /dev/null +++ b/src/ess/loki/nurf/uv.py @@ -0,0 +1,458 @@ +# standard library imports +import itertools +import os +from typing import Optional, Type, Union + +# related third party imports +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import cm +import matplotlib.gridspec as gridspec +from IPython.display import display, HTML +from scipy.optimize import leastsq # needed for fitting of turbidity + +# local application imports +import scippneutron as scn +import scippnexus as snx +import scipp as sc +from ess.loki.nurf import utils +from scipp.scipy.ndimage import median_filter + + +def normalize_uv( + *, sample: sc.DataArray , reference: sc.DataArray , dark: sc.DataArray +) -> sc.DataArray : + """Calculates the absorbance of the UV signal. + + Parameters + ---------- + sample: sc.DataArray + DataArray containing sample UV signal, one spectrum or multiple spectra. + reference: sc.DataArray + DataArray containing reference UV signal, one spectrum expected. + dark: sc.DataArray + DataArray containing dark UV signal, one spectrum expected. + + Returns + ---------- + normalized: sc.DataArray + DataArray that contains the normalized UV signal, one spectrum or mulitple spectra. + + """ + + normalized = sc.log10( + (reference - dark) / (sample - dark) + ) # results in DataArrays with multiple spectra + + return normalized + +def load_and_normalize_uv(name: str) -> sc.DataArray : + """Loads the UV data from the corresponding entry in the LoKI.nxs filename and + calculates the absorbance of each UV spectrum. + For an averaged spectrum based on all UV spectra in the file, use average_uv. + + Parameters + ---------- + name: str + Filename, e.g. 066017.nxs + + Returns + ---------- + normalized: sc.DataArray + DataArray that contains the normalized UV signal, one spectrum or mulitple spectra. + + """ + uv_dict = utils.load_nurfloki_file(name, 'uv') + normalized = normalize_uv(**uv_dict) # results in DataArrays with multiple spectra + # provide source of each spectrum in the file + normalized.attrs['source'] = sc.scalar(name).broadcast(['spectrum'], [normalized.sizes['spectrum']]) + + return normalized + +def average_uv(name: str) -> sc.DataArray : + """Processses all UV spectra in a single LoKI.nxs and averages them to one corrected + UV spectrum. + + Parameters + ---------- + name: str + Filename for a LoKI.nxs file containting UV entry. + + Returns + ---------- + normalized: + One averaged UV spectrum. Averaged over all UV spectra contained in the file + under UV entry data. Preserves original source. + + """ + #uv_dict = utils.load_nurfloki_file(name, 'uv') + #normalized = normalize_uv(**uv_dict) + normalized = load_and_normalize_uv(name) + + # returns averaged uv spectrum + return normalized.groupby('source').mean('spectrum').squeeze() + + +def gather_uv_avg_set(filelist:list) -> sc.Dataset: + """Creates a sc.DataSet for set of given filenames for an experiment composed of + multiple, separated UV measurements over time. Multiple UV spectra in each file will be averaged. + Function will fail, if not all files contain the same number of UV spectra. + + Parameters + ---------- + filelist: list of str + List of filenames containing UV data + + Returns + ---------- + uv_spectra_set: sc.Dataset + DataSet of multiple UV DataArrays, where the UV signal for each experiment was + averaged + + """ + + uv_spectra_set = sc.Dataset({name: average_uv(name) for name in filelist}) + return uv_spectra_set + + + +def gather_uv_set(filelist: list) -> Union[sc.Dataset, sc.DataArray]: + """Gathers from multiple input LoKI.files the UV spectra. + If all LoKI.nxs files contain the same number of UV spectra, the function returns + a sc.Dataset, if not a sc.DataArray is returned. The source attribute provides + the reference of origin. + """ + + #check first if numbers of uv spectra in each file are the same + num_uv_spectra=np.empty(len(filelist)) + for count, name in enumerate(filelist): + uv_da=load_and_normalize_uv(name) + num_uv_spectra[count]=uv_da.sizes['spectrum'] + + # compare all entries with the first entry + if np.all(num_uv_spectra == num_uv_spectra[0]): + # prepare as output an sc.Dataset + res=sc.Dataset({name:load_and_normalize_uv(name) for name in filelist}) + + else: + data_arrays = [] + for name in filelist: + da = load_and_normalize_uv(name) + #da.attrs['source'] = sc.scalar(name).broadcast(['spectrum'], [da.sizes['spectrum']]) + data_arrays.append(da) + res = sc.concat(data_arrays, dim='spectrum') + + return res + + +def uv_peak_int(uv_da: sc.DataArray , wavelength: Optional[sc.Variable] = None, tol=None) -> dict: + """Extract uv peak intensity for a given wavelength and a given interval. + If no wavelength is given, 280 nm is chosen as value. + If no tolerance is given, a tolerance of 0.5 nm is chosen. + + UV peak intensity is calculated for a given wavelength in two different ways: + 1. Interpolation of intensity around given wavelength, selection of interpolated intensity value for requested wavelength ("one_wavelength") + 2. Selection of 2*tol wavelength interval around requested wavelength. Average over all intensity values in this interval. ("wl_interval") + + Parameters + ---------- + uv_da: sc.DataArray + DataArray containing uv spectra + wavelength: sc.Variable + Wavelength with a unit + tol: float + Tolerance, 2*tol defines the interval around the given wavelength + + Returns + ---------- + uv_peak_int: dict + Dictionary that contains the peak intensity for the requested wavelength, the peak intensity averaged over the requested interval, the requested wavelength with its unit, and the tolerance + + """ + assert ( + "wavelength" in uv_da.dims + ), "sc.DataArray is missing the wavelength dimension" # assert that 'wavelength' is a dimension in the uv_da sc.DataArray + + # set default value for wavelength: + if wavelength is None: + wavelength = sc.scalar(280, unit='nm') + else: + if not isinstance(wavelength, sc.Variable): + raise TypeError("Wavelength needs to be of type sc.Variable.") + assert(wavelength.unit==uv_da.coords["wavelength"].unit) + + # set default value for tolerance: + if tol is None: + tol = sc.scalar(0.5, unit='nm') + else: + if not isinstance(tol,sc.Variable): + raise TypeError("Tol needs to be of type sc.Variable.") + + # filter spectrum values for the specified interval, filtered along the wavelength + # dimension + uv_da_filt = uv_da[ + "wavelength", + (wavelength - tol) : (wavelength + tol) , + ] + # average intensity value in interval + uv_int_mean_interval = uv_da_filt.mean(dim="wavelength") + + # interpolation approach + from scipp.interpolate import interp1d + + uv_interp = interp1d(uv_da, "wavelength") + x = sc.linspace( + dim="wavelength", start=wavelength, stop=wavelength, num=1, unit=wavelength.unit) + uv_int_one_wl = uv_interp(x) + + # prepare a dict for output + uv_peak_int = { + "one_wavelength": uv_int_one_wl, + "wl_interval": uv_int_mean_interval, + "wavelength": wavelength, + "tol": tol, + } + + return uv_peak_int + +def turbidity(wl: np.ndarray, b: np.ndarray, m: np.ndarray)-> np.ndarray: + """Function describing turbidity tau. tau = b* lambda **(-m) + Fitting parameters: b, m. b corresponds to the baseline found for higher wavelengths (flat line in UV spectrum), m corresponds to the slope. + lambda: wavelength + + Parameters + ---------- + b: np.ndarray + Offset, baseline + + m: np.ndarray + Slope + + wl: np.ndarray + UV wavelengths + + Returns + ---------- + y: np.ndarray + Turbidity + + """ + + y = b * wl ** (-m) + return y + + +def residual(p: list, x:np.ndarray, y:np.ndarray)-> np.ndarray: + """Calculates the residuals between fitted turbidity and measured UV data + + Parameters + ---------- + p: list + Fit parameters for turbidity + + x: np.ndarray + x values, here: UV wavelength + + y: np.ndarray + y values, here: UV intensity + + Returns + ---------- + y - turbidity(x, *p): np.ndarray + Difference between measured UV intensity values and fitted turbidity + + """ + + return y - turbidity(x, *p) + + + + +def uv_turbidity_fit( + uv_da: sc.DataArray , + fit_llim: Optional[sc.Variable] = None, + fit_ulim: Optional[sc.Variable] = None, + b_llim: Optional[sc.Variable] = None, + b_ulim: Optional[sc.Variable] = None, + m=None +) -> sc.DataArray : + """Fit turbidity to the experimental data. Turbidity: tau=b * wavelength^(-m) Parameters of interest: b, m. + b is the baseline and m is the slope. b can be obtained by averaging over the flat range of the UV spectrum + in the higher wavelength range. + m: make an educated guess. Advice: Limit fitting range to wavelengths after spectroscopy peaks. + + Parameters + ---------- + uv_da: sc.DataArray + UV sc.DataArray containing one or more normalized UV spectra + fit_llim: sc.Variable + Lower wavelength limit of fit range for turbidity + fit_ulim: sc.Variable + Upper wavelength limit of fit range for turbidity + b_llim: sc.Variable + Lower wavelength limit of fit range for b + b_ulim: sc.Variable + Upper wavelength limit of fit range for b + m: int + Educated guess start value of slope parameter in turbidity, default: 0.01 + + + Returns: + ---------- + uv_da_turbcorr: sc.DataArray + uv_da dataarray where each spectrum was corrected for a fitted turbidity, export for all wavelength values + + """ + # obtain unit of wavelength: + #if wl_unit is None: + # wl_unit = uv_da.coords["wavelength"].unit + #else: + # if not isinstance(wl_unit, sc.Unit): + # raise TypeError + # assert ( + # wl_unit == uv_da.coords["wavelength"].unit + # ) # we check that the given unit corresponds to the unit for the wavelength + if not isinstance(uv_da, sc.DataArray ): + raise TypeError('uv_da must be of type sc.DataArray ') + + if fit_llim is None: + fit_llim=sc.scalar(350, unit='nm') + if fit_ulim is None: + fit_ulim=sc.scalar(600, unit='nm') + + if b_llim is None: + b_llim=sc.scalar(500, unit='nm') + if b_ulim is None: + b_ulim=sc.scalar(800, unit='nm') + + if fit_llim is not None and fit_ulim is not None: + assert (fit_llim < fit_ulim).value, "fit_llim < fit_ulim" + if b_llim is not None and b_ulim is not None: + assert (b_llim < b_ulim).value, "b_llim sc.DataArray: + """Applies turbidity correction to UV spectra for a set of LoKI.nxs files. + b: offset, m: slope. Same values are applied to all given spectra. + filelist: dict + Dict of sc.DataArrays, key: filename, value: sc.DataArray + + """ + + uv_collection = {} + for name, uv_da in filelist.items(): + + uv_da_turbcorr = uv_turbidity_fit( + uv_da, + fit_llim=fit_llim, + fit_ulim=fit_ulim, + b_llim=b_llim, + b_ulim=b_ulim, + m=m + ) + + # if utils.load_nurfloki_file and normalize_uv are used, source attribute does not exist + # same holds if mean is applied to a sc.DataArray + # this is in preparation to rewrite uv_multi_turbidity to accept dataarrays or dict of dataarrays, not filelist + if not "source" in uv_da_turbcorr.attrs.keys(): + # append names as attributes + uv_da_turbcorr.attrs["source"] = sc.array( + dims=["spectrum"], values=[name] * uv_da_turbcorr.sizes["spectrum"] + ) + + uv_collection[f"{name}"] = uv_da_turbcorr + + # print(name,uv_da_turbcorr.data.shape,uv_da_turbcorr.data.values.ndim ) + + multi_uv_turb_corr_da = sc.concat( + [uv_collection[f"{name}"] for name in filelist], dim="spectrum" + ) + #display(multi_uv_turb_corr_da) + + + return multi_uv_turb_corr_da + + + + + +