PFASGroups 3.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- HalogenGroups/__init__.py +246 -0
- PFASGroups/ComponentsSolverModel.py +977 -0
- PFASGroups/HalogenGroupModel.py +810 -0
- PFASGroups/PFASDefinitionModel.py +393 -0
- PFASGroups/PFASEmbeddings.py +3315 -0
- PFASGroups/__init__.py +21 -0
- PFASGroups/cli.py +618 -0
- PFASGroups/core.py +415 -0
- PFASGroups/data/Halogen_groups_smarts.json +9024 -0
- PFASGroups/data/PFAS_definitions_smarts.json +170 -0
- PFASGroups/data/component_smarts.json +4 -0
- PFASGroups/data/component_smarts_halogens.json +142 -0
- PFASGroups/data/diatomic_bonds_dict.json +12802 -0
- PFASGroups/draw_mols.py +374 -0
- PFASGroups/embeddings.py +150 -0
- PFASGroups/fragmentation.py +548 -0
- PFASGroups/generate_homologues.py +256 -0
- PFASGroups/generate_mol.py +656 -0
- PFASGroups/generate_paper_figures.py +266 -0
- PFASGroups/getter.py +111 -0
- PFASGroups/homologue_series.py +473 -0
- PFASGroups/parser.py +942 -0
- PFASGroups/prioritise.py +439 -0
- pfasgroups-3.2.2.dist-info/METADATA +724 -0
- pfasgroups-3.2.2.dist-info/RECORD +28 -0
- pfasgroups-3.2.2.dist-info/WHEEL +5 -0
- pfasgroups-3.2.2.dist-info/entry_points.txt +3 -0
- pfasgroups-3.2.2.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""HomologueSeries – result container for :func:`~PFASGroups.generate_homologues`.
|
|
2
|
+
|
|
3
|
+
Backward-compatible with the raw ``{InChIKey: {formula: mol}}`` dict that
|
|
4
|
+
``generate_homologues`` previously returned. All existing code that iterates
|
|
5
|
+
or indexes the dict continues to work unchanged.
|
|
6
|
+
|
|
7
|
+
The class adds:
|
|
8
|
+
|
|
9
|
+
- :meth:`summary` / :meth:`summarise` – coloured text summary
|
|
10
|
+
- :meth:`show` / :meth:`plot` – grid image of all homologues
|
|
11
|
+
- :meth:`svg` – export to an SVG file
|
|
12
|
+
- :meth:`to_sql` – persist to SQLite or PostgreSQL
|
|
13
|
+
|
|
14
|
+
Example
|
|
15
|
+
-------
|
|
16
|
+
>>> from rdkit import Chem
|
|
17
|
+
>>> from HalogenGroups import generate_homologues
|
|
18
|
+
>>> pfoa = Chem.MolFromSmiles('OC(=O)' + 'C(F)(F)' * 7 + 'F')
|
|
19
|
+
>>> series = generate_homologues(pfoa)
|
|
20
|
+
>>> series.summary()
|
|
21
|
+
>>> img = series.show()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from io import BytesIO
|
|
28
|
+
from typing import Any, List, Optional, Tuple, Union
|
|
29
|
+
|
|
30
|
+
from PIL import Image
|
|
31
|
+
from rdkit import Chem
|
|
32
|
+
from rdkit.Chem import Draw
|
|
33
|
+
from rdkit.Chem.rdMolDescriptors import CalcMolFormula
|
|
34
|
+
|
|
35
|
+
# Re-use ANSI helpers and grid utility from PFASEmbeddings
|
|
36
|
+
from .PFASEmbeddings import _ansi, _ANSI_BOLD, _ANSI_HALOGEN, _grid_images
|
|
37
|
+
|
|
38
|
+
Colour = Tuple[float, float, float]
|
|
39
|
+
Color = Colour # US alias
|
|
40
|
+
|
|
41
|
+
# Colours for parent vs homologue panels
|
|
42
|
+
_PARENT_COLOUR: Colour = (0.95, 0.50, 0.10) # orange
|
|
43
|
+
_PARENT_COLOR = _PARENT_COLOUR # US alias
|
|
44
|
+
_HOMOLOGUE_COLOUR: Colour = (0.20, 0.60, 0.95) # blue
|
|
45
|
+
_HOMOLOGUE_COLOR = _HOMOLOGUE_COLOUR # US alias
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# HomologueEntry – lightweight descriptor for a single homologue
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class HomologueEntry:
|
|
54
|
+
"""One homologue molecule from a :class:`HomologueSeries`.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
mol :
|
|
59
|
+
RDKit molecule.
|
|
60
|
+
formula :
|
|
61
|
+
Molecular formula string (e.g. ``'C6HF11O2'``).
|
|
62
|
+
inchikey :
|
|
63
|
+
InChIKey of this homologue.
|
|
64
|
+
n_removed :
|
|
65
|
+
Number of CX2 units removed relative to the parent.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
mol: Chem.Mol
|
|
69
|
+
formula: str
|
|
70
|
+
inchikey: str
|
|
71
|
+
n_removed: int
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def smiles(self) -> str:
|
|
75
|
+
return Chem.MolToSmiles(self.mol)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def n_carbons(self) -> int:
|
|
79
|
+
return sum(1 for a in self.mol.GetAtoms() if a.GetSymbol() == 'C')
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
82
|
+
return (
|
|
83
|
+
f"HomologueEntry(formula={self.formula!r}, "
|
|
84
|
+
f"n_removed={self.n_removed}, smiles={self.smiles!r})"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# HomologueSeries – dict subclass (backward-compatible) with rich API
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class HomologueSeries(dict):
|
|
93
|
+
"""Result container for :func:`~PFASGroups.generate_homologues`.
|
|
94
|
+
|
|
95
|
+
Inherits from :class:`dict` so that ``series[inchikey][formula]`` access
|
|
96
|
+
and ``for inchikey, inner in series.items()`` still work exactly as before.
|
|
97
|
+
|
|
98
|
+
Additional attributes set by :func:`~PFASGroups.generate_homologues`:
|
|
99
|
+
|
|
100
|
+
Attributes
|
|
101
|
+
----------
|
|
102
|
+
parent_mol : rdkit.Chem.Mol or None
|
|
103
|
+
The original molecule that was shortened.
|
|
104
|
+
parent_smiles : str or None
|
|
105
|
+
Canonical SMILES of the parent.
|
|
106
|
+
parent_formula : str or None
|
|
107
|
+
Molecular formula of the parent.
|
|
108
|
+
halogen : str
|
|
109
|
+
Halogen element symbol used (``'F'``, ``'Cl'``, …).
|
|
110
|
+
component_name : str
|
|
111
|
+
Name of the component SMARTS used (e.g. ``'Perfluoroalkyl'``).
|
|
112
|
+
|
|
113
|
+
These are set post-construction via :meth:`_set_metadata` (called
|
|
114
|
+
internally by ``generate_homologues``).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, *args, **kwargs):
|
|
118
|
+
super().__init__(*args, **kwargs)
|
|
119
|
+
# Metadata – populated by generate_homologues after construction
|
|
120
|
+
self.parent_mol: Optional[Chem.Mol] = None
|
|
121
|
+
self.parent_smiles: Optional[str] = None
|
|
122
|
+
self.parent_formula: Optional[str] = None
|
|
123
|
+
self.halogen: str = 'F'
|
|
124
|
+
self.component_name: str = 'Perfluoroalkyl'
|
|
125
|
+
|
|
126
|
+
def _set_metadata(
|
|
127
|
+
self,
|
|
128
|
+
parent_mol: Chem.Mol,
|
|
129
|
+
halogen: str,
|
|
130
|
+
component_name: str,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Called by ``generate_homologues`` to attach provenance info."""
|
|
133
|
+
self.parent_mol = parent_mol
|
|
134
|
+
self.parent_smiles = Chem.MolToSmiles(parent_mol)
|
|
135
|
+
self.parent_formula = CalcMolFormula(parent_mol)
|
|
136
|
+
self.halogen = halogen
|
|
137
|
+
self.component_name = component_name
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Convenience iteration helpers
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def entries(self) -> List[HomologueEntry]:
|
|
144
|
+
"""Return all homologues as a sorted list of :class:`HomologueEntry`.
|
|
145
|
+
|
|
146
|
+
Sorted by the number of CX2 units removed (ascending), i.e. from
|
|
147
|
+
the longest (closest to parent) to the shortest homologue.
|
|
148
|
+
"""
|
|
149
|
+
parent_c = (
|
|
150
|
+
sum(1 for a in self.parent_mol.GetAtoms() if a.GetSymbol() == 'C')
|
|
151
|
+
if self.parent_mol is not None else None
|
|
152
|
+
)
|
|
153
|
+
result: List[HomologueEntry] = []
|
|
154
|
+
for inchikey, inner in self.items():
|
|
155
|
+
for formula, mol in inner.items():
|
|
156
|
+
child_c = sum(1 for a in mol.GetAtoms() if a.GetSymbol() == 'C')
|
|
157
|
+
n_removed = (parent_c - child_c) if parent_c is not None else 0
|
|
158
|
+
result.append(
|
|
159
|
+
HomologueEntry(
|
|
160
|
+
mol=mol,
|
|
161
|
+
formula=formula,
|
|
162
|
+
inchikey=inchikey,
|
|
163
|
+
n_removed=n_removed,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
result.sort(key=lambda e: e.n_removed)
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def mols(self) -> List[Chem.Mol]:
|
|
170
|
+
"""Return all homologue molecules in order (shortest first)."""
|
|
171
|
+
return [e.mol for e in self.entries()]
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
# Text output
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def summarise(self) -> str:
|
|
178
|
+
"""Return a coloured text summary of the homologous series."""
|
|
179
|
+
hal_code = _ANSI_HALOGEN.get(self.halogen, '')
|
|
180
|
+
lines: List[str] = []
|
|
181
|
+
lines.append(_ansi("HomologueSeries summary", _ANSI_BOLD))
|
|
182
|
+
if self.parent_smiles:
|
|
183
|
+
lines.append(
|
|
184
|
+
f"- Parent : {self.parent_smiles} "
|
|
185
|
+
f"({_ansi(self.parent_formula or '?', hal_code)})"
|
|
186
|
+
)
|
|
187
|
+
lines.append(
|
|
188
|
+
f"- Halogen : {_ansi(self.halogen, hal_code)}"
|
|
189
|
+
f" | Component : {self.component_name}"
|
|
190
|
+
)
|
|
191
|
+
if not self:
|
|
192
|
+
lines.append("- No homologues found (no repeating CX2 units).")
|
|
193
|
+
return "\n".join(lines)
|
|
194
|
+
|
|
195
|
+
lines.append(f"- Homologues ({len(self)}):")
|
|
196
|
+
for entry in self.entries():
|
|
197
|
+
line = (
|
|
198
|
+
f" [{entry.n_removed:+d} CX2] "
|
|
199
|
+
f"{_ansi(entry.formula, hal_code)} "
|
|
200
|
+
f"{entry.smiles}"
|
|
201
|
+
)
|
|
202
|
+
lines.append(line)
|
|
203
|
+
return "\n".join(lines)
|
|
204
|
+
|
|
205
|
+
def summary(self) -> None:
|
|
206
|
+
"""Print the coloured text summary."""
|
|
207
|
+
print(self.summarise())
|
|
208
|
+
|
|
209
|
+
# ------------------------------------------------------------------
|
|
210
|
+
# Visualisation helpers
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _draw_mol_image(
|
|
214
|
+
self,
|
|
215
|
+
mol: Chem.Mol,
|
|
216
|
+
legend: str = "",
|
|
217
|
+
subwidth: int = 300,
|
|
218
|
+
subheight: int = 300,
|
|
219
|
+
highlight_atoms: Optional[List[int]] = None,
|
|
220
|
+
highlight_colour: Optional[Colour] = None,
|
|
221
|
+
) -> Image.Image:
|
|
222
|
+
"""Render *mol* as a PIL image using Cairo."""
|
|
223
|
+
d2d = Draw.MolDraw2DCairo(subwidth, subheight)
|
|
224
|
+
dopts = d2d.drawOptions()
|
|
225
|
+
dopts.useBWAtomPalette()
|
|
226
|
+
dopts.fixedBondLength = 20
|
|
227
|
+
dopts.addAtomIndices = False
|
|
228
|
+
dopts.maxFontSize = 14
|
|
229
|
+
dopts.minFontSize = 11
|
|
230
|
+
|
|
231
|
+
hl_atoms = highlight_atoms or []
|
|
232
|
+
if hl_atoms and highlight_colour:
|
|
233
|
+
atom_colors = {idx: highlight_colour for idx in hl_atoms}
|
|
234
|
+
d2d.DrawMolecule(
|
|
235
|
+
mol,
|
|
236
|
+
legend=legend,
|
|
237
|
+
highlightAtoms=hl_atoms,
|
|
238
|
+
highlightAtomColors=atom_colors,
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
d2d.DrawMolecule(mol, legend=legend, highlightAtoms=hl_atoms)
|
|
242
|
+
|
|
243
|
+
d2d.FinishDrawing()
|
|
244
|
+
return Image.open(BytesIO(d2d.GetDrawingText()))
|
|
245
|
+
|
|
246
|
+
def _draw_mol_svg(
|
|
247
|
+
self,
|
|
248
|
+
mol: Chem.Mol,
|
|
249
|
+
legend: str = "",
|
|
250
|
+
subwidth: int = 300,
|
|
251
|
+
subheight: int = 300,
|
|
252
|
+
) -> str:
|
|
253
|
+
"""Render *mol* as an SVG string."""
|
|
254
|
+
d2d = Draw.MolDraw2DSVG(subwidth, subheight)
|
|
255
|
+
dopts = d2d.drawOptions()
|
|
256
|
+
dopts.useBWAtomPalette()
|
|
257
|
+
dopts.fixedBondLength = 20
|
|
258
|
+
dopts.addAtomIndices = False
|
|
259
|
+
dopts.maxFontSize = 14
|
|
260
|
+
dopts.minFontSize = 11
|
|
261
|
+
d2d.DrawMolecule(mol, legend=legend)
|
|
262
|
+
d2d.FinishDrawing()
|
|
263
|
+
return d2d.GetDrawingText()
|
|
264
|
+
|
|
265
|
+
def show(
|
|
266
|
+
self,
|
|
267
|
+
display: bool = True,
|
|
268
|
+
subwidth: int = 300,
|
|
269
|
+
subheight: int = 300,
|
|
270
|
+
ncols: int = 4,
|
|
271
|
+
show_parent: bool = True,
|
|
272
|
+
) -> Image.Image:
|
|
273
|
+
"""Render all homologues as a grid image.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
display :
|
|
278
|
+
Whether to call ``image.show()`` immediately.
|
|
279
|
+
subwidth, subheight :
|
|
280
|
+
Pixel dimensions of each panel.
|
|
281
|
+
ncols :
|
|
282
|
+
Number of columns in the grid.
|
|
283
|
+
show_parent :
|
|
284
|
+
If ``True``, prepend a panel showing the parent molecule
|
|
285
|
+
(highlighted in orange) before the homologues.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
PIL.Image.Image
|
|
290
|
+
Grid image.
|
|
291
|
+
"""
|
|
292
|
+
if not self and not (show_parent and self.parent_mol is not None):
|
|
293
|
+
raise ValueError("No molecules to display (empty HomologueSeries).")
|
|
294
|
+
|
|
295
|
+
imgs: List[Image.Image] = []
|
|
296
|
+
|
|
297
|
+
if show_parent and self.parent_mol is not None:
|
|
298
|
+
parent_label = f"Parent {self.parent_formula or ''}"
|
|
299
|
+
img = self._draw_mol_image(
|
|
300
|
+
self.parent_mol,
|
|
301
|
+
legend=parent_label,
|
|
302
|
+
subwidth=subwidth,
|
|
303
|
+
subheight=subheight,
|
|
304
|
+
)
|
|
305
|
+
imgs.append(img)
|
|
306
|
+
|
|
307
|
+
for entry in self.entries():
|
|
308
|
+
label = f"-{entry.n_removed} CX2 {entry.formula}"
|
|
309
|
+
img = self._draw_mol_image(
|
|
310
|
+
entry.mol,
|
|
311
|
+
legend=label,
|
|
312
|
+
subwidth=subwidth,
|
|
313
|
+
subheight=subheight,
|
|
314
|
+
)
|
|
315
|
+
imgs.append(img)
|
|
316
|
+
|
|
317
|
+
if not imgs:
|
|
318
|
+
raise ValueError("No molecules to display (empty HomologueSeries).")
|
|
319
|
+
|
|
320
|
+
grid, _, _ = _grid_images(imgs, buffer=4, ncols=ncols)
|
|
321
|
+
if display:
|
|
322
|
+
grid.show()
|
|
323
|
+
return grid
|
|
324
|
+
|
|
325
|
+
# Alias
|
|
326
|
+
plot = show
|
|
327
|
+
|
|
328
|
+
def svg(
|
|
329
|
+
self,
|
|
330
|
+
filename: str,
|
|
331
|
+
subwidth: int = 300,
|
|
332
|
+
subheight: int = 300,
|
|
333
|
+
ncols: int = 4,
|
|
334
|
+
show_parent: bool = True,
|
|
335
|
+
) -> str:
|
|
336
|
+
"""Export all homologues to an SVG file.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
filename :
|
|
341
|
+
Destination path (e.g. ``'series.svg'``).
|
|
342
|
+
subwidth, subheight :
|
|
343
|
+
Pixel dimensions of each panel.
|
|
344
|
+
ncols :
|
|
345
|
+
Number of columns.
|
|
346
|
+
show_parent :
|
|
347
|
+
Prepend a parent-molecule panel.
|
|
348
|
+
|
|
349
|
+
Returns
|
|
350
|
+
-------
|
|
351
|
+
str
|
|
352
|
+
The path written to (*filename*).
|
|
353
|
+
"""
|
|
354
|
+
import svgutils.transform as sg
|
|
355
|
+
from .draw_mols import merge_svg
|
|
356
|
+
|
|
357
|
+
svg_strs: List[str] = []
|
|
358
|
+
|
|
359
|
+
if show_parent and self.parent_mol is not None:
|
|
360
|
+
parent_label = f"Parent {self.parent_formula or ''}"
|
|
361
|
+
svg_strs.append(
|
|
362
|
+
self._draw_mol_svg(self.parent_mol, legend=parent_label,
|
|
363
|
+
subwidth=subwidth, subheight=subheight)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
for entry in self.entries():
|
|
367
|
+
label = f"-{entry.n_removed} CX2 {entry.formula}"
|
|
368
|
+
svg_strs.append(
|
|
369
|
+
self._draw_mol_svg(entry.mol, legend=label,
|
|
370
|
+
subwidth=subwidth, subheight=subheight)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if not svg_strs:
|
|
374
|
+
raise ValueError("No molecules to export (empty HomologueSeries).")
|
|
375
|
+
|
|
376
|
+
figs = [sg.fromstring(s) for s in svg_strs]
|
|
377
|
+
grid, _, _ = merge_svg(figs, buffer=4, ncols=ncols)
|
|
378
|
+
grid.save(filename)
|
|
379
|
+
return filename
|
|
380
|
+
|
|
381
|
+
# ------------------------------------------------------------------
|
|
382
|
+
# Persistence
|
|
383
|
+
# ------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def to_sql(
|
|
386
|
+
self,
|
|
387
|
+
conn: Optional[Union[str, Any]] = None,
|
|
388
|
+
filename: Optional[str] = None,
|
|
389
|
+
table_name: str = "homologue_series",
|
|
390
|
+
if_exists: str = "append",
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Save the homologous series to a SQL database.
|
|
393
|
+
|
|
394
|
+
Each row represents one homologue. The parent molecule is stored in
|
|
395
|
+
the ``parent_smiles`` column for grouping.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
conn :
|
|
400
|
+
SQLAlchemy engine, connection string, or ``None``.
|
|
401
|
+
filename :
|
|
402
|
+
SQLite database path (used when *conn* is ``None``).
|
|
403
|
+
table_name :
|
|
404
|
+
Name of the table to write to.
|
|
405
|
+
if_exists :
|
|
406
|
+
``'append'``, ``'replace'``, or ``'fail'``.
|
|
407
|
+
|
|
408
|
+
Raises
|
|
409
|
+
------
|
|
410
|
+
ValueError
|
|
411
|
+
If neither *conn* nor *filename* is supplied.
|
|
412
|
+
ImportError
|
|
413
|
+
If ``pandas`` or ``sqlalchemy`` are not installed.
|
|
414
|
+
|
|
415
|
+
Examples
|
|
416
|
+
--------
|
|
417
|
+
>>> series.to_sql(filename='pfas_homologues.db')
|
|
418
|
+
>>> series.to_sql(conn='postgresql://user:pass@localhost/pfasdb')
|
|
419
|
+
"""
|
|
420
|
+
try:
|
|
421
|
+
import pandas as pd
|
|
422
|
+
import sqlalchemy
|
|
423
|
+
except ImportError as exc:
|
|
424
|
+
raise ImportError(
|
|
425
|
+
"pandas and sqlalchemy are required for to_sql. "
|
|
426
|
+
"Install with: pip install pandas sqlalchemy"
|
|
427
|
+
) from exc
|
|
428
|
+
|
|
429
|
+
if conn is None and filename is None:
|
|
430
|
+
raise ValueError("Either 'conn' or 'filename' must be provided.")
|
|
431
|
+
|
|
432
|
+
if conn is not None:
|
|
433
|
+
engine = (
|
|
434
|
+
sqlalchemy.create_engine(conn)
|
|
435
|
+
if isinstance(conn, str)
|
|
436
|
+
else conn
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
engine = sqlalchemy.create_engine(f"sqlite:///{filename}")
|
|
440
|
+
|
|
441
|
+
rows = []
|
|
442
|
+
for entry in self.entries():
|
|
443
|
+
rows.append(
|
|
444
|
+
{
|
|
445
|
+
"parent_smiles": self.parent_smiles or "",
|
|
446
|
+
"parent_formula": self.parent_formula or "",
|
|
447
|
+
"halogen": self.halogen,
|
|
448
|
+
"component_name": self.component_name,
|
|
449
|
+
"inchikey": entry.inchikey,
|
|
450
|
+
"formula": entry.formula,
|
|
451
|
+
"smiles": entry.smiles,
|
|
452
|
+
"n_removed": entry.n_removed,
|
|
453
|
+
"n_carbons": entry.n_carbons,
|
|
454
|
+
}
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if not rows:
|
|
458
|
+
return # nothing to write
|
|
459
|
+
|
|
460
|
+
df = pd.DataFrame(rows)
|
|
461
|
+
df.to_sql(table_name, engine, if_exists=if_exists, index=False)
|
|
462
|
+
|
|
463
|
+
# ------------------------------------------------------------------
|
|
464
|
+
# Dunder niceties
|
|
465
|
+
# ------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
468
|
+
parent = self.parent_smiles or "?"
|
|
469
|
+
return (
|
|
470
|
+
f"HomologueSeries(parent={parent!r}, "
|
|
471
|
+
f"halogen={self.halogen!r}, "
|
|
472
|
+
f"n_homologues={len(self)})"
|
|
473
|
+
)
|