|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import argparse |
| 4 | +import logging |
| 5 | +import re |
| 6 | +import sys |
| 7 | +from abc import ABC, abstractmethod |
| 8 | + |
| 9 | +import numpy as np |
| 10 | +import pandas as pd |
| 11 | +from unidecode import unidecode |
| 12 | + |
| 13 | +from fairmd.lipids._base import SampleComposition |
| 14 | +from fairmd.lipids.api import get_OP |
| 15 | +from fairmd.lipids.core import System, initialize_databank |
| 16 | +from fairmd.lipids.experiment import ExperimentCollection, OPExperiment |
| 17 | +from fairmd.lipids.molecules import Molecule |
| 18 | + |
| 19 | + |
| 20 | +class OPDataError(Exception): |
| 21 | + """Our specific exception""" |
| 22 | + |
| 23 | + |
| 24 | +class OPDataStorer(ABC): |
| 25 | + """Abstract class for OP data storage""" |
| 26 | + |
| 27 | + @abstractmethod |
| 28 | + def prepare_dataframe(self) -> None: |
| 29 | + """Prepare dataframe for storing""" |
| 30 | + |
| 31 | + @property |
| 32 | + @abstractmethod |
| 33 | + def ass_id(self) -> str: |
| 34 | + """Get id of assoc object""" |
| 35 | + |
| 36 | + @property |
| 37 | + @abstractmethod |
| 38 | + def sample(self) -> SampleComposition: |
| 39 | + """Return sample instance""" |
| 40 | + |
| 41 | + def _prepare_df_common(self, mol: Molecule, opdict: dict, err_extractor: callable) -> None: |
| 42 | + """Condition the dataframe for storage""" |
| 43 | + smi2uname = {} |
| 44 | + for uname, aprops in mol.mapping_dict.items(): |
| 45 | + if "SMILEIDX" in aprops: |
| 46 | + smid = int(aprops["SMILEIDX"]) |
| 47 | + smi2uname[smid] = uname |
| 48 | + if not smi2uname: |
| 49 | + # NO SMILEIDX. Cannot store. |
| 50 | + msg = f"Instance {self.ass_id} // {self._lname} cannot be stored: we don't have SMILEIDX." |
| 51 | + raise OPDataError(msg) |
| 52 | + smi2uname = dict(sorted(smi2uname.items())) |
| 53 | + df = pd.DataFrame(columns=["id", "val", "err"]) |
| 54 | + cur_row = 0 |
| 55 | + for id_, uname in smi2uname.items(): |
| 56 | + for opdict_row in OPDataStorer.by_c_uname(opdict, uname): |
| 57 | + df.loc[cur_row] = [id_, opdict_row[0], err_extractor(opdict_row)] |
| 58 | + cur_row += 1 |
| 59 | + self._df = df |
| 60 | + |
| 61 | + @staticmethod |
| 62 | + def by_c_uname(opdict: dict, uname: str) -> dict: |
| 63 | + """Return only OP vals of named heavy atoms. Sorted by val.""" |
| 64 | + res = [] |
| 65 | + for k, v in opdict.items(): |
| 66 | + if k.split()[0] == uname: |
| 67 | + res.append(v) |
| 68 | + return sorted(res, key=lambda x: x[0]) |
| 69 | + |
| 70 | + def get_mcontent(self) -> dict: |
| 71 | + """Generate membrane content dictionary""" |
| 72 | + _mcontent = self.sample.membrane_composition(basis="molar") |
| 73 | + rdict = {"name": [], "inchikey": [], "fraction": []} |
| 74 | + for lname, frac in _mcontent.items(): |
| 75 | + k = lname |
| 76 | + ik = self.sample.lipids[lname].metadata["bioschema_properties"]["inChIKey"] |
| 77 | + rdict["name"] += [k] |
| 78 | + rdict["inchikey"] += [ik] |
| 79 | + rdict["fraction"] += [frac] |
| 80 | + return rdict |
| 81 | + |
| 82 | + def get_DF_attrs(self) -> tuple[dict, dict]: |
| 83 | + """Genererate attributes for OP and Composition dataframes""" |
| 84 | + inchikey = self.sample.lipids[self._lname].metadata["bioschema_properties"]["inChIKey"] |
| 85 | + smiles = self.sample.lipids[self._lname].metadata["bioschema_properties"]["smiles"] |
| 86 | + opt_attr = { |
| 87 | + "inchikey": inchikey, |
| 88 | + "smiles": smiles, |
| 89 | + } |
| 90 | + hydration = self.sample.get_hydration() |
| 91 | + scontent = self.sample.solution_composition(basis="molar") |
| 92 | + smp_attr = { |
| 93 | + "hydration": hydration, |
| 94 | + "solution": ", ".join([f"{k:<25} {v * 100:>6.1f}%" for k, v in sorted(scontent.items())]), |
| 95 | + } |
| 96 | + return opt_attr, smp_attr |
| 97 | + |
| 98 | + @abstractmethod |
| 99 | + def get_H5_gname(self) -> str: |
| 100 | + """Generate name for the record in the H5 table""" |
| 101 | + |
| 102 | + def store_to_hdf5(self, hdf_fname: str) -> None: |
| 103 | + mcontent = self.get_mcontent() |
| 104 | + opt_attr, smp_attr = self.get_DF_attrs() |
| 105 | + # store all vars and df to the HDF5 table |
| 106 | + group = self.get_H5_gname() |
| 107 | + with pd.HDFStore(hdf_fname, "a") as store: |
| 108 | + # DataFrame table |
| 109 | + store.put(f"{group}/op_values", self._df, format="table", data_columns=True) |
| 110 | + opdf = pd.DataFrame(mcontent) |
| 111 | + print(opdf) |
| 112 | + store.put(f"{group}/sample_table", opdf, format="table", data_columns=True) |
| 113 | + # Metadata attributes - I |
| 114 | + op_storer = store.get_storer(f"{group}/op_values") |
| 115 | + for k, v in opt_attr.items(): |
| 116 | + op_storer.attrs[k] = v |
| 117 | + # -//- II |
| 118 | + sample_storer = store.get_storer(f"{group}/sample_table") |
| 119 | + for k, v in smp_attr.items(): |
| 120 | + sample_storer.attrs[k] = v |
| 121 | + |
| 122 | + |
| 123 | +class ExpOPDataStorer(OPDataStorer): |
| 124 | + """OP data storer for experiments""" |
| 125 | + |
| 126 | + DEFAULT_EXP_HDFNAME = "exp-op-dataset.h5" |
| 127 | + """Default filename for the experimental dataset""" |
| 128 | + |
| 129 | + @property |
| 130 | + def ass_id(self) -> str: |
| 131 | + return self._e.exp_id |
| 132 | + |
| 133 | + @property |
| 134 | + def sample(self) -> SampleComposition: |
| 135 | + return self._e |
| 136 | + |
| 137 | + def get_H5_gname(self) -> str: |
| 138 | + group = "E" |
| 139 | + group += re.sub(r"[^A-Za-z0-9_]", "_", unidecode(self._e.exp_id)) |
| 140 | + group += "__" + self._lname |
| 141 | + return group |
| 142 | + |
| 143 | + def get_DF_attrs(self) -> tuple[dict, dict]: |
| 144 | + opt_attr, smp_attr = super().get_DF_attrs() |
| 145 | + opt_attr ["fmdl_expid"] = self._e.exp_id |
| 146 | + nmr_method = self._e.metadata.get("NMR", {}).get("METHOD", False) |
| 147 | + if nmr_method: |
| 148 | + opt_attr["nmr_method"] = nmr_method |
| 149 | + smp_attr["temperature"] = self._e["TEMPERATURE"] |
| 150 | + return opt_attr, smp_attr |
| 151 | + |
| 152 | + def __init__(self, e: OPExperiment, lname: str) -> None: |
| 153 | + """Initialize with experiment object and lipid name""" |
| 154 | + self._e: OPExperiment = e |
| 155 | + self._lname = lname |
| 156 | + |
| 157 | + def prepare_dataframe(self) -> None: |
| 158 | + """Call dataframe preparation""" |
| 159 | + mol = self._e.lipids[self._lname] |
| 160 | + opdict = self._e.data[self._lname] |
| 161 | + self._prepare_df_common( |
| 162 | + mol, |
| 163 | + opdict, |
| 164 | + err_extractor=lambda x: OPExperiment.DEFAULT_ERROR if len(x) == 1 else x[1], |
| 165 | + ) |
| 166 | + |
| 167 | + |
| 168 | +class SimOPDataStorer(OPDataStorer): |
| 169 | + DEFAULT_SIMS_HDFNAME = "sims-op-dataset.h5" |
| 170 | + """Default Dataset Filename""" |
| 171 | + |
| 172 | + @property |
| 173 | + def ass_id(self): |
| 174 | + return self._s["ID"] |
| 175 | + |
| 176 | + @property |
| 177 | + def sample(self) -> SampleComposition: |
| 178 | + return self._s |
| 179 | + |
| 180 | + def prepare_dataframe(self): |
| 181 | + mol = self._s.lipids[self._lname] |
| 182 | + opdict = get_OP(self._s)[self._lname] |
| 183 | + self._prepare_df_common(mol, opdict, err_extractor=lambda x: x[2]) |
| 184 | + |
| 185 | + def get_H5_gname(self) -> str: |
| 186 | + return f"SIM_{self._s['ID']}__{self._lname}" |
| 187 | + |
| 188 | + def get_mcontent(self): |
| 189 | + retdic = super().get_mcontent() |
| 190 | + retdic["number"] = [0] * len(retdic["name"]) |
| 191 | + retdic["asymmetry"] = [0] * len(retdic["name"]) |
| 192 | + _simcomp = self._s["COMPOSITION"] |
| 193 | + for i, lname in enumerate(retdic["name"]): |
| 194 | + cnt = _simcomp[lname]["COUNT"] |
| 195 | + if isinstance(cnt, int): |
| 196 | + asm = np.nan |
| 197 | + cnt = [cnt / 2, cnt / 2] |
| 198 | + else: |
| 199 | + asm = cnt[0] / sum(cnt) |
| 200 | + retdic["number"][i] = sum(cnt) / 2 |
| 201 | + retdic["asymmetry"][i] = asm |
| 202 | + return retdic |
| 203 | + |
| 204 | + def get_DF_attrs(self) -> tuple[dict, dict]: |
| 205 | + opt_attr, smp_attr = super().get_DF_attrs() |
| 206 | + opt_attr["fmdl_simid"] = self._s["ID"] |
| 207 | + smp_attr["temperature"] = self._s["TEMPERATURE"] |
| 208 | + ff_name = self._s.readme.get("FF", False) |
| 209 | + if ff_name: |
| 210 | + smp_attr["ff_name"] = ff_name |
| 211 | + return opt_attr, smp_attr |
| 212 | + |
| 213 | + def __init__(self, sim: System, lname: str) -> None: |
| 214 | + self._s = sim |
| 215 | + self._lname = lname |
| 216 | + |
| 217 | + |
| 218 | +def main_exps(log: logging.Logger) -> tuple[int, int]: |
| 219 | + """Generate dataset for experiments""" |
| 220 | + stat_ok, stat_fail = 0, 0 |
| 221 | + log.info("\n\nGenerating OP datasets from experiments.") |
| 222 | + exps = ExperimentCollection.load_from_data("OPExperiment") |
| 223 | + for exp in exps: |
| 224 | + for lname in exp.data: |
| 225 | + log.info("%s // %s", str(exp), lname) |
| 226 | + ods = ExpOPDataStorer(exp, lname) |
| 227 | + try: |
| 228 | + ods.prepare_dataframe() |
| 229 | + except OPDataError as e: |
| 230 | + log.error("[from .prepare_dataframe] %s", str(e)) # noqa: TRY400 |
| 231 | + stat_fail += 1 |
| 232 | + continue |
| 233 | + else: |
| 234 | + stat_ok += 1 |
| 235 | + ods.store_to_hdf5(ExpOPDataStorer.DEFAULT_EXP_HDFNAME) |
| 236 | + log.info("..stored!") |
| 237 | + return stat_ok, stat_fail |
| 238 | + |
| 239 | + |
| 240 | +def main_sims(log: logging.Logger) -> tuple[int, int]: |
| 241 | + """Generate dataset from simulations""" |
| 242 | + stat_ok, stat_fail = 0, 0 |
| 243 | + log.info("\n\nGenerating OP dataset from simulations.") |
| 244 | + sims = initialize_databank() |
| 245 | + for sim in sims: |
| 246 | + opdata = get_OP(sim) |
| 247 | + for lname in opdata: |
| 248 | + if opdata is None or opdata[lname] is None: |
| 249 | + stat_fail += 1 |
| 250 | + continue |
| 251 | + log.info("%s // %s", str(sim), lname) |
| 252 | + ods = SimOPDataStorer(sim, lname) |
| 253 | + try: |
| 254 | + ods.prepare_dataframe() |
| 255 | + except OPDataError as e: |
| 256 | + log.error("[from .prepare_dataframe] %s", str(e)) # noqa: TRY400 |
| 257 | + stat_fail += 1 |
| 258 | + continue |
| 259 | + else: |
| 260 | + stat_ok += 1 |
| 261 | + ods.store_to_hdf5(SimOPDataStorer.DEFAULT_SIMS_HDFNAME) |
| 262 | + log.info("..stored!") |
| 263 | + return stat_ok, stat_fail |
| 264 | + |
| 265 | + |
| 266 | +if __name__ == "__main__": |
| 267 | + parser = argparse.ArgumentParser( |
| 268 | + prog="OP Dataset Generator", |
| 269 | + description=""" |
| 270 | +CI helper script for delivering dataset for Kaggle in HDF5 format. |
| 271 | +Two DataFrames are stored for each Sim/Exp-lipid pair: |
| 272 | +1. SMILES-aligned values for each atom of the molecule |
| 273 | +2. Composition table of membrane part of the system""", |
| 274 | + formatter_class=argparse.RawDescriptionHelpFormatter, |
| 275 | + ) |
| 276 | + parser.add_argument("--exps", action="store_true", help="Generate DS from experiments") |
| 277 | + parser.add_argument("--sims", action="store_true", help="Generate DS from simulations") |
| 278 | + args = parser.parse_args() |
| 279 | + |
| 280 | + if not args.exps and not args.sims: |
| 281 | + parser.print_usage() |
| 282 | + sys.exit(1) |
| 283 | + |
| 284 | + lg = logging.getLogger("cli") |
| 285 | + lg.setLevel(logging.INFO) |
| 286 | + h_stderr = logging.StreamHandler(sys.stderr) |
| 287 | + h_stderr.setLevel(logging.INFO) |
| 288 | + lg.addHandler(h_stderr) |
| 289 | + |
| 290 | + if args.exps: |
| 291 | + e_ok, e_fail = main_exps(lg) |
| 292 | + if args.sims: |
| 293 | + s_ok, s_fail = main_sims(lg) |
| 294 | + |
| 295 | + lg.info("======= STATISTICS ========") |
| 296 | + if args.exps: |
| 297 | + lg.info(f"Stored experiment-lipid pairs: {e_ok} // failed: {e_fail}") |
| 298 | + if args.sims: |
| 299 | + lg.info(f"Stored simulation-lipid pairs: {s_ok} // failed: {s_fail}") |
0 commit comments