Skip to content

Commit

Permalink
Merge pull request #13 from NMRTeamTBI/dev
Browse files Browse the repository at this point in the history
MultiNMRFit 2.1.2
  • Loading branch information
pierremillard authored Dec 5, 2024
2 parents f153d41 + d5dc85a commit d35aa2c
Show file tree
Hide file tree
Showing 16 changed files with 100 additions and 44 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ __pycache__/
/multinmrfit/ui/conf/default_conf.pickle
multinmrfit/ui/conf/default_conf.pickle
multinmrfit/last_version.txt
multinmrfit/models/model_acetate.py
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
[![Documentation Status](https://readthedocs.org/projects/multinmrfit/badge/?version=latest)](http://multinmrfit.readthedocs.io/?badge=latest)
[![Python 3.8+](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/)


## What is MultiNMRFit?
MultiNMRFit is a scientific tool designed to extract quantitative information (chemical shifts, signal intensity, coupling constants, etc) from a serie of 1D spectra (acquired individually or as pseudo 2D spectra) by fitting.

It is one of the routine tools that we use for NMR studies of metabolic systems at the [NMR](http://www.toulouse-biotechnology-institute.fr/en/research/physiology-and-engineering-of-microbial-metabolism/rmn.html) and [MetaSys](http://www.toulouse-biotechnology-institute.fr/en/research/physiology-and-engineering-of-microbial-metabolism/metasys.html) teams of the [Toulouse Biotechnology Institute](http://www.toulouse-biotechnology-institute.fr/en/).
MultiNMRFit is a scientific tool designed to extract quantitative information (chemical shifts, signal intensity, coupling constants, etc) by fitting a serie of 1D spectra (acquired individually or as pseudo 2D spectra).

It is one of the routine tools that we use for NMR studies of metabolic systems at the [NMR](https://www.toulouse-biotechnology-institute.fr/en/poles/equipe-rmn-2/) and [MetaSys](https://www.toulouse-biotechnology-institute.fr/en/poles/equipe-metasys/) teams of the [Toulouse Biotechnology Institute](http://www.toulouse-biotechnology-institute.fr/en/).

The code is open-source, and available under a GPLv3 license. Additional information will be available in an upcoming [publication](https://doi.org/xxx.xxx).

Detailed documentation can be found online at Read the Docs ([https://multinmrfit.readthedocs.io/](https://multinmrfit.readthedocs.io/)).

## Key features

* **fit series of 1D spectra** (acquired as individual 1D spectra, as a pseudo 2D spectrum, or provided as tabulated text files),
* can be used with **all nuclei** (<sup>1</sup>H, <sup>13</sup>C, <sup>15</sup>N, <sup>31</sup>P, etc),
* estimation of several parameters for each signal of interest (**intensity**, **area**, **chemical shift**, **linewidth**, **coupling constant(s)**, etc),
Expand All @@ -26,8 +27,8 @@ Detailed documentation can be found online at Read the Docs ([https://multinmrfi
* open-source, free and easy to install everywhere where Python 3 and pip run,
* biologist-friendly.


## Quick-start

MultiNMRFit requires Python 3.8 or higher and run on all platforms (Windows, MacOS and Unix).
Please check [the documentation](https://multinmrfit.readthedocs.io/en/latest/quickstart.html) for complete
installation and usage instructions.
Expand All @@ -47,20 +48,22 @@ $ multinmrfit
MultiNMRFit is also available as a Python library.

## Bug and feature requests

If you have an idea on how we could improve MultiNMRFit please submit a new *issue*
to [our GitHub issue tracker](https://github.com/NMRTeamTBI/MultiNMRFit/issues).


## Developers guide
### Contributions
Contributions are very welcome! :heart:

Contributions are very welcome! :heart:

### Local install with pip

In development mode, do a `pip install -e /path/to/MultiNMRFit` to install
locally the development version.

### Build the documentation locally

Build the HTML documentation with:

```bash
Expand All @@ -72,11 +75,14 @@ The PDF documentation can be built locally by replacing `html` by `latexpdf`
in the command above. You will need a recent latex installation.

## How to cite

In preparation, 2024, [doi: xxx.xxxx](https://doi.org/xxx.xxxx)

## Authors
Pierre Millard, Cyril Charlier

Pierre Millard, Cyril Charlier

## Contact
:email: [email protected]
:email: [email protected]

:email: [email protected]
:email: [email protected]
2 changes: 1 addition & 1 deletion multinmrfit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"""

# Version number MUST be maintained here (x.y.z format)
__version__ = '2.1.1'
__version__ = '2.1.2'
2 changes: 1 addition & 1 deletion multinmrfit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def main():
thread.start()
path_to_app = Path(multinmrfit.__file__).parent
path_to_app = path_to_app / "ui" / "01_Inputs_&_Outputs.py"
run(["streamlit", "run", str(path_to_app)])
run(["streamlit", "run", str(path_to_app), "--server.maxUploadSize", "4096"])


if __name__ == '__main__':
Expand Down
22 changes: 22 additions & 0 deletions multinmrfit/base/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ def compounds(self, rowno=None, region=None):
else:
compounds = self.results[rowno][region].params.signal_id.values.tolist()

# remove 'full spectrum'
compounds = [i for i in compounds if i != "full_spectrum"]

# remove duplicates
compounds = list(set(compounds))

Expand All @@ -374,6 +377,9 @@ def spectra(self, region=None):
@staticmethod
def update_cnstr_wd(params, cnstr_wd):

params.reset_index(inplace=True, drop=True)
cnstr_wd.reset_index(inplace=True, drop=True)

mask_rel = cnstr_wd['relative'].tolist()
mask_abs = [not i for i in mask_rel]

Expand All @@ -398,6 +404,19 @@ def update_cnstr_wd(params, cnstr_wd):
mask = params['par'].isin(['intensity'])
params.loc[mask, "lb"] = 1.0

# if some initial parameters are outside the bounds, adjust bounds
# identify params with negative initial values
mask_neg = (params['ini'] < 0)
mask_pos = [not i for i in mask_neg]
# set lower bounds
mask = (params['ini'] < params['lb'])
params.loc[(mask & mask_neg), "lb"] = params.loc[(mask & mask_neg), "ini"]*10
params.loc[(mask & mask_pos), "lb"] = params.loc[(mask & mask_pos), "ini"]*0.1
# set upper bounds
mask = (params['ini'] > params['ub'])
params.loc[(mask & mask_neg), "ub"] = params.loc[(mask & mask_neg), "ini"]*0.1
params.loc[(mask & mask_pos), "ub"] = params.loc[(mask & mask_pos), "ini"]*10

return params

def fit_from_ref(self, rowno, region, ref, update_pars_from_previous=True, update_cnstr_wd=None, method="L-BFGS-B"):
Expand Down Expand Up @@ -425,6 +444,7 @@ def fit_from_ref(self, rowno, region, ref, update_pars_from_previous=True, updat

# get params from previous spectrum
prev_params = self.results[ref][region].params.copy(deep=True)
#print(f"Previous params:\n{prev_params}")

# update initial values
if update_pars_from_previous:
Expand All @@ -434,6 +454,8 @@ def fit_from_ref(self, rowno, region, ref, update_pars_from_previous=True, updat
if update_cnstr_wd is not None:
prev_params = self.update_cnstr_wd(prev_params, update_cnstr_wd)

#print(f"Updated params:\n{prev_params}")

# update params in spectrum
self.update_params(prev_params, spectrum=rowno, region=region)

Expand Down
12 changes: 9 additions & 3 deletions multinmrfit/base/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ def update_offset(self, offset: dict) -> None:
self.params.drop(self.params[
(self.params["signal_id"] == 'full_spectrum') & (self.params["par"] == 'offset')
].index, inplace=True)
self.cnstr_wd.drop(self.cnstr_wd[
(self.cnstr_wd["signal_id"] == 'full_spectrum') & (self.cnstr_wd["par"] == 'offset')
].index, inplace=True)
self.offset = False
else:
if isinstance(offset, dict):
Expand All @@ -380,6 +383,9 @@ def update_offset(self, offset: dict) -> None:
offset.get('lb', -default_offset_bound),
offset.get('ub', default_offset_bound)
]
_cnstr_offset = pd.DataFrame({'signal_id': 'full_spectrum', 'model': None, 'par': 'offset', 'shift_allowed':default_offset_bound, 'relative': False}, index=[0])
self.cnstr_wd = pd.concat([self.cnstr_wd, _cnstr_offset])
self.cnstr_wd.reset_index(inplace=True, drop=True)
else:
raise TypeError("offset must be a dict or None")

Expand Down Expand Up @@ -448,7 +454,7 @@ def fit(self, method: str = "L-BFGS-B"):
self._check_parameters()

# set scaling factor to stabilize convergence
mean_sp = np.mean(self.intensity)
mean_sp = np.max(self.intensity)
scaling_factor = 1 if -1 < mean_sp < 1 else abs(mean_sp)

# apply scaling factor on parameters (intensity & offset)
Expand Down Expand Up @@ -482,7 +488,7 @@ def fit(self, method: str = "L-BFGS-B"):
args=(self._simulate, self.models, self.ppm, data_scaled, self.offset),
method="L-BFGS-B",
bounds=bounds,
options={'maxcor': 30, 'maxls': 30}
options={'maxcor': 40, 'maxls': 40, 'disp': 1}
)


Expand All @@ -494,7 +500,7 @@ def fit(self, method: str = "L-BFGS-B"):
args=(self._simulate, self.models, self.ppm, data_scaled, self.offset),
method="L-BFGS-B",
bounds=bounds,
options={'maxcor': 30, 'maxls': 30}
options={'maxcor': 40, 'maxls': 40, 'disp': 1}
)

else:
Expand Down
3 changes: 2 additions & 1 deletion multinmrfit/models/model_doublet.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
'relative': [False, True, True, True, False]}

def pplist2signal(self, peak_list):

detected_peak_position = np.mean(peak_list.ppm.values)
detected_peak_intensity = peak_list.intensity.values[0]
detected_coupling_constant = np.abs(max(peak_list.ppm.values)-min(peak_list.ppm.values))
Expand All @@ -35,7 +36,7 @@ def pplist2signal(self, peak_list):
'J': {'ini': detected_coupling_constant, 'lb': 0.8*detected_coupling_constant, 'ub': 1.2*detected_coupling_constant},
}
}
# add lw

return signal

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions multinmrfit/models/model_doublet_of_doublet.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
'relative': [False, True, True, True, True, False]}

def pplist2signal(self, peak_list):

detected_peak_position = np.mean(peak_list.ppm.values)
detected_peak_intensity = np.mean(peak_list.intensity.values)

Expand Down
3 changes: 2 additions & 1 deletion multinmrfit/models/model_quartet.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
'relative': [False, True, True, True, False]}

def pplist2signal(self, peak_list):

detected_peak_position = np.mean(peak_list.ppm.values)
detected_peak_intensity = np.mean(peak_list.intensity.values)
detected_coupling_constant = np.abs(max(peak_list.ppm.values)-min(peak_list.ppm.values))/3
Expand All @@ -35,7 +36,7 @@ def pplist2signal(self, peak_list):
'J': {'ini': detected_coupling_constant, 'lb': 0.8*detected_coupling_constant, 'ub': 1.2*detected_coupling_constant},
}
}
# add lw

return signal

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions multinmrfit/models/model_singlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
'relative': [False, True, True, False]}

def pplist2signal(self, peak_list):

detected_peak_position = peak_list.ppm.values[0]
detected_peak_intensity = peak_list.intensity.values[0]

Expand Down
3 changes: 2 additions & 1 deletion multinmrfit/models/model_triplet.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self):
'relative': [False, True, True, True, False]}

def pplist2signal(self, peak_list):

detected_peak_position = np.mean(peak_list.ppm.values)
detected_peak_intensity = np.mean(peak_list.intensity.values)
detected_coupling_constant = np.abs(max(peak_list.ppm.values)-min(peak_list.ppm.values))/2
Expand All @@ -35,7 +36,7 @@ def pplist2signal(self, peak_list):
'J': {'ini': detected_coupling_constant, 'lb': 0.8*detected_coupling_constant, 'ub': 1.2*detected_coupling_constant},
}
}
# add lw

return signal

@staticmethod
Expand Down
14 changes: 8 additions & 6 deletions multinmrfit/ui/01_Inputs_&_Outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
page="inputs_outputs"
)

session.object_space["consolidate"] = True

def save_defaults(options):
save_path = pathlib.Path(multinmrfit.__file__).resolve().parent / "ui" / "conf"
Expand Down Expand Up @@ -62,9 +63,9 @@ def load_defaults():
if lastversion != multinmrfit.__version__:
# change the next line to streamlit
update_info = st.info(
f'New version available ({lastversion}). '
f'You can update multiNMRFit with: "pip install --upgrade '
f'multinmrfit". Check the documentation for more '
f'New version available (v{lastversion}). '
f'You can update multiNMRFit with the following command: \n\npip install --upgrade '
f'multinmrfit \n\nCheck the documentation for more '
f'information.'
)
except Exception:
Expand All @@ -76,9 +77,10 @@ def load_defaults():
if uploaded_file is not None:

# load process object
with uploaded_file as file:
# process = pd.read_pickle(file)
process = pickle.load(file)
with st.spinner('Loading process file...'):
with uploaded_file as file:
# process = pd.read_pickle(file)
process = pickle.load(file)

# save in session state
session.object_space["process"] = process
Expand Down
22 changes: 15 additions & 7 deletions multinmrfit/ui/pages/02_Process_spectra.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import math

st.set_page_config(page_title="Process ref spectrum", layout="wide")
st.title("Process reference spectrum")
st.title("Process spectra")


session = SessI(
Expand All @@ -15,12 +15,14 @@

# get process
process = session.object_space["process"]

session.object_space["consolidate"] = True

def append_line(chem_shift, process, name):
new_line = pd.DataFrame({"ppm": [chem_shift], "intensity": [process.get_current_intensity(chem_shift)], "cID": [name]})
process.current_spectrum.edited_peak_table = pd.concat([process.current_spectrum.edited_peak_table, new_line])

def update_params(process, parameters):
process.update_params(parameters)

if process is None:

Expand Down Expand Up @@ -236,7 +238,7 @@ def append_line(chem_shift, process, name):

if len(process.current_spectrum.params):

with st.form("Fit spectrum"):
with st.container(border=True):

st.write("### Fitting")

Expand All @@ -253,15 +255,14 @@ def append_line(chem_shift, process, name):
disabled=["signal_id", "model", "par", "opt", "opt_sd", "integral"]
)

upd_pars = st.button("Update parameters", on_click=update_params, args=(process, parameters))

use_DE = st.checkbox('Refine initial values using Differential evolution', value=session.widget_space["use_DE"], key="use_DE")

fit_ok = st.form_submit_button("Fit spectrum")
fit_ok = st.button("Fit spectrum")

if fit_ok:

# update parameters
process.update_params(parameters)

# fit reference spectrum
with st.spinner('Fitting in progress, please wait...'):
method = "differential_evolution" if use_DE else "L-BFGS-B"
Expand All @@ -279,6 +280,13 @@ def append_line(chem_shift, process, name):
# save as pickle file
process.save_process_to_file()
st.success("Spectrum has been fitted.")
else:
# plot fit results
fig = process.current_spectrum.plot(ini=True, fit=False)
fig.update_layout(autosize=False, width=800, height=600)
fig.update_layout(legend=dict(yanchor="top", xanchor="right", y=1.15))
st.plotly_chart(fig, theme=None)


if process.current_spectrum.fit_results is not None:

Expand Down
6 changes: 5 additions & 1 deletion multinmrfit/ui/pages/03_Process_from_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
session = SessI(session_state=st.session_state, page="Fitting")

process = session.get_object(key="process")
session.object_space["consolidate"] = True

def update_checkbox(widget):
session.register_widgets({widget: not session.widget_space[widget]})

spectra_list = []

Expand Down Expand Up @@ -69,7 +73,7 @@
session.register_widgets({"adapt_cnstr_wd": True})

if use_previous:
adapt_cnstr_wd = st.checkbox('Adjust initial bounds dynamically', value=session.widget_space["adapt_cnstr_wd"], key="adapt_cnstr_wd")
adapt_cnstr_wd = st.checkbox('Adjust bounds dynamically', value=session.widget_space["adapt_cnstr_wd"], key="adapt_cnstr_wd", on_change=update_checkbox, args=["adapt_cnstr_wd"])
if adapt_cnstr_wd:
with st.container(border=True):
df_cnstr_wd = st.data_editor(process.results[reference_spectrum][region].cnstr_wd,
Expand Down
Loading

0 comments on commit d35aa2c

Please sign in to comment.