From 17b0f7d56a1daf938f75859e588b230233268c17 Mon Sep 17 00:00:00 2001 From: Lan Le Date: Fri, 6 Oct 2023 11:58:08 +0200 Subject: [PATCH] feat: add cv data with offset --- chem_spectra/controller/helper/share.py | 2 + chem_spectra/controller/transform_api.py | 2 +- chem_spectra/lib/composer/ni.py | 81 +++++++++++++++--------- chem_spectra/lib/converter/jcamp/ni.py | 27 +++++++- chem_spectra/lib/converter/share.py | 8 +++ chem_spectra/lib/shared/calc.py | 57 +++++++++++++++++ chem_spectra/model/transformer.py | 34 ++++++++-- setup.py | 2 +- tests/lib/shared/test_calc.py | 11 ++++ 9 files changed, 187 insertions(+), 37 deletions(-) diff --git a/chem_spectra/controller/helper/share.py b/chem_spectra/controller/helper/share.py index 8eb05405..90380187 100644 --- a/chem_spectra/controller/helper/share.py +++ b/chem_spectra/controller/helper/share.py @@ -101,6 +101,7 @@ def extract_params(request): # simulatenmr = bool(request.form.get('simulatenmr', default=False)) simulatenmr = request.form.get('simulatenmr', default=False) == 'true' waveLength = request.form.get('wave_length', default=None) + axesUnits = request.form.get('axes_units', default=None) cyclicvolta = request.form.get('cyclic_volta', default=None) jcamp_idx = parse_int(request.form.get('jcamp_idx', default=0), 0) list_file_names = request.form.getlist('list_file_names[]') @@ -125,6 +126,7 @@ def extract_params(request): 'cyclic_volta': cyclicvolta, 'jcamp_idx': jcamp_idx, 'list_file_names': list_file_names, + 'axesUnits': axesUnits, } has_params = ( params.get('peaks_str') or diff --git a/chem_spectra/controller/transform_api.py b/chem_spectra/controller/transform_api.py index f25706e9..2ae439c2 100644 --- a/chem_spectra/controller/transform_api.py +++ b/chem_spectra/controller/transform_api.py @@ -203,7 +203,7 @@ def combine_images(): list_files.append(file_container) params = extract_params(request) - transform_model = TraModel(None, params=params, multiple_files=list_files) + transform_model = TraModel(None, params=params, multiple_files=list_files) tf_combine = transform_model.tf_combine(list_file_names=params['list_file_names']) if (not tf_combine): abort(400) diff --git a/chem_spectra/lib/composer/ni.py b/chem_spectra/lib/composer/ni.py index de10840f..2cbee17e 100644 --- a/chem_spectra/lib/composer/ni.py +++ b/chem_spectra/lib/composer/ni.py @@ -16,7 +16,7 @@ extrac_dic, calc_npoints, BaseComposer ) from chem_spectra.lib.shared.calc import ( # noqa: E402 - calc_mpy_center, calc_ks, get_curve_endpoint, cal_slope, cal_xyIntegration + calc_mpy_center, calc_ks, get_curve_endpoint, cal_slope, cal_xyIntegration, ) @@ -141,6 +141,8 @@ def __gen_cyclic_voltammetry_data_peaks(self): content = ['##$CSCYCLICVOLTAMMETRYDATA=\n'] if self.core.is_cyclic_volta: listMaxMinPeaks = self.core.max_min_peaks_table + cyclicvolta_data = self.core.params['cyclicvolta'] + current_jcamp_idx = self.core.params['jcamp_idx'] if self.core.params['list_max_min_peaks'] is not None: listMaxMinPeaks = self.core.params['list_max_min_peaks'] @@ -152,11 +154,22 @@ def __gen_cyclic_voltammetry_data_peaks(self): y_pecker = self.core.ys[idx] for peak in listMaxMinPeaks: - max_peak, min_peak = peak['max'], peak['min'] + max_peak, min_peak = None, None + if 'max' in peak: + max_peak = peak['max'] + if 'min' in peak: + min_peak = peak['min'] x_max_peak, y_max_peak = self.__get_xy_of_peak(max_peak) x_min_peak, y_min_peak = self.__get_xy_of_peak(min_peak) - if (x_max_peak == '' and x_min_peak == ''): + x_pecker = '' + if 'pecker' in peak and peak['pecker'] is not None: + pecker = peak['pecker'] + x_pecker = pecker['x'] + y_pecker = pecker['y'] + x_pecker = f"{float(x_pecker)}" + + if (x_max_peak == '' and x_min_peak == '' and x_pecker == ''): # ignore if missing both max and min peak continue @@ -164,24 +177,21 @@ def __gen_cyclic_voltammetry_data_peaks(self): delta = '' else: delta = abs(x_max_peak - x_min_peak) - - x_pecker = '' + # calculate ratio if (y_min_peak == '' or y_max_peak == ''): ratio = '' else: - if 'pecker' in peak and peak['pecker'] is not None: - pecker = peak['pecker'] - x_pecker = pecker['x'] - y_pecker = pecker['y'] first_expr = abs(y_min_peak) / abs(y_max_peak) second_expr = 0.485 * abs(y_pecker) / abs(y_max_peak) ratio = first_expr + second_expr + 0.086 if (y_pecker) == 0: y_pecker = '' + is_ref = peak.get('isRef', False) + is_ref_in_number = 1 if is_ref else 0 content.append( - '({x_max}, {y_max}, {x_min}, {y_min}, {ratio}, {delta}, {x_pecker}, {y_pecker})\n'.format(x_max=x_max_peak, y_max=y_max_peak, x_min=x_min_peak, y_min=y_min_peak, ratio=ratio, delta=delta, x_pecker=x_pecker, y_pecker=y_pecker) # noqa: E501 + '({x_max}, {y_max}, {x_min}, {y_min}, {ratio}, {delta}, {x_pecker}, {y_pecker}, {is_ref})\n'.format(x_max=x_max_peak, y_max=y_max_peak, x_min=x_min_peak, y_min=y_min_peak, ratio=ratio, delta=delta, x_pecker=x_pecker, y_pecker=y_pecker, is_ref=is_ref_in_number) # noqa: E501 ) return content @@ -256,6 +266,7 @@ def tf_img(self): # PLOT data plt.plot(self.core.xs, self.core.ys) x_max, x_min = self.core.boundary['x']['max'], self.core.boundary['x']['min'] # noqa: E501 + xlim_left, xlim_right = [x_min, x_max] if (self.core.is_tga or self.core.is_uv_vis or self.core.is_hplc_uv_vis or self.core.is_xrd or self.core.is_cyclic_volta or self.core.is_sec or self.core.is_cds or self.core.is_aif or self.core.is_emissions or self.core.is_dls_acf or self.core.is_dls_intensity) else [x_max, x_min] # noqa: E501 plt.xlim(xlim_left, xlim_right) y_max, y_min = np.max(self.core.ys), np.min(self.core.ys) @@ -296,7 +307,11 @@ def tf_img(self): listMaxMinPeaks = self.core.params['list_max_min_peaks'] for peak in listMaxMinPeaks: - max_peak, min_peak = peak['max'], peak['min'] + max_peak, min_peak = None, None + if 'max' in peak: + max_peak = peak['max'] + if 'min' in peak: + min_peak = peak['min'] x_max_peak, y_max_peak = self.__get_xy_of_peak(max_peak) x_min_peak, y_min_peak = self.__get_xy_of_peak(min_peak) @@ -470,26 +485,26 @@ def tf_img(self): def __prepare_metadata_info_for_csv(self, csv_writer: csv.DictWriter): csv_writer.writerow({ - 'Max x': 'Measurement type', - 'Max y': 'Cyclic Voltammetry', + 'Ox E(V)': 'Measurement type', + 'Red E(V)': 'Cyclic Voltammetry', }) csv_writer.writerow({ - 'Max x': 'Measurement type ID', + 'Ox E(V)': 'Measurement type ID', }) csv_writer.writerow({ - 'Max x': 'Sample ID', + 'Ox E(V)': 'Sample ID', }) csv_writer.writerow({ - 'Max x': 'Analysis ID', + 'Ox E(V)': 'Analysis ID', }) csv_writer.writerow({ - 'Max x': 'Dataset ID', + 'Ox E(V)': 'Dataset ID', }) csv_writer.writerow({ - 'Max x': 'Dataset name', + 'Ox E(V)': 'Dataset name', }) csv_writer.writerow({ - 'Max x': 'Link to sample', + 'Ox E(V)': 'Link to sample', }) csv_writer.writerow({ }) @@ -507,8 +522,7 @@ def tf_csv(self): listMaxMinPeaks = self.core.params['list_max_min_peaks'] with open(tf_csv.name, 'w', newline='', encoding='utf-8') as csvfile: - # fieldnames = ['Max', 'Min', 'I λ0', 'I ratio', 'Pecker'] - fieldnames = ['Max x', 'Max y', 'Min x', 'Min y', 'Delta Ep', 'I lambda0', 'I ratio'] + fieldnames = ['Ox E(V)', 'Ox I(A)', 'Red E(V)', 'Red I(A)', 'I lambda0(A)', 'I ratio', 'E1/2(V)', 'Delta Ep(mV)'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) self.__prepare_metadata_info_for_csv(writer) @@ -523,7 +537,13 @@ def tf_csv(self): y_pecker = self.core.ys[idx] for peak in listMaxMinPeaks: - max_peak, min_peak = peak['max'], peak['min'] + max_peak, min_peak, e12 = None, None, None + if 'max' in peak: + max_peak = peak['max'] + if 'min' in peak: + min_peak = peak['min'] + if 'e12' in peak: + e12 = peak['e12'] x_max_peak, y_max_peak = self.__get_xy_of_peak(max_peak) x_min_peak, y_min_peak = self.__get_xy_of_peak(min_peak) @@ -534,7 +554,7 @@ def tf_csv(self): if (x_max_peak == '' or x_min_peak == ''): delta = '' else: - delta = abs(x_max_peak - x_min_peak) + delta = abs(x_max_peak - x_min_peak) * 1000 x_pecker = '' # calculate ratio @@ -552,13 +572,14 @@ def tf_csv(self): y_pecker = '' writer.writerow({ - 'Max x': '{x_max}'.format(x_max=x_max_peak), - 'Max y': '{y_max}'.format(y_max=y_max_peak), - 'Min x': '{x_min}'.format(x_min=x_min_peak), - 'Min y': '{y_min}'.format(y_min=y_min_peak), - 'Delta Ep': '{y_pecker}'.format(y_pecker=y_pecker), - 'I lambda0': '{ratio}'.format(ratio=ratio), - 'I ratio': '{delta}'.format(delta=delta) + 'Ox E(V)': '{x_max}'.format(x_max=x_max_peak), + 'Ox I(A)': '{y_max}'.format(y_max=y_max_peak), + 'Red E(V)': '{x_min}'.format(x_min=x_min_peak), + 'Red I(A)': '{y_min}'.format(y_min=y_min_peak), + 'I lambda0(A)': '{y_pecker}'.format(y_pecker=y_pecker), + 'I ratio': '{ratio}'.format(ratio=ratio), + 'E1/2(V)': '{e12}'.format(e12=e12), + 'Delta Ep(mV)': '{delta}'.format(delta=delta) }) return tf_csv diff --git a/chem_spectra/lib/converter/jcamp/ni.py b/chem_spectra/lib/converter/jcamp/ni.py index 9ffec181..15d8dc93 100644 --- a/chem_spectra/lib/converter/jcamp/ni.py +++ b/chem_spectra/lib/converter/jcamp/ni.py @@ -2,7 +2,7 @@ from scipy import signal from chem_spectra.lib.converter.datatable import DatatableModel -from chem_spectra.lib.shared.calc import to_float +from chem_spectra.lib.shared.calc import (to_float, cal_cyclic_volta_shift_prev_offset_at_index) from chem_spectra.lib.converter.jcamp.data_parse import make_ni_data_ys, make_ni_data_xs @@ -54,6 +54,8 @@ def __init__(self, base): self.x_unit = self.__set_x_unit() self.ys = self.__read_ys() self.xs = self.__read_xs(base) + self.__check_cylic_volta_shifted_info() + self.factor = self.__set_factor(base) self.__set_first_last_xs() self.clear = self.__refresh_solvent() @@ -250,6 +252,14 @@ def __set_label(self): target['y'] = 'TRANSMITTANCE' if (self.is_xrd): target['x'] = '2Theta' + + if 'axesUnits' in self.params and self.params['axesUnits'] is not None: + axesUnits = self.params['axesUnits'] + xUnit, yUnit = axesUnits['xUnit'], axesUnits['yUnit'] + if xUnit != '': + target['x'] = xUnit + if yUnit != '': + target['y'] = yUnit return target @@ -302,6 +312,12 @@ def __set_factor(self, base): def __set_x_unit(self): x_unit = None + + if 'axesUnits' in self.params and self.params['axesUnits'] is not None: + axesUnits = self.params['axesUnits'] + xUnit = axesUnits['xUnit'] + if xUnit != '': + return xUnit try: # jcamp version 6 units = self.dic['UNITS'] @@ -634,3 +650,12 @@ def __refresh_solvent(self): def __set_first_last_xs(self): self.first_x = self.xs[0] self.last_x = self.xs[-1] + + def __check_cylic_volta_shifted_info(self): + if self.is_cyclic_volta == False: + return + + cyclicvolta_data = self.params['cyclicvolta'] + current_jcamp_idx = self.params['jcamp_idx'] + offset = cal_cyclic_volta_shift_prev_offset_at_index(cyclicvolta_data, current_jcamp_idx) + self.xs = np.array([x - offset for x in self.xs]) diff --git a/chem_spectra/lib/converter/share.py b/chem_spectra/lib/converter/share.py index 4db1001e..5af39f85 100644 --- a/chem_spectra/lib/converter/share.py +++ b/chem_spectra/lib/converter/share.py @@ -21,6 +21,9 @@ def parse_params(params): 'fname': '', 'waveLength': default_wavelength, 'list_max_min_peaks': None, + 'cyclicvolta': None, + 'jcamp_idx': 0, + 'axesUnits': None, } select_x = params.get('select_x', None) @@ -42,6 +45,8 @@ def parse_params(params): fname = '.'.join(fname) waveLength = params.get('waveLength') waveLength = json.loads(waveLength) if waveLength else default_wavelength + axesUnits = params.get('axesUnits') + axesUnits = json.loads(axesUnits) if axesUnits else None jcamp_idx = params.get('jcamp_idx', 0) cyclicvolta = params.get('cyclic_volta') @@ -75,6 +80,9 @@ def parse_params(params): 'fname': fname, 'waveLength': waveLength, 'list_max_min_peaks': listMaxMinPeaks, + 'cyclicvolta': cyclicvolta, + 'jcamp_idx': jcamp_idx, + 'axesUnits': axesUnits, } diff --git a/chem_spectra/lib/shared/calc.py b/chem_spectra/lib/shared/calc.py index 3f66486e..c4494b92 100644 --- a/chem_spectra/lib/shared/calc.py +++ b/chem_spectra/lib/shared/calc.py @@ -100,3 +100,60 @@ def cal_area_multiplicity(xL, xU, data_xs, data_ys): lower_value = Decimal(str(ks[iL])) return float(abs(upper_value - lower_value)) + +def cal_cyclic_volta_shift_offset(cyclic_data): + if cyclic_data is None: + return [] + if isinstance(cyclic_data, dict) == False: + return [] + if 'spectraList' not in cyclic_data: + return [] + + spectra_list = cyclic_data['spectraList'] + if isinstance(spectra_list, list) == False: + return [] + + list_offsets = [] + + for spectra in spectra_list: + offset = 0.0 + analysed_data = spectra['list'] + arr_has_ref_value = list(filter(lambda x: x['isRef'] == True, analysed_data)) + if len(arr_has_ref_value) > 0: + shift = spectra['shift'] + val = shift['val'] + ref_value = arr_has_ref_value[0] + e12 = ref_value['e12'] + offset = e12 - val + list_offsets.append(offset) + + return list_offsets + +def cal_cyclic_volta_shift_prev_offset_at_index(cyclic_data, index=0): + if cyclic_data is None: + return 0.0 + if isinstance(cyclic_data, dict) == False: + return 0.0 + if 'spectraList' not in cyclic_data: + return 0.0 + + spectra_list = cyclic_data['spectraList'] + if isinstance(spectra_list, list) == False: + return 0.0 + if len(spectra_list) < index: + return 0.0 + + offset = 0.0 + + if index == len(spectra_list): + return 0.0 + + spectra = spectra_list[index] + analysed_data = spectra['list'] + arr_has_ref_value = list(filter(lambda x: ('isRef' in x and x['isRef'] == True), analysed_data)) + if len(arr_has_ref_value) > 0: + shift = spectra['shift'] + if 'prevValue' in shift: + offset = shift['prevValue'] + + return offset diff --git a/chem_spectra/model/transformer.py b/chem_spectra/model/transformer.py index cad495ff..b42c926c 100644 --- a/chem_spectra/model/transformer.py +++ b/chem_spectra/model/transformer.py @@ -275,13 +275,23 @@ def tf_combine(self, list_file_names=None): plt.rcParams['figure.figsize'] = [16, 9] plt.rcParams['font.size'] = 14 + curve_idx = self.params.get('jcamp_idx', 0) + + xlabel, ylabel = '', '' + xlabel_set, ylabel_set = [], [] + dic_x_label, dic_y_label = {}, {} + + for idx, file in enumerate(self.multiple_files): + if (list_file_names is not None) and idx < len(list_file_names): + file.name = list_file_names[idx] + self.multiple_files[idx] = file + + self.multiple_files.sort(key=lambda file: file.name) for idx, file in enumerate(self.multiple_files): tf = store_str_in_tmp(file.core) jbcv = JcampBaseConverter(tf.name, self.params) filename = file.name - if (list_file_names is not None) and idx < len(list_file_names): - filename = list_file_names[idx] if jbcv.typ == 'MS': mscv = JcampMSConverter(jbcv) mscp = MSComposer(mscv) @@ -302,10 +312,26 @@ def tf_combine(self, list_file_names=None): plt.plot(xs, ys, label=filename, marker=marker) # PLOT label - plt.xlabel("X ({})".format(nicp.core.label['x']), fontsize=18) - plt.ylabel("Y ({})".format(nicp.core.label['y']), fontsize=18) + core_label_x = nicp.core.label['x'] + core_label_y = nicp.core.label['y'] + if nicp.core.is_cyclic_volta: + if core_label_x not in dic_x_label: + xlabel_set.append(core_label_x) + dic_x_label[core_label_x] = 1 + if core_label_y not in dic_y_label: + ylabel_set.append(core_label_y) + dic_y_label[core_label_y] = 1 + if (idx == len(self.multiple_files) - 1): + xlabel = ', '.join(xlabel_set) + ylabel = ', '.join(ylabel_set) + else: + xlabel = "X ({})".format(core_label_x) + ylabel = "Y ({})".format(core_label_y) + tf.close() + plt.xlabel(xlabel, fontsize=18) + plt.ylabel(ylabel, fontsize=18) plt.legend() tf_img = tempfile.NamedTemporaryFile(suffix='.png') plt.savefig(tf_img, format='png') diff --git a/setup.py b/setup.py index 5cfae772..bf2c6383 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='chem-spectra-app', - version='0.14.0', + version='0.15.0', packages=find_packages(), include_package_data=True, zip_safe=False, diff --git a/tests/lib/shared/test_calc.py b/tests/lib/shared/test_calc.py index ed6bbb12..0aa33da9 100644 --- a/tests/lib/shared/test_calc.py +++ b/tests/lib/shared/test_calc.py @@ -1,4 +1,5 @@ from chem_spectra.lib.shared.calc import * +import pytest # def test_calc_j(): # #TODO: implement later @@ -101,3 +102,13 @@ def test_cal_area_multiplicity(): data_ys = [2.0, 4.0] area = cal_area_multiplicity(xL, xU, data_xs=data_xs, data_ys=data_ys) assert area == 1.0 + +@pytest.fixture +def cyclic_data(): + cyclic_data = {'spectraList':[{'list':[{'min':{'x':-1.48904,'y':-1.10686e-05},'max':{'x':1.80895,'y':9.51171e-06},'isRef':True,'e12':0.15995500000000007,'pecker':{'x':-0.129871,'y':7.78418e-07}}],'selectedIdx':0,'isWorkMaxPeak':True,'jcampIdx':0,'shift':{'ref':None,'val':0,'prevValue': 2.5,}},{'list':[{'min':{'x':-1.48904,'y':-3.3747399999999995e-05},'max':{'x':0.929483,'y':0.00023741},'isRef':True,'e12':-0.27977849999999993}],'selectedIdx':0,'isWorkMaxPeak':True,'jcampIdx':1,'shift':{'ref':None,'val':5}},{'list':[{'min':{'x':0.45977,'y':-0.000226347},'max':{'x':1.00943,'y':0.000371349},'isRef':False,'e12':0.7346}],'selectedIdx':0,'isWorkMaxPeak':True,'jcampIdx':2,'shift':{'ref':None,'val':0}}]} + return cyclic_data + +def test_cal_cyclic_volta_shift_prev_offset_at_index(cyclic_data): + expected_offset = 2.5 + offset = cal_cyclic_volta_shift_prev_offset_at_index(cyclic_data, 0) + assert offset == expected_offset