|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +from abc import ABC |
| 4 | + |
| 5 | +import numpy as np |
| 6 | +import pandas as pd |
| 7 | + |
| 8 | +from fairmd.lipids.api import get_OP |
| 9 | +from fairmd.lipids.core import System, initialize_databank |
| 10 | +from fairmd.lipids.experiment import ExperimentCollection |
| 11 | + |
| 12 | + |
| 13 | +class OPQDataError(Exception): |
| 14 | + """Our specific exception""" |
| 15 | + |
| 16 | + |
| 17 | +class OPQDataStorer(ABC): |
| 18 | + def __init__(self, s: System, lname: str) -> None: |
| 19 | + self._s = s |
| 20 | + self._lname = lname |
| 21 | + |
| 22 | + def _cvt_op_df(self, opdict: dict, err_pos: int) -> pd.DataFrame: |
| 23 | + op_clean = pd.DataFrame( |
| 24 | + columns=["c", "h", "val", "err"], |
| 25 | + ) |
| 26 | + smi2uname = self._get_smi2uname() |
| 27 | + for smid, uname in smi2uname.items(): |
| 28 | + _c_dict = { |
| 29 | + k: [v[0], 0.02 if err_pos >= len(v) else v[err_pos]] # check |
| 30 | + for k, v in opdict.items() |
| 31 | + if k.split()[0] == uname |
| 32 | + } |
| 33 | + _c_dict_len = len(_c_dict) |
| 34 | + if _c_dict_len == 0: |
| 35 | + continue |
| 36 | + vearr = np.array([_c_dict.popitem()[1] for _ in range(_c_dict_len)]) |
| 37 | + if _c_dict_len == 1: # one H |
| 38 | + op_clean.loc[len(op_clean)] = [smid, 1, vearr[0, 0], vearr[0, 1]] |
| 39 | + elif _c_dict_len == 3: # three H. They are always symmetric. |
| 40 | + op_clean.loc[len(op_clean)] = [smid, 1, np.mean(vearr[:, 0]), np.mean(vearr[:, 1])] |
| 41 | + elif _c_dict_len == 2: # four H. They could be asymmetric. |
| 42 | + vearr = vearr[np.argsort(np.abs(vearr[:, 0]))] |
| 43 | + op_clean.loc[len(op_clean)] = [smid, 1, vearr[0, 0], np.mean(vearr[0, 1])] |
| 44 | + op_clean.loc[len(op_clean)] = [smid, 2, vearr[1, 0], np.mean(vearr[1, 1])] |
| 45 | + else: |
| 46 | + msg = f"Unexpected number of H for {uname} in instance {self.ass_id} // {self._lname}: {_c_dict_len}. Cannot store." |
| 47 | + raise OPQDataError(msg) |
| 48 | + return op_clean.astype({"c": np.int64, "h": np.int64, "val": np.float64, "err": np.float64}) |
| 49 | + |
| 50 | + def prepare_sim_dataframe(self) -> None: |
| 51 | + """Prepare dataframe for storing.""" |
| 52 | + opdict = get_OP(self._s)[self._lname] |
| 53 | + self._sim_op = self._cvt_op_df(opdict, 2) |
| 54 | + |
| 55 | + @property |
| 56 | + def ass_id(self) -> str: |
| 57 | + """Get id of assoc object""" |
| 58 | + return self._s["ID"] |
| 59 | + |
| 60 | + def _get_smi2uname(self) -> dict: |
| 61 | + mol = self._s.lipids[self._lname] |
| 62 | + smiles = mol.metadata["bioschema_properties"]["smiles"] |
| 63 | + print(smiles) |
| 64 | + s2u = {} |
| 65 | + for uname, aprops in mol.mapping_dict.items(): |
| 66 | + if "SMILEIDX" in aprops: |
| 67 | + smid = int(aprops["SMILEIDX"]) |
| 68 | + s2u[smid] = uname |
| 69 | + if not s2u: |
| 70 | + # NO SMILEIDX. Cannot store. |
| 71 | + msg = f"Instance {self.ass_id} // {self._lname} cannot be stored: we don't have SMILEIDX." |
| 72 | + raise OPQDataError(msg) |
| 73 | + return dict(sorted(s2u.items())) |
| 74 | + |
| 75 | + def add_experiment_data(self, exp_opdict: dict) -> None: |
| 76 | + """Add experimental OP data to the storer. We will use it for Q estimation.""" |
| 77 | + if not hasattr(self, "_exp_opdict"): |
| 78 | + self._exp_opdicts = [] |
| 79 | + self._exp_opdicts.append(self._cvt_op_df(exp_opdict, 1)) |
| 80 | + |
| 81 | + def average_experiment_data(self) -> None: |
| 82 | + """Average experimental OP data if we have more than one.""" |
| 83 | + concdf = pd.concat(self._exp_opdicts, ignore_index=True) |
| 84 | + self._exp_opdict = concdf.groupby(["c", "h"]).mean().reset_index() |
| 85 | + |
| 86 | + def compute_q_points(self) -> None: |
| 87 | + """Compute shift btw simulation and mean(exp) datapoints""" |
| 88 | + df1 = self._sim_op.merge( |
| 89 | + self._exp_opdict, |
| 90 | + on=["c", "h"], |
| 91 | + how="inner", |
| 92 | + suffixes=("_s", "_e"), |
| 93 | + ) |
| 94 | + df1["val_e"] = df1["val_e"].abs() * np.sign(df1["val_s"]) |
| 95 | + self._qpoints = df1 |
| 96 | + |
| 97 | + def store_to_hdf5(self, hdf_fname: str) -> None: |
| 98 | + """Store the record""" |
| 99 | + _mcontent = self._s["COMPOSITION"] |
| 100 | + mcontent = {"name": [], "inchikey": [], "number": [], "asymmetry": []} |
| 101 | + for lname, lip in self._s.lipids.items(): |
| 102 | + ik = lip.metadata["bioschema_properties"]["inChIKey"] |
| 103 | + cnt = _mcontent[lname]["COUNT"] |
| 104 | + if isinstance(cnt, int): |
| 105 | + asm = np.nan |
| 106 | + cnt = [cnt / 2, cnt / 2] |
| 107 | + else: |
| 108 | + asm = cnt[0] / sum(cnt) |
| 109 | + mcontent["name"] += [lname] |
| 110 | + mcontent["inchikey"] += [ik] |
| 111 | + mcontent["number"] += [sum(cnt) / 2] |
| 112 | + mcontent["asymmetry"] += [asm] |
| 113 | + hydration = self._s.get_hydration() |
| 114 | + scontent = self._s.solution_composition(basis="molar") |
| 115 | + temperature = self._s["TEMPERATURE"] |
| 116 | + inchikey = self._s.lipids[self._lname].metadata["bioschema_properties"]["inChIKey"] |
| 117 | + smiles = self._s.lipids[self._lname].metadata["bioschema_properties"]["smiles"] |
| 118 | + ff_name = self._s.readme.get("FF", False) |
| 119 | + # store all vars and df to the HDF5 table |
| 120 | + group = f"SIM_{self._s['ID']}__{self._lname}" |
| 121 | + with pd.HDFStore(hdf_fname, "a") as store: |
| 122 | + # DataFrame table |
| 123 | + store.put(f"{group}/op_values", self._qpoints, format="table", data_columns=True) |
| 124 | + store.put(f"{group}/simulation_table", pd.DataFrame(mcontent), format="table", data_columns=True) |
| 125 | + # Metadata attributes - I |
| 126 | + op_storer = store.get_storer(f"{group}/op_values") |
| 127 | + upd_attr = { |
| 128 | + "inchikey": inchikey, |
| 129 | + "smiles": smiles, |
| 130 | + "fmdl_simid": self._s["ID"], |
| 131 | + } |
| 132 | + for k, v in upd_attr.items(): |
| 133 | + op_storer.attrs[k] = v |
| 134 | + # -//- II |
| 135 | + sample_storer = store.get_storer(f"{group}/simulation_table") |
| 136 | + upd_attr = { |
| 137 | + "temperature": temperature, |
| 138 | + "hydration": hydration, |
| 139 | + "solution": ", ".join([f"{k:<25} {v * 100:>6.1f}%" for k, v in sorted(scontent.items())]), |
| 140 | + } |
| 141 | + if ff_name: |
| 142 | + upd_attr["ff_name"] = ff_name |
| 143 | + for k, v in upd_attr.items(): |
| 144 | + sample_storer.attrs[k] = v |
| 145 | + |
| 146 | + |
| 147 | +H5_QSIMS_MASTER = "atomic-opq-dataset.h5" |
| 148 | + |
| 149 | +def load_sims() -> None: |
| 150 | + print("Generating OP dataset from simulations.") |
| 151 | + exps = ExperimentCollection.load_from_data("OPExperiment") |
| 152 | + sims = initialize_databank() |
| 153 | + for sim in sims: |
| 154 | + paired_opedict = sim["EXPERIMENT"].get("ORDERPARAMETER", {}) |
| 155 | + for lname in sim.lipids: |
| 156 | + if len(paired_opedict.get(lname, [])) == 0: |
| 157 | + continue |
| 158 | + print(sim) |
| 159 | + |
| 160 | + ods = OPQDataStorer(sim, lname) |
| 161 | + try: |
| 162 | + ods.prepare_sim_dataframe() |
| 163 | + except OPQDataError as e: |
| 164 | + print("ERROR: ", e) |
| 165 | + continue |
| 166 | + |
| 167 | + for expid in paired_opedict[lname]: |
| 168 | + _exp = exps.loc(expid) |
| 169 | + ods.add_experiment_data(_exp.data[lname]) |
| 170 | + ods.average_experiment_data() |
| 171 | + |
| 172 | + ods.compute_q_points() |
| 173 | + ods.store_to_hdf5(H5_QSIMS_MASTER) |
| 174 | + |
| 175 | + |
| 176 | +if __name__ == "__main__": |
| 177 | + load_sims() |
0 commit comments