diff --git a/dev/gui/dev_gui_secB.py b/dev/gui/dev_gui_secB.py index 4c8ce354..9495fc15 100644 --- a/dev/gui/dev_gui_secB.py +++ b/dev/gui/dev_gui_secB.py @@ -21,8 +21,6 @@ from pyhdx.web.utils import load_state, fix_multiindex_dtypes from pyhdx.config import cfg, reset_config -reset_config() - #def printfunc(args): # 1/0 @@ -132,24 +130,21 @@ def init_dashboard(): input_control._action_load_datasets() d_uptake_control = ctrl.control_panels["DUptakeFitControl"] - d_uptake_control.repeats = 3 - + d_uptake_control.repeats = 2 ctrl.sources['pdb'].add_from_string(pdb_string, '1qyn') src = ctrl.sources['main'] - df = csv_to_dataframe(web_data_dir / 'd_uptake.csv') - df.columns = fix_multiindex_dtypes(df.columns) - src.add_table('d_uptake', df) - src.updated = True - - # todo needs to be done on _add_table / add_table - df = csv_to_dataframe(web_data_dir / 'dG.csv') - df.columns = fix_multiindex_dtypes(df.columns) - src.add_table('dG', df) - - #src.param.trigger('updated') - src.updated = True + # df = csv_to_dataframe(web_data_dir / 'd_uptake.csv') + # df.columns = fix_multiindex_dtypes(df.columns) + # src.add_table('d_uptake', df) + # src.updated = True + # + # df = csv_to_dataframe(web_data_dir / 'dG.csv') + # df.columns = fix_multiindex_dtypes(df.columns) + # src.add_table('dG', df) + + # src.updated = True # guess_control = ctrl.control_panels['InitialGuessControl'] diff --git a/pyhdx/web/apps/pyhdx_app.yaml b/pyhdx/web/apps/pyhdx_app.yaml index 9ee0545e..e90c73ca 100644 --- a/pyhdx/web/apps/pyhdx_app.yaml +++ b/pyhdx/web/apps/pyhdx_app.yaml @@ -10,6 +10,8 @@ sources: type: pyhdx pdb: type: pdb + metadata: + type: dict transforms: peptide_src: diff --git a/pyhdx/web/apps/rfu_app.yaml b/pyhdx/web/apps/rfu_app.yaml index d0815fb5..7608c8c5 100644 --- a/pyhdx/web/apps/rfu_app.yaml +++ b/pyhdx/web/apps/rfu_app.yaml @@ -10,6 +10,8 @@ sources: type: pyhdx pdb: type: pdb + metadata: + type: dict transforms: peptide_src: diff --git a/pyhdx/web/controllers.py b/pyhdx/web/controllers.py index b8314d46..cbf10050 100644 --- a/pyhdx/web/controllers.py +++ b/pyhdx/web/controllers.py @@ -4,7 +4,6 @@ import sys import uuid import zipfile -from datetime import datetime from io import StringIO, BytesIO from typing import Any @@ -195,8 +194,9 @@ def _action_debug(self): print(df_rfu) def _action_test(self): - pdbe_view = self.views["protein"] - pdbe_view.pdbe.test = not pdbe_view.pdbe.test + src = self.sources['metadata'] + d = src.get('user_settings') + print(d) @property def _layout(self): @@ -265,7 +265,7 @@ def make_dict(self): def config_download_callback(self) -> StringIO: # Generate and set filename - timestamp = datetime.now().strftime("%Y%m%d%H%M") + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") self.widgets[ "config_download" ].filename = f"PyHDX_config_{timestamp}.yaml" @@ -400,15 +400,12 @@ def _action_load_datasets(self) -> None: ) def spec_download_callback(self) -> StringIO: - timestamp = datetime.now().strftime("%Y%m%d%H%M") + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") self.widgets[ "download_spec_button" ].filename = f"PyHDX_state_spec_{timestamp}.yaml" - s = yaml.dump(clean_types(self.state_spec), sort_keys=False) - output = "# " + pyhdx.VERSION_STRING + "\n" + s - sio = StringIO(output) - + sio = self.parent.state_spec_callback() return sio @property @@ -1119,8 +1116,19 @@ def _action_fit(self): self.param["do_fit"].constant = True self.widgets["do_fit"].loading = True + user_dict = self.sources['metadata'].get('user_settings') + user_dict['d_uptake_fit'][self.fit_name] = self.get_user_settings() async_execute(self._fit_d_uptake) + def get_user_settings(self) -> dict: + """ + Returns a dictionary with the current user settings. + """ + keys = ['bounds', 'r1'] + d = {k: getattr(self, k) for k in keys} + + return d + async def _fit_d_uptake(self): name = self.fit_name @@ -1276,7 +1284,9 @@ def _action_fit(self): self.param["do_fit1"].constant = True self.widgets["do_fit1"].loading = True - num_samples = len(self.src.hdxm_objects) + user_dict = self.sources['metadata'].get('user_settings') + user_dict['initial_guess'][self.guess_name] = self.get_user_settings() + if self.fitting_model.lower() in ["association", "dissociation"]: loop = asyncio.get_running_loop() loop.create_task(self._fit_rates(self.guess_name)) @@ -1322,6 +1332,21 @@ async def _fit_rates(self, name): self.widgets["do_fit1"].loading = False self.parent.logger.info(f"Finished initial guess fit {name}") + def get_user_settings(self) -> dict: + """ + Returns a dictionary with the current user settings. + """ + + d = {'fitting_model': self.fitting_model} + if self.fitting_model in ["association", "dissociation"]: + d['global_bounds'] = self.global_bounds + if self.global_bounds: + d["bounds"] = [self.lower_bound, self.upper_bound] + else: + d["bounds"] = self.bounds + + return d + class FitControl(PyHDXControlPanel): """ @@ -1481,6 +1506,9 @@ def _action_fit(self): self._fit_names.append(self.fit_name) self.parent.logger.info("Started PyTorch fit") + user_dict = self.sources['metadata'].get('user_settings') + user_dict['dG_fit'][self.fit_name] = self.get_user_settings() + self._current_jobs += 1 # if self._current_jobs >= self._max_jobs: # self.widgets['do_fit'].constant = True @@ -1600,6 +1628,24 @@ def fit_kwargs(self): return fit_kwargs + def get_user_settings(self) -> dict: + """ + Returns a dictionary with the current user settings. + """ + + d = { + 'initial_guess': self.initial_guess, + 'guess_mode': self.guess_mode + } + + if self.guess_mode == 'One-to-many': + d['guess_state'] = self.guess_state + d['fit_mode'] = self.fit_mode + + d.update(self.fit_kwargs) + + return d + class DifferentialControl(PyHDXControlPanel): _type = "diff" @@ -1660,6 +1706,9 @@ def _action_add_comparison(self): ) return + user_dict = self.sources['metadata'].get('user_settings') + user_dict['differential_HDX'][self.comparison_name] = self.get_user_settings() + # RFU only app has no dGs, if "ddG_fit_select" in self.transforms: self.add_ddG_comparison() @@ -1790,6 +1839,14 @@ def add_dd_uptake_comparison(self): self.src._add_table(dd_uptake, "dd_uptake") + def get_user_settings(self) -> dict: + """ + Returns a dictionary with the current user settings. + """ + + d = {'reference_state': self.reference_state} + + return d class ColorTransformControl(PyHDXControlPanel): """ @@ -2430,12 +2487,39 @@ def make_dict(self): callback=self.color_export_callback, ) + widgets["divider"] = pn.layout.Divider() + + widgets["download_state_spec"] = pn.widgets.FileDownload( + label="Download HDX spec", + callback=self.state_spec_callback, + ) + + widgets["download_config"] = pn.widgets.FileDownload( + label="Download config", + callback=self.config_callback, + ) + + widgets["download_user_settings"] = pn.widgets.FileDownload( + label="Download user settings", + callback=self.user_settings_callback, + ) + + widgets["download_log"] = pn.widgets.FileDownload( + label="Download log", + callback = self.log_callback, + ) + widget_order = [ "table", "export_format", "export_tables", "export_pml", "export_colors", + "divider", + "download_state_spec", + "download_config", + "download_user_settings", + "download_log", ] final_widgets = {w: widgets[w] for w in widget_order} @@ -2521,6 +2605,42 @@ def color_export_callback(self): else: return None + def state_spec_callback(self) -> StringIO: + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") + self.widgets[ + "download_state_spec" + ].filename = f"PyHDX_state_spec_{timestamp}.yaml" + + sio = self.parent.state_spec_callback() + return sio + + def config_callback(self) -> StringIO: + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") + self.widgets[ + "download_config" + ].filename = f"PyHDX_config_{timestamp}.yaml" + + sio = self.parent.config_callback() + return sio + + def user_settings_callback(self) -> StringIO: + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") + self.widgets[ + "download_user_settings" + ].filename = f"PyHDX_config_{timestamp}.yaml" + + sio = self.parent.user_settings_callback() + return sio + + def log_callback(self) -> StringIO: + timestamp = self.parent.session_time.strftime("%Y%m%d%H%M") + self.widgets[ + "download_log" + ].filename = f"PyHDX_log_{timestamp}.txt" + + sio = self.parent.log_callback() + return sio + class FigureExportControl(PyHDXControlPanel): @@ -2805,8 +2925,7 @@ def make_dict(self): return widgets def export_session_callback(self): - dt = datetime.today().strftime("%Y%m%d_%H%M") - self.widgets["export_session"].filename = f"{dt}_PyHDX_session.zip" + self.widgets["export_session"].filename = f"{self.parent.session_time.strftime('%Y%m%d_%H%M')}_PyHDX_session.zip" bio = BytesIO() with zipfile.ZipFile(bio, "w") as session_zip: # Write tables @@ -2814,21 +2933,22 @@ def export_session_callback(self): sio = dataframe_to_stringio(table) session_zip.writestr(name + ".csv", sio.getvalue()) - # Write config file - masked_conf = OmegaConf.masked_copy(cfg.conf, cfg.conf.keys() - {'server'}) - s = OmegaConf.to_yaml(masked_conf) - - version_string = "# pyhdx configuration file " + __version__ + "\n\n" - session_zip.writestr("PyHDX_config.yaml", version_string + s) - - # Write state spec file - input_controllers = {"PeptideFileInputControl", "PeptideRFUFileInputControl"} - input_ctrls = self.parent.control_panels.keys() & input_controllers - if len(input_ctrls) == 1: - input_ctrl = self.parent.control_panels[list(input_ctrls)[0]] - sio = input_ctrl.spec_download_callback() + # Write HDX measurement state specifications + if sio := self.parent.state_spec_callback(): session_zip.writestr("PyHDX_state_spec.yaml", sio.read()) + # Write config file + sio = self.parent.config_callback() + session_zip.writestr("PyHDX_config.yaml", sio.read()) + + # Write user settings + sio = self.parent.user_settings_callback() + session_zip.writestr("PyHDX_user_settings.yaml", sio.read()) + + # Write log file + sio = self.parent.log_callback() + session_zip.writestr("PyHDX_log.txt", sio.read()) + bio.seek(0) return bio diff --git a/pyhdx/web/main_controllers.py b/pyhdx/web/main_controllers.py index 5d8ebc18..1a7c434a 100644 --- a/pyhdx/web/main_controllers.py +++ b/pyhdx/web/main_controllers.py @@ -1,7 +1,15 @@ import logging import warnings +from io import StringIO +from datetime import datetime +from typing import Optional import param +import yaml +from omegaconf import OmegaConf +from pyhdx.config import cfg +from pyhdx.support import clean_types +import pyhdx class MainController(param.Parameterized): @@ -51,6 +59,7 @@ def __init__(self, control_panels, **params): self.template = None # Panel template (remove?) + self.session_time = datetime.now() self.update() # todo check to see if this is really needed # from lumen.target.Target @@ -87,10 +96,90 @@ class PyHDXController(MainController): def __init__(self, *args, **kwargs): super(PyHDXController, self).__init__(*args, **kwargs) + self.log_io = StringIO() + sh = logging.StreamHandler(self.log_io) + sh.terminator = " \n" + # sh.setLevel(logging.CRITICAL) + formatter = logging.Formatter( + "%(asctime)s [%(levelname)s]: %(message)s", "%Y-%m-%d %H:%M:%S" + ) + sh.setFormatter(formatter) + self.logger.addHandler(sh) + @property def logger(self): return self.loggers["pyhdx"] + def _get_file_header(self) -> str: + """ + Returns header for txt file outputs with version and date/time information + + """ + + s = f"# {pyhdx.VERSION_STRING} \n" + s += f"# {self.session_time.strftime('%Y/%m/%d %H:%M:%S')}" \ + f"({int(self.session_time.timestamp())})\n" + + return s + + def state_spec_callback(self) -> Optional[StringIO]: + """ + Get a StringIO with input HDX measurement specifications. + + """ + input_controllers = {"PeptideFileInputControl", "PeptideRFUFileInputControl"} + input_ctrls = self.control_panels.keys() & input_controllers + if len(input_ctrls) == 1: + input_ctrl = self.control_panels[list(input_ctrls)[0]] + + s = yaml.dump(clean_types(input_ctrl.state_spec), sort_keys=False) + output = self._get_file_header() + "\n" + s + sio = StringIO(output) + + return sio + else: + return None + + def config_callback(self) -> StringIO: + """ + Get a StringIO with global configuration settings. + + """ + + masked_conf = OmegaConf.masked_copy(cfg.conf, cfg.conf.keys() - {'server'}) + s = OmegaConf.to_yaml(masked_conf) + + output = self._get_file_header() + "\n" + s + sio = StringIO(output) + + return sio + + def user_settings_callback(self) -> StringIO: + """ + Get a StringIO with user settings. + + """ + user_dict = self.sources['metadata'].get('user_settings') + s = yaml.dump(clean_types(user_dict), sort_keys=False) + + output = self._get_file_header() + "\n" + s + sio = StringIO(output) + + return sio + + def log_callback(self) -> StringIO: + """ + Get a StringIO with the full log. + + """ + + self.log_io.seek(0) + s = self.log_io.read() + + output = self._get_file_header() + "\n" + s + sio = StringIO(output) + + return sio # single amide slider only first? class PeptideController(MainController): diff --git a/pyhdx/web/sources.py b/pyhdx/web/sources.py index a7f3fd01..5fb7dccf 100644 --- a/pyhdx/web/sources.py +++ b/pyhdx/web/sources.py @@ -1,5 +1,9 @@ +import json import os import urllib.request +import uuid +from collections import defaultdict +from typing import Optional, Any import numpy as np import pandas as pd @@ -323,3 +327,46 @@ def get(self): def get_pdb(self, pdb_id): return self.pdb_files[pdb_id] + + +class DictSource(Source): + """Source for (metadata) dictionaries""" + + + _type = 'dict' + + _items = param.Dict({}) + + hashes = param.Dict({}) + + def set(self, item: dict, name: Optional[str] = None): + if not isinstance(item, dict): + raise TypeError(f"Invalid type of 'item', must be {dict!r}, got {type(item)!r}") + # self.make_room() + name = name or f"_item_{uuid.uuid4()}" # self.new_key() + self._items[name] = item + self.hashes[name] = self.hash_item(item) + + # def set_value(self, name: str, key: Any, value: Any,): + # d = self._items[name] + # + # + # d[key] = value + # self.hashes[name] = self.hash_item(self._items[name]) + + def hash_item(self, item: dict) -> int: + return hash(json.dumps(item)) + + def update(self) -> None: + self.hashes = {key: self.hash_item(item) for key, item in self._items} + self.updated = True + + #todo does not match base object + def get(self, name: str) -> Optional[dict]: + if name not in self._items: + item = defaultdict(dict) + self._items[name] = item + return item + + name = name or next(iter(self.keys())) + return self._items[name] \ No newline at end of file