From a5d6d74f393dd4e7cf6ae2cb5af5cc624f85874c Mon Sep 17 00:00:00 2001 From: Lan Le Date: Thu, 29 Feb 2024 12:44:50 +0100 Subject: [PATCH] feat: update to handle shift data for cv layout --- chem_spectra/controller/transform_api.py | 4 +- chem_spectra/lib/composer/ni.py | 34 +++- chem_spectra/lib/converter/share.py | 16 +- chem_spectra/lib/shared/calc.py | 10 +- chem_spectra/model/transformer.py | 49 ++++- setup.py | 2 +- tests/lib/converter/test_share.py | 219 ++++++++++++++++++++++- tests/lib/shared/test_calc.py | 12 +- 8 files changed, 330 insertions(+), 16 deletions(-) diff --git a/chem_spectra/controller/transform_api.py b/chem_spectra/controller/transform_api.py index 2ae439c2..8bdb8231 100644 --- a/chem_spectra/controller/transform_api.py +++ b/chem_spectra/controller/transform_api.py @@ -203,8 +203,10 @@ def combine_images(): list_files.append(file_container) params = extract_params(request) + extras = request.form.get('extras', default=None) + transform_model = TraModel(None, params=params, multiple_files=list_files) - tf_combine = transform_model.tf_combine(list_file_names=params['list_file_names']) + tf_combine = transform_model.tf_combine(list_file_names=params['list_file_names'], extraParams=extras) if (not tf_combine): abort(400) diff --git a/chem_spectra/lib/composer/ni.py b/chem_spectra/lib/composer/ni.py index 6d30bd0c..25779588 100644 --- a/chem_spectra/lib/composer/ni.py +++ b/chem_spectra/lib/composer/ni.py @@ -285,6 +285,12 @@ def tf_img(self): ] codes, verts = zip(*path_data) marker = mpath.Path(verts, codes) + + circle = mpath.Path.unit_circle() + cirle_verts = np.concatenate([circle.vertices, verts]) + cirle_codes = np.concatenate([circle.codes, codes]) + cut_star_marker = mpath.Path(cirle_verts, cirle_codes) + x_peaks = [] y_peaks = [] if self.core.edit_peaks: @@ -296,6 +302,7 @@ def tf_img(self): x_peckers = [] y_peckers = [] + x_peaks_ref, y_peaks_ref = [], [] if self.core.is_cyclic_volta: x_peaks = [] y_peaks = [] @@ -328,8 +335,13 @@ def tf_img(self): x_peaks.extend([x_max_peak]) y_peaks.extend([y_max_peak]) else: - x_peaks.extend([x_max_peak, x_min_peak]) - y_peaks.extend([y_max_peak, y_min_peak]) + is_ref = peak.get('isRef', False) if 'isRef' in peak else False + if is_ref: + x_peaks_ref.extend([x_max_peak, x_min_peak]) + y_peaks_ref.extend([y_max_peak, y_min_peak]) + else: + x_peaks.extend([x_max_peak, x_min_peak]) + y_peaks.extend([y_max_peak, y_min_peak]) if 'pecker' in peak and peak['pecker'] is not None: pecker = peak['pecker'] @@ -346,6 +358,15 @@ def tf_img(self): peak_label = 'x: {x}\ny: {y}'.format(x=x_float, y=y_float) plt.text(x_pos, y_pos, peak_label) + # display x value of ref peak for cyclic voltammetry + for i in range(len(x_peaks_ref)): + x_pos = x_peaks_ref[i] + y_pos = y_peaks_ref[i] + h * 0.1 + x_float = '{:.2e}'.format(x_pos) + y_float = '{:.2e}'.format(y_peaks_ref[i]) + peak_label = 'x: {x}\ny: {y}'.format(x=x_float, y=y_float) + plt.text(x_pos, y_pos, peak_label) + plt.plot( x_peaks, y_peaks, @@ -364,6 +385,15 @@ def tf_img(self): markersize=50, ) + plt.plot( + x_peaks_ref, + y_peaks_ref, + 'r', + ls='', + marker=cut_star_marker, + markersize=50, + ) + # ----- PLOT integration ----- refShift, refArea = self.refShift, self.refArea itg_h = y_max + h * 0.1 diff --git a/chem_spectra/lib/converter/share.py b/chem_spectra/lib/converter/share.py index ac0de974..7766d420 100644 --- a/chem_spectra/lib/converter/share.py +++ b/chem_spectra/lib/converter/share.py @@ -33,23 +33,35 @@ def parse_params(params): peaks_str = params.get('peaks_str', None) delta = 0.0 mass = params.get('mass', 0) + mass = mass if mass else 0 scan = params.get('scan', None) thres = params.get('thres', None) clear = params.get('clear', False) + clear = clear if clear else False integration = params.get('integration') integration = json.loads(integration) if integration else default_itg multiplicity = params.get('multiplicity') multiplicity = json.loads(multiplicity) if multiplicity else default_mpy ext = params.get('ext', '') + ext = ext if ext else '' fname = params.get('fname', '').split('.') fname = fname[:-2] if (len(fname) > 2 and (fname[-2] in ['edit', 'peak'])) else fname[:-1] 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) + jcamp_idx = jcamp_idx if jcamp_idx else 0 + axesUnitsJson = params.get('axesUnits') + axesUnitsDic = json.loads(axesUnitsJson) if axesUnitsJson else None + axesUnits = None + if axesUnitsDic != None and 'axes' in axesUnitsDic: + axes = axesUnitsDic.get('axes', [{'xUnit': '', 'yUnit': ''}]) + try: + axesUnits = axes[jcamp_idx] + except: + pass + cyclicvolta = params.get('cyclic_volta') cyclicvolta = json.loads(cyclicvolta) if cyclicvolta else None listMaxMinPeaks = None diff --git a/chem_spectra/lib/shared/calc.py b/chem_spectra/lib/shared/calc.py index c4494b92..80cd4a37 100644 --- a/chem_spectra/lib/shared/calc.py +++ b/chem_spectra/lib/shared/calc.py @@ -149,11 +149,9 @@ def cal_cyclic_volta_shift_prev_offset_at_index(cyclic_data, index=0): 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'] + hasRefPeak = spectra.get('hasRefPeak', False) == True + shift = spectra['shift'] + if 'prevValue' in shift: + offset = shift['prevValue'] if hasRefPeak else 0.0 return offset diff --git a/chem_spectra/model/transformer.py b/chem_spectra/model/transformer.py index b42c926c..ea2211e1 100644 --- a/chem_spectra/model/transformer.py +++ b/chem_spectra/model/transformer.py @@ -19,6 +19,8 @@ from chem_spectra.lib.composer.base import BaseComposer # noqa: F401 from chem_spectra.lib.converter.nmrium.base import NMRiumDataConverter import matplotlib.pyplot as plt # noqa: E402 +import matplotlib.path as mpath # noqa: E402 +import numpy as np # noqa: E402 from chem_spectra.model.concern.property import decorate_sim_property @@ -268,13 +270,45 @@ def tf_nmrium(self): nicp = NIComposer(converter) tf_jcamp = nicp.tf_jcamp() return tf_jcamp - - def tf_combine(self, list_file_names=None): + + def __get_cyclic_volta_ref_peaks(self, curve_idx, extraParams): + x_peaks, y_peaks = [], [] + try: + extras_dict = json.loads(extraParams) if extraParams else None + cyclic_volta_str = extras_dict['cyclicvolta'] if extras_dict else None + cyclic_volta = json.loads(cyclic_volta_str) if cyclic_volta_str else None + spectra_list = cyclic_volta['spectraList'] if cyclic_volta else None + spectra_extra = spectra_list[curve_idx] if spectra_list and curve_idx < len(spectra_list) else None + list_peaks = spectra_extra['list'] if spectra_extra else [] + x_peaks, y_peaks = [], [] + for peak in list_peaks: + min_peak, max_peak, isRef = peak['min'], peak['max'], peak['isRef'] + if isRef == True: + x_peaks.extend([min_peak['x'], max_peak['x']]) + y_peaks.extend([min_peak['y'], max_peak['y']]) + except: + pass + + return x_peaks, y_peaks + + def tf_combine(self, list_file_names=None, extraParams=None): if not self.multiple_files: return False + + path_data = [ + (mpath.Path.MOVETO, (0, 5)), + (mpath.Path.LINETO, (0, 20)), + ] + codes, verts = zip(*path_data) + + circle = mpath.Path.unit_circle() + cirle_verts = np.concatenate([circle.vertices, verts]) + cirle_codes = np.concatenate([circle.codes, codes]) + cut_star_marker = mpath.Path(cirle_verts, cirle_codes) plt.rcParams['figure.figsize'] = [16, 9] plt.rcParams['font.size'] = 14 + plt.rcParams['legend.loc'] = 'upper left' curve_idx = self.params.get('jcamp_idx', 0) xlabel, ylabel = '', '' @@ -315,6 +349,17 @@ def tf_combine(self, list_file_names=None): core_label_x = nicp.core.label['x'] core_label_y = nicp.core.label['y'] if nicp.core.is_cyclic_volta: + x_peaks, y_peaks = self.__get_cyclic_volta_ref_peaks(curve_idx, extraParams) + + plt.plot( + x_peaks, + y_peaks, + 'r', + ls='', + marker=cut_star_marker, + markersize=50, + ) + if core_label_x not in dic_x_label: xlabel_set.append(core_label_x) dic_x_label[core_label_x] = 1 diff --git a/setup.py b/setup.py index d4d01c1e..fc5db5fc 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='chem-spectra-app', - version='1.1.1', + version='1.2.0', packages=find_packages(), include_package_data=True, zip_safe=False, diff --git a/tests/lib/converter/test_share.py b/tests/lib/converter/test_share.py index b2adf166..cd19ba11 100644 --- a/tests/lib/converter/test_share.py +++ b/tests/lib/converter/test_share.py @@ -1,9 +1,226 @@ import numpy as np +import pytest from chem_spectra.lib.converter.share import ( - parse_solvent, reduce_pts + parse_params, parse_solvent, reduce_pts ) +@pytest.fixture +def expected_default_params(): + default_itg = {'stack': [], 'refArea': 1, 'refFactor': 1, 'shift': 0} + default_mpy = {'stack': [], 'smExtext': False, 'shift': 0} + default_wavelength = {'name': 'CuKalpha', 'value': 0.15406, 'label': 'Cu K-alpha', 'unit': 'nm'} + return { + 'select_x': None, + 'ref_name': None, + 'ref_value': None, + 'peaks_str': None, + 'delta': 0.0, + 'mass': 0, + 'scan': None, + 'thres': None, + 'clear': False, + 'integration': default_itg, + 'multiplicity': default_mpy, + 'fname': '', + 'waveLength': default_wavelength, + 'list_max_min_peaks': None, + 'cyclicvolta': None, + 'jcamp_idx': 0, + 'axesUnits': None, + 'detector': None, + } + +def test_parse_params_without_params(expected_default_params): + parsed_data = parse_params(None) + assert expected_default_params == parsed_data + +def test_parse_params_select_x(): + params = {'select_x': None} + parsed_data = parse_params(params) + assert parsed_data['select_x'] is None + + params = {'select_x': 0.0} + parsed_data = parse_params(params) + assert parsed_data['select_x'] == 0.0 + +def test_parse_params_ref_name(): + params = {'ref_name': None} + parsed_data = parse_params(params) + assert parsed_data['ref_name'] is None + + params = {'ref_name': 'just a text'} + parsed_data = parse_params(params) + assert parsed_data['ref_name'] == 'just a text' + +def test_parse_params_ref_value(): + params = {'ref_value': None} + parsed_data = parse_params(params) + assert parsed_data['ref_value'] is None + + params = {'ref_value': 0.0} + parsed_data = parse_params(params) + assert parsed_data['ref_value'] == 0.0 + +def test_parse_params_peaks_str(): + params = {'peaks_str': None} + parsed_data = parse_params(params) + assert parsed_data['peaks_str'] is None + + params = {'peaks_str': 'just a text'} + parsed_data = parse_params(params) + assert parsed_data['peaks_str'] == 'just a text' + +def test_parse_params_delta(): + params = {'select_x': None} + parsed_data = parse_params(params) + assert parsed_data['delta'] == 0.0 + + params = {'select_x': 0.0} + parsed_data = parse_params(params) + assert parsed_data['delta'] == 0.0 + + params = {'select_x': 1.0, 'ref_name': '- - -'} + parsed_data = parse_params(params) + assert parsed_data['delta'] == 0.0 + + params = {'select_x': 1.0, 'ref_name': 'just a text'} + parsed_data = parse_params(params) + assert parsed_data['delta'] == 0.0 + + params = {'select_x': 1.0, 'ref_name': 'just a text', 'ref_value': 1.5} + parsed_data = parse_params(params) + assert parsed_data['delta'] == 0.5 + +def test_parse_params_mass(): + params = {'mass': None} + parsed_data = parse_params(params) + assert parsed_data['mass'] == 0 + + params = {'mass': 1.5} + parsed_data = parse_params(params) + assert parsed_data['mass'] == 1.5 + +def test_parse_params_scan(): + params = {'scan': None} + parsed_data = parse_params(params) + assert parsed_data['scan'] is None + + params = {'scan': 2} + parsed_data = parse_params(params) + assert parsed_data['scan'] == 2 + +def test_parse_params_thres(): + params = {'thres': None} + parsed_data = parse_params(params) + assert parsed_data['thres'] is None + + params = {'thres': 2} + parsed_data = parse_params(params) + assert parsed_data['thres'] == 2 + +def test_parse_params_clear(): + params = {'clear': None} + parsed_data = parse_params(params) + assert parsed_data['clear'] == False + + params = {'clear': False} + parsed_data = parse_params(params) + assert parsed_data['clear'] == False + + params = {'clear': True} + parsed_data = parse_params(params) + assert parsed_data['clear'] == True + +def test_parse_params_ext(): + params = {'ext': None} + parsed_data = parse_params(params) + assert parsed_data['ext'] == '' + + params = {'ext': '.jdx'} + parsed_data = parse_params(params) + assert parsed_data['ext'] == '.jdx' + +def test_parse_params_fname(): + params = {'fname': 'just a normal text'} + parsed_data = parse_params(params) + assert parsed_data['fname'] == '' + + params = {'fname': 'original.jdx'} + parsed_data = parse_params(params) + assert parsed_data['fname'] == 'original' + + params = {'fname': 'original.changed.jdx'} + parsed_data = parse_params(params) + assert parsed_data['fname'] == 'original.changed' + + params = {'fname': 'original.peak.jdx'} + parsed_data = parse_params(params) + assert parsed_data['fname'] == 'original' + + params = {'fname': 'original.edit.jdx'} + parsed_data = parse_params(params) + assert parsed_data['fname'] == 'original' + +def test_parse_params_integration(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_multiplicity(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_waveLength(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_list_max_min_peaks(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_cyclic_volta(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_jcamp_idx(): + params = {'jcamp_idx': None} + parsed_data = parse_params(params) + assert parsed_data['jcamp_idx'] == 0 + + params = {'jcamp_idx': 1} + parsed_data = parse_params(params) + assert parsed_data['jcamp_idx'] == 1 + +def test_parse_params_axesUnits(): + params = {'axesUnits': None} + parsed_data = parse_params(params) + assert parsed_data['axesUnits'] is None + + params = {'axesUnits': '{"axes": null}'} + parsed_data = parse_params(params) + assert parsed_data['axesUnits'] is None + + params = {'axesUnits': '{"axes": []}'} + parsed_data = parse_params(params) + assert parsed_data['axesUnits'] is None + + params = {'jcamp_idx': 1, 'axesUnits': '{"axes": [{"xUnit": "label x", "yUnit": "label y"}]}'} + parsed_data = parse_params(params) + assert parsed_data['axesUnits'] is None + + params = {'axesUnits': '{"axes": [{"xUnit": "label x", "yUnit": "label y"}]}'} + parsed_data = parse_params(params) + assert parsed_data['axesUnits'] == {"xUnit": "label x", "yUnit": "label y"} + +def test_parse_params_data_type_mapping(): + #TODO: need to be updated + assert 1==1 + +def test_parse_params_detector(): + #TODO: need to be updated + assert 1==1 + def test_parse_solvent(): + #TODO: need to be updated assert 1==1 def test_reduce_pts_when_does_not_have_any_x(): diff --git a/tests/lib/shared/test_calc.py b/tests/lib/shared/test_calc.py index 0aa33da9..eaccfc47 100644 --- a/tests/lib/shared/test_calc.py +++ b/tests/lib/shared/test_calc.py @@ -105,10 +105,20 @@ def test_cal_area_multiplicity(): @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}}]} + 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,}, 'hasRefPeak':True},{'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 + +@pytest.fixture +def cyclic_data_no_ref_value(): + 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,}, 'hasRefPeak':False},{'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 + +def test_cal_cyclic_volta_shift_prev_offset_at_index_no_ref_value(cyclic_data_no_ref_value): + expected_offset = 0 + offset = cal_cyclic_volta_shift_prev_offset_at_index(cyclic_data_no_ref_value, 0) + assert offset == expected_offset