From 10c688b3b55d0d4670840c66a611d56a7d242cd8 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Thu, 5 May 2022 07:52:22 -0400 Subject: [PATCH 1/6] start of excel workup writer --- pyproject.toml | 3 +- rimseval/data_io/excel_writer.py | 70 +++++++++++++++++++++++++ tests/func/data_io/test_excel_writer.py | 18 +++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 rimseval/data_io/excel_writer.py create mode 100644 tests/func/data_io/test_excel_writer.py diff --git a/pyproject.toml b/pyproject.toml index 4e259da..d4490ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ requires = [ "numba~=0.55.1", "numpy~=1.21.5", "PyQt6", - "scipy" + "scipy", + "xlsxwriter" ] requires-python=">=3.8" classifiers = [ "License :: OSI Approved :: MIT License",] diff --git a/rimseval/data_io/excel_writer.py b/rimseval/data_io/excel_writer.py new file mode 100644 index 0000000..dd8ed83 --- /dev/null +++ b/rimseval/data_io/excel_writer.py @@ -0,0 +1,70 @@ +"""Write Excel Files from the files that we have, e.g., a workup file.""" + +from datetime import datetime +from pathlib import Path + +from iniabu.utilities import item_formatter +import xlsxwriter + +from .. import CRDFileProcessor +from ..utilities import ini + + +def workup_file_writer(crd: CRDFileProcessor, fname: Path) -> None: + """Write out an Excel workup file. + + This is for the user to write out an excel workup file, which will already be + filled with the integrals of the given CRD file. + + :param crd: CRD file processor file to write out. + :param fname: File name for the file to write out to. + """ + if crd.def_integrals is None: + return + else: + int_names, _ = crd.def_integrals + + # format names + for it, name in enumerate(int_names): + int_names[it] = item_formatter(name) + + fname = fname.with_suffix(".xlsx").absolute() # ensure correct format + + wb = xlsxwriter.Workbook(str(fname)) + ws = wb.add_worksheet() + + # formats + fmt_title = wb.add_format({"bold": True, "font_size": 14}) + fmt_bold = wb.add_format({"bold": True}) + fmt_italic = wb.add_format({"italic": True}) + fmt_bold_italic = wb.add_format({"bold": True, "italic": True}) + fmt_std_abus = wb.add_format({"bold": True, "color": "red"}) + + # write the title + ws.write(0, 0, f"Workup {datetime.today().date()}", fmt_title) + + # write data header + hdr_row = 3 # row to start header of the data in + general_headers = ["Remarks", "File Name", "# Shots"] + + int_col = len(general_headers) # start of integral column + delta_col = int_col + 2 * len(int_names) # start of delta column + + for col, hdr in enumerate(general_headers): + ws.write(hdr_row, col, general_headers[col], fmt_bold_italic) + for col, name in enumerate(int_names): + ws.write(hdr_row, 2 * col + int_col, name, fmt_bold_italic) + ws.write(hdr_row, 2 * col + 1 + int_col, f"σ{name})", fmt_bold_italic) + + # close the workbook + wb.close() + + +def iso_format_excel(iso: str) -> str: + """Format isotope name from `iniabu` to format as written to Excel. + + :param iso: Isotope, formatted according to `iniabu`, e.g., "Si-28" + :return: Excel write-out format. + """ + iso_split = iso.split("-") + return f"{iso_split[1]}{iso_split[0]}" diff --git a/tests/func/data_io/test_excel_writer.py b/tests/func/data_io/test_excel_writer.py new file mode 100644 index 0000000..765e762 --- /dev/null +++ b/tests/func/data_io/test_excel_writer.py @@ -0,0 +1,18 @@ +"""Test Excel writer function.""" + +from pathlib import Path + +from rimseval import CRDFileProcessor +import rimseval.data_io.excel_writer as exw + + +def test_workup_file_writer_no_integrals(crd_file): + """Assure nothing is done if no integrals were set.""" + _, _, _, fname = crd_file + crd = CRDFileProcessor(Path(fname)) + + # create a file for excel outptut - which won't get generated + ex_fname = Path("test.xlsx") + exw.workup_file_writer(crd, ex_fname) + + assert not ex_fname.is_file() From 43c977b53d076cbf7d39e6cceab9fda58a7d0dff Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Mon, 9 May 2022 18:13:47 -0400 Subject: [PATCH 2/6] First full implementation of the excel file writer Includes a "run through test", which simply creates the excel file and ensures that it is there --- rimseval/data_io/excel_writer.py | 191 ++++++++++++++++++++++-- tests/func/data_io/test_excel_writer.py | 26 ++++ 2 files changed, 205 insertions(+), 12 deletions(-) diff --git a/rimseval/data_io/excel_writer.py b/rimseval/data_io/excel_writer.py index dd8ed83..b49569c 100644 --- a/rimseval/data_io/excel_writer.py +++ b/rimseval/data_io/excel_writer.py @@ -5,12 +5,15 @@ from iniabu.utilities import item_formatter import xlsxwriter +from xlsxwriter.utility import xl_rowcol_to_cell from .. import CRDFileProcessor from ..utilities import ini -def workup_file_writer(crd: CRDFileProcessor, fname: Path) -> None: +def workup_file_writer( + crd: CRDFileProcessor, fname: Path, timestamp: bool = False +) -> None: """Write out an Excel workup file. This is for the user to write out an excel workup file, which will already be @@ -18,6 +21,7 @@ def workup_file_writer(crd: CRDFileProcessor, fname: Path) -> None: :param crd: CRD file processor file to write out. :param fname: File name for the file to write out to. + :param timestamp: Create a column for the time stamp? Defaults to ``False`` """ if crd.def_integrals is None: return @@ -26,7 +30,18 @@ def workup_file_writer(crd: CRDFileProcessor, fname: Path) -> None: # format names for it, name in enumerate(int_names): - int_names[it] = item_formatter(name) + formatted_name = item_formatter(name) + if "-" in formatted_name: # only format if it's an isotope! + int_names[it] = item_formatter(name) + + # get normalizing isotope + norm_names = [] + for name in int_names: + try: + ele = name.split("-")[0] + norm_names.append(ini._get_norm_iso(ele)) + except IndexError: # norm isotope does not exist + norm_names.append(None) fname = fname.with_suffix(".xlsx").absolute() # ensure correct format @@ -35,26 +50,175 @@ def workup_file_writer(crd: CRDFileProcessor, fname: Path) -> None: # formats fmt_title = wb.add_format({"bold": True, "font_size": 14}) - fmt_bold = wb.add_format({"bold": True}) - fmt_italic = wb.add_format({"italic": True}) - fmt_bold_italic = wb.add_format({"bold": True, "italic": True}) + fmt_hdr_0 = wb.add_format({"bold": True, "italic": True, "left": True}) + fmt_hdr = wb.add_format({"bold": True, "italic": True}) fmt_std_abus = wb.add_format({"bold": True, "color": "red"}) + fmt_counts_0 = wb.add_format({"num_format": "0", "left": True}) + fmt_counts = wb.add_format({"num_format": "0"}) + fmt_counts_unc = wb.add_format( + {"num_format": "0", "color": "gray", "valign": "left"} + ) + fmt_delta_0 = wb.add_format({"num_format": "0", "left": True}) + fmt_delta = wb.add_format({"num_format": "0"}) + fmt_delta_unc = wb.add_format( + {"num_format": "0", "color": "gray", "valign": "left"} + ) + fmt_timestamp = wb.add_format({"num_format": "YYYY-MM-DD HH:MM:SS"}) + + # cell widths for data + wdth_counts = 8 + wdth_delta = 12 + wdth_delta_unc = 6 + + # rows to define + abu_row = 4 + num_eqn_rows = 999 + + # special headers and user definitions + general_headers = ["Remarks", "File Name", "# Shots"] + general_headers_widths = [20, 20, 10] + fname_col = 1 + shots_col = 2 + + if timestamp: + general_headers.append("Timestamp") + general_headers_widths.append(18) # write the title ws.write(0, 0, f"Workup {datetime.today().date()}", fmt_title) - # write data header - hdr_row = 3 # row to start header of the data in - general_headers = ["Remarks", "File Name", "# Shots"] + # write data header and abundances + hdr_row = abu_row + 1 # row to start header of the data in int_col = len(general_headers) # start of integral column delta_col = int_col + 2 * len(int_names) # start of delta column for col, hdr in enumerate(general_headers): - ws.write(hdr_row, col, general_headers[col], fmt_bold_italic) + ws.write(hdr_row, col, general_headers[col], fmt_hdr) + ws.set_column(col, col, general_headers_widths[col]) + for col, name in enumerate(int_names): - ws.write(hdr_row, 2 * col + int_col, name, fmt_bold_italic) - ws.write(hdr_row, 2 * col + 1 + int_col, f"σ{name})", fmt_bold_italic) + # write abundances + try: + abu_col = ini.iso[name].abu_rel + ws.write(abu_row, 2 * col + int_col, abu_col, fmt_std_abus) + except IndexError: + pass + # write integral header + name = iso_format_excel(name) + + fmt_hdr_use = fmt_hdr_0 if col == 0 else fmt_hdr + + ws.write(hdr_row, 2 * col + int_col, name, fmt_hdr_use) + ws.write(hdr_row, 2 * col + 1 + int_col, f"±1σ", fmt_hdr) + + # integral column width + ws.set_column(int_col, int_col + 2 * len(int_names) - 1, wdth_counts) + + col = delta_col + for it, name in enumerate(int_names): + norm_iso_name = norm_names[it] + fmt_hdr_use = fmt_hdr_0 if col == delta_col else fmt_hdr + if ( + norm_iso_name is not None and norm_iso_name in int_names + ): # norm isotope valid + ws.write( + hdr_row, + col, + f"δ({iso_format_excel(name)}/{iso_format_excel(norm_iso_name)})", + fmt_hdr_use, + ) + ws.write(hdr_row, col + 1, f"±1σ", fmt_hdr) + + # set width + ws.set_column(col, col, wdth_delta) + ws.set_column(col + 1, col + 1, wdth_delta_unc) + + col += 2 + + # WRITE DATA + data_row = hdr_row + 1 + + # file naame + ws.write(data_row, fname_col, crd.fname.name) + + # write the number of shots + ws.write(data_row, shots_col, crd.nof_shots, fmt_counts) + + for col in range(data_row + 1, num_eqn_rows + data_row): + ws.write_blank(col, shots_col, None, fmt_counts) + + # write the timestamp if requested + if timestamp: + ws.write(data_row, shots_col + 1, crd.timestamp, fmt_timestamp) + + for col in range(data_row + 1, num_eqn_rows + data_row): + ws.write_blank(col, shots_col + 1, None, fmt_timestamp) + + # write integrals + for col, dat in enumerate(crd.integrals): + fmt_counts_use = fmt_counts_0 if col == 0 else fmt_counts + ws.write(data_row, 2 * col + int_col, dat[0], fmt_counts_use) + ws.write(data_row, 2 * col + int_col + 1, dat[1], fmt_counts_unc) + + for col in range(data_row + 1, num_eqn_rows + data_row): # boarder for integrals + ws.write_blank(col, int_col, None, fmt_counts_0) + + # write delta equations + col = delta_col + for it, name in enumerate(int_names): + norm_iso_name = norm_names[it] + if ( + norm_iso_name is not None and norm_iso_name in int_names + ): # norm isotope valid + for eq_row in range(num_eqn_rows): + # get cell values, nominators and denominators + nom_iso = xl_rowcol_to_cell(data_row + eq_row, 2 * it + int_col) + nom_iso_unc = xl_rowcol_to_cell(data_row + eq_row, 2 * it + int_col + 1) + den_iso = xl_rowcol_to_cell( + data_row + eq_row, + 2 * int_names.index(norm_iso_name) + int_col, + col_abs=True, + ) + den_iso_unc = xl_rowcol_to_cell( + data_row + eq_row, + 2 * int_names.index(norm_iso_name) + int_col + 1, + col_abs=True, + ) + # get standard abundances cell names + nom_std = xl_rowcol_to_cell(abu_row, 2 * it + int_col, row_abs=True) + den_std = xl_rowcol_to_cell( + abu_row, + 2 * int_names.index(norm_iso_name) + int_col, + row_abs=True, + col_abs=True, + ) + + # decide if we write a boarder or not + fmt_delta_use = fmt_delta_0 if col == delta_col else fmt_delta + + # write the values for the delta formula + ws.write( + data_row + eq_row, + col, + f'=IF({nom_iso}<>"",' + f'(({nom_iso}/{den_iso})/({nom_std}/{den_std})-1)*1000, "")', + fmt_delta_use, + ) + # equation for uncertainty + ws.write( + data_row + eq_row, + col + 1, + f'=IF({nom_iso}<>"",' + f"1000*SQRT(" + f"(({nom_iso_unc}/{den_iso})/({nom_std}/{den_std}))^2+" + f"(({nom_iso}*{den_iso_unc}/{den_iso}^2)/({nom_std}/{den_std}))^2" + f'), "")', + fmt_delta_unc, + ) + + # increment column + col += 2 # close the workbook wb.close() @@ -67,4 +231,7 @@ def iso_format_excel(iso: str) -> str: :return: Excel write-out format. """ iso_split = iso.split("-") - return f"{iso_split[1]}{iso_split[0]}" + if len(iso_split) != 2: + return iso + else: + return f"{iso_split[1]}{iso_split[0]}" diff --git a/tests/func/data_io/test_excel_writer.py b/tests/func/data_io/test_excel_writer.py index 765e762..91300a0 100644 --- a/tests/func/data_io/test_excel_writer.py +++ b/tests/func/data_io/test_excel_writer.py @@ -2,6 +2,8 @@ from pathlib import Path +import numpy as np + from rimseval import CRDFileProcessor import rimseval.data_io.excel_writer as exw @@ -16,3 +18,27 @@ def test_workup_file_writer_no_integrals(crd_file): exw.workup_file_writer(crd, ex_fname) assert not ex_fname.is_file() + + +def test_workup_file_run_through(crd_file, tmpdir): + """Run through excel writer and assure it creates an xlsx file.""" + _, _, _, fname = crd_file + + crd = CRDFileProcessor(Path(fname)) + crd.spectrum_full() + + # set some random mass cal from 1 to 2 + crd.def_mcal = np.array([[crd.tof.min(), 1.0], [crd.tof.max(), 2.0]]) + crd.mass_calibration() + + # now set the integrals to include everything + crd.def_integrals = ( + ["Fe54", "Fe56", "FeO"], + np.array([[0.9, 1.2], [1.2, 2.0], [2.0, 2.1]]), + ) + crd.integrals_calc() + + # create a file for excel outptut - which won't get generated + ex_fname = tmpdir.join("test.xlsx") + exw.workup_file_writer(crd, Path(ex_fname), timestamp=True) + assert ex_fname.isfile() From efb36bb49cb79cc9246685e683f9f11a8c4a2c1c Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Mon, 9 May 2022 18:51:46 -0400 Subject: [PATCH 3/6] Linting --- rimseval/data_io/excel_writer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rimseval/data_io/excel_writer.py b/rimseval/data_io/excel_writer.py index b49569c..9405003 100644 --- a/rimseval/data_io/excel_writer.py +++ b/rimseval/data_io/excel_writer.py @@ -94,7 +94,7 @@ def workup_file_writer( delta_col = int_col + 2 * len(int_names) # start of delta column for col, hdr in enumerate(general_headers): - ws.write(hdr_row, col, general_headers[col], fmt_hdr) + ws.write(hdr_row, col, hdr, fmt_hdr) ws.set_column(col, col, general_headers_widths[col]) for col, name in enumerate(int_names): @@ -110,7 +110,7 @@ def workup_file_writer( fmt_hdr_use = fmt_hdr_0 if col == 0 else fmt_hdr ws.write(hdr_row, 2 * col + int_col, name, fmt_hdr_use) - ws.write(hdr_row, 2 * col + 1 + int_col, f"±1σ", fmt_hdr) + ws.write(hdr_row, 2 * col + 1 + int_col, "±1σ", fmt_hdr) # integral column width ws.set_column(int_col, int_col + 2 * len(int_names) - 1, wdth_counts) @@ -128,7 +128,7 @@ def workup_file_writer( f"δ({iso_format_excel(name)}/{iso_format_excel(norm_iso_name)})", fmt_hdr_use, ) - ws.write(hdr_row, col + 1, f"±1σ", fmt_hdr) + ws.write(hdr_row, col + 1, "±1σ", fmt_hdr) # set width ws.set_column(col, col, wdth_delta) @@ -166,7 +166,7 @@ def workup_file_writer( # write delta equations col = delta_col - for it, name in enumerate(int_names): + for it, _ in enumerate(int_names): norm_iso_name = norm_names[it] if ( norm_iso_name is not None and norm_iso_name in int_names From 7672bb0b39fc7e0036df809f58f5947aea8bce1a Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Mon, 9 May 2022 19:15:14 -0400 Subject: [PATCH 4/6] Update docs --- docs/dev/api/data_io.rst | 14 ++++++++++++++ docs/pkg/special.rst | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/docs/dev/api/data_io.rst b/docs/dev/api/data_io.rst index 3f62f74..9718902 100644 --- a/docs/dev/api/data_io.rst +++ b/docs/dev/api/data_io.rst @@ -99,6 +99,20 @@ for processing `LST` files. +.. py:currentmodule:: rimseval.data_io.excel_writer + +------------ +Excel Writer +------------ + +************************** +:func:`workup_file_writer` +************************** + +.. autofunction:: workup_file_writer + + + .. py:currentmodule:: rimseval.data_io.export ---------------- diff --git a/docs/pkg/special.rst b/docs/pkg/special.rst index 3c709c3..6d054c5 100644 --- a/docs/pkg/special.rst +++ b/docs/pkg/special.rst @@ -6,6 +6,47 @@ The ``rimseval`` package has some special functions incorporated, which allow you to perform various analysis on your data and your RIMS setup. +------------------------ +Excel Workup file writer +------------------------ + +Sometimes, +it is useful to further process multiple evaluated spectra +in an Excel file. +You can use the +:meth:`rimseval.data_io.excel_writer.workup_file_writer` +to create a workbook to continue working on your data. +This workbook will contain integrals, +:math:`\delta`-values. + +Here is an example usage for a file +that would contain zirconium data. +Note that :math:`\delta`-values would usually +be normalized to the most abundant :sup:`90`\Zr, +however, we would like to normalize here +to :sup:`94`\Zr. +This is accomplished by setting the according +normalization isotope of ``iniabu``. + +The following code assumes that ``crd`` is +an instance of ``CRDFileProcessor`` +and that a mass calibration has been performed +and integrals have been set for zirconium isotopes. + +.. code-block:: python + + from pathlib import Path + + from rimseval.data_io import excel_writer + from rimseval.utilities import ini + + # set Zr normalization isotope to Zr-94 + ini.norm_isos = {"Zr": "Zr-94"} + + # Write the excel file + my_output_file = Path("workup_file.xlsx") + excel_writer.workup_file_writer(crd, my_output_file) + ------------------- Hist. ions per shot ------------------- From c1540a59f9f0ba15c344ee8945a3fe730f1248eb Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Mon, 9 May 2022 19:21:25 -0400 Subject: [PATCH 5/6] bump version --- .bumpversion.cfg | 2 +- docs/conf.py | 2 +- rimseval/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9ebe549..a379583 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.0.dev5 +current_version = 2.0.0.dev6 commit = False tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? diff --git a/docs/conf.py b/docs/conf.py index 973cb64..4344dda 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = "RIMSEval" author = "Reto Trappitsch" copyright = f"2021, {author}" -version = "2.0.0.dev5" +version = "2.0.0.dev6" release = version diff --git a/rimseval/__init__.py b/rimseval/__init__.py index 6a32134..0f729bb 100644 --- a/rimseval/__init__.py +++ b/rimseval/__init__.py @@ -22,7 +22,7 @@ ] # Package information -__version__ = "2.0.0.dev5" +__version__ = "2.0.0.dev6" __title__ = "rimseval" __description__ = ( From 8792be979b0458e8b3aca5a4126a7046fff0dde0 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Mon, 9 May 2022 19:24:19 -0400 Subject: [PATCH 6/6] Update bumpversion configuration to not push tag --- .bumpversion.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a379583..a802d23 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] current_version = 2.0.0.dev6 commit = False -tag = True +tag = false parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\.(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}.{release}{build}