Skip to content

Fingerprints

Inorganic Composition Fingerprints

matgpr.inorganic_fingerprints

CompositionFingerprintResult dataclass

Result returned by composition featurization helpers.

Attributes:

Name Type Description
features DataFrame

Numeric composition descriptors indexed like the input formulas.

failed DataFrame

Formulas that could not be parsed or featurized. Empty when errors="raise" and no exception was raised.

cache_keys Series | None

Deterministic cache keys for each input row.

cache_hit Series | None

Boolean flags indicating whether each row was loaded from cache.

Source code in matgpr/inorganic_fingerprints.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@dataclass(frozen=True)
class CompositionFingerprintResult:
    """Result returned by composition featurization helpers.

    Attributes
    ----------
    features
        Numeric composition descriptors indexed like the input formulas.
    failed
        Formulas that could not be parsed or featurized. Empty when
        ``errors="raise"`` and no exception was raised.
    cache_keys
        Deterministic cache keys for each input row.
    cache_hit
        Boolean flags indicating whether each row was loaded from cache.
    """

    features: pd.DataFrame
    failed: pd.DataFrame
    cache_keys: pd.Series | None = None
    cache_hit: pd.Series | None = None

clean_formula(formula)

Normalize common text artifacts before parsing an inorganic formula.

Source code in matgpr/inorganic_fingerprints.py
80
81
82
83
84
def clean_formula(formula: object) -> str:
    """Normalize common text artifacts before parsing an inorganic formula."""
    if formula is None or (isinstance(formula, float) and np.isnan(formula)):
        return ""
    return str(formula).replace("\u00a0", "").strip()

composition_fingerprint(formula, *, properties=DEFAULT_ELEMENTAL_PROPERTIES, statistics=DEFAULT_COMPOSITION_STATISTICS)

Create statistical elemental-property descriptors for one composition.

The default descriptor set follows a compact materials-informatics pattern: each elemental property is summarized by composition-weighted statistics such as the fraction-weighted mean (fwm), absolute deviation (ad), minimum, maximum, range, and standard deviation.

Parameters:

Name Type Description Default
formula object

Inorganic composition string, for example "B4C" or "Ag0.05Gd0.048Pd0.902".

required
properties Sequence[str]

Element-level properties to summarize. Supported defaults include atomic number, mass, electronegativity, radius, periodic-table row and column, and s/p/d/f valence counts.

DEFAULT_ELEMENTAL_PROPERTIES
statistics Sequence[str]

Summary statistics to compute for each property.

DEFAULT_COMPOSITION_STATISTICS
Source code in matgpr/inorganic_fingerprints.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def composition_fingerprint(
    formula: object,
    *,
    properties: Sequence[str] = DEFAULT_ELEMENTAL_PROPERTIES,
    statistics: Sequence[str] = DEFAULT_COMPOSITION_STATISTICS,
) -> dict[str, float]:
    """Create statistical elemental-property descriptors for one composition.

    The default descriptor set follows a compact materials-informatics pattern:
    each elemental property is summarized by composition-weighted statistics
    such as the fraction-weighted mean (``fwm``), absolute deviation (``ad``),
    minimum, maximum, range, and standard deviation.

    Parameters
    ----------
    formula
        Inorganic composition string, for example ``"B4C"`` or
        ``"Ag0.05Gd0.048Pd0.902"``.
    properties
        Element-level properties to summarize. Supported defaults include
        atomic number, mass, electronegativity, radius, periodic-table row and
        column, and s/p/d/f valence counts.
    statistics
        Summary statistics to compute for each property.
    """
    composition = _parse_composition(formula)
    element_amounts = composition.get_el_amt_dict()
    total_amount = float(sum(element_amounts.values()))
    if total_amount <= 0:
        raise ValueError(f"Composition '{formula}' has no positive element amounts")

    elements = [_element(symbol) for symbol in element_amounts]
    fractions = np.array(
        [element_amounts[element.symbol] / total_amount for element in elements], dtype=float
    )

    features: dict[str, float] = {}
    for property_name in properties:
        values = np.array(
            [_element_property(element, property_name) for element in elements], dtype=float
        )
        for statistic in statistics:
            features[f"{property_name}_{statistic}"] = _weighted_statistic(
                values,
                fractions,
                statistic,
                property_name=property_name,
            )
    return features

default_element_symbols(max_atomic_number=118)

Return periodic-table element symbols up to max_atomic_number.

Source code in matgpr/inorganic_fingerprints.py
138
139
140
141
142
143
144
def default_element_symbols(max_atomic_number: int = 118) -> tuple[str, ...]:
    """Return periodic-table element symbols up to ``max_atomic_number``."""
    if not isinstance(max_atomic_number, int) or max_atomic_number < 1:
        raise ValueError("max_atomic_number must be a positive integer")
    if max_atomic_number > 118:
        raise ValueError("max_atomic_number must be less than or equal to 118")
    return tuple(_element_from_atomic_number(z).symbol for z in range(1, max_atomic_number + 1))

element_fraction_fingerprint(formula, *, elements=None)

Create a fixed-length elemental-fraction vector for one formula.

Each output feature is named element_fraction_<symbol>. If elements is omitted, the vector spans all elements up to atomic number 118. Passing a smaller element list is useful for project-specific models, but every element present in formula must be included in that list.

Source code in matgpr/inorganic_fingerprints.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def element_fraction_fingerprint(
    formula: object,
    *,
    elements: Sequence[str] | None = None,
) -> dict[str, float]:
    """Create a fixed-length elemental-fraction vector for one formula.

    Each output feature is named ``element_fraction_<symbol>``. If ``elements``
    is omitted, the vector spans all elements up to atomic number 118. Passing a
    smaller element list is useful for project-specific models, but every
    element present in ``formula`` must be included in that list.
    """
    symbols = _validate_element_symbols(default_element_symbols() if elements is None else elements)
    return _element_fraction_values(formula, symbols)

featurize_element_fractions(formulas, *, elements=None, errors='raise', cache_dir=None)

Featurize formulas as fixed element-fraction vectors.

Element-fraction vectors are the recommended inputs for :class:matgpr.ElementFractionKernel. They preserve direct compositional information, unlike statistical elemental-property descriptors.

Source code in matgpr/inorganic_fingerprints.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def featurize_element_fractions(
    formulas: Iterable[object],
    *,
    elements: Sequence[str] | None = None,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> CompositionFingerprintResult:
    """Featurize formulas as fixed element-fraction vectors.

    Element-fraction vectors are the recommended inputs for
    :class:`matgpr.ElementFractionKernel`. They preserve direct compositional
    information, unlike statistical elemental-property descriptors.
    """
    if errors not in {"raise", "coerce"}:
        raise ValueError("errors must be either 'raise' or 'coerce'")

    symbols = _validate_element_symbols(default_element_symbols() if elements is None else elements)
    feature_names = _element_fraction_feature_names(symbols)
    cache_parameters = {"elements": list(symbols)}

    rows: list[Mapping[str, float]] = []
    failures: list[dict[str, object]] = []
    cache_keys: list[str] = []
    cache_hits: list[bool] = []

    for index, formula in enumerate(formulas):
        normalized_formula = clean_formula(formula)
        cache_key = fingerprint_cache_key(
            namespace="element_fraction",
            value=normalized_formula,
            parameters=cache_parameters,
        )
        cache_keys.append(cache_key)

        try:
            cached_features = _read_cached_feature_row(
                cache_dir,
                namespace="element_fraction",
                cache_key=cache_key,
                feature_names=feature_names,
            )
            if cached_features is not None:
                rows.append(cached_features)
                cache_hits.append(True)
                continue

            features = _element_fraction_values(formula, symbols)
            rows.append(features)
            cache_hits.append(False)
            _write_cached_feature_row(
                cache_dir,
                namespace="element_fraction",
                cache_key=cache_key,
                input_value=normalized_formula,
                features=features,
                metadata=cache_parameters,
            )
        except Exception as exc:
            if errors == "raise":
                raise ValueError(
                    f"Could not featurize element fractions at position {index}: {formula!r}"
                ) from exc
            failures.append(
                {
                    "index": index,
                    "formula": formula,
                    "cache_key": cache_key,
                    "error": str(exc),
                }
            )
            rows.append({name: np.nan for name in feature_names})
            cache_hits.append(False)

    return CompositionFingerprintResult(
        features=pd.DataFrame(rows, columns=feature_names),
        failed=pd.DataFrame(failures, columns=["index", "formula", "cache_key", "error"]),
        cache_keys=pd.Series(cache_keys, name="element_fraction_cache_key"),
        cache_hit=pd.Series(cache_hits, name="element_fraction_cache_hit"),
    )

featurize_compositions(formulas, *, properties=DEFAULT_ELEMENTAL_PROPERTIES, statistics=DEFAULT_COMPOSITION_STATISTICS, errors='raise', cache_dir=None)

Featurize a sequence of inorganic formulas.

Parameters:

Name Type Description Default
formulas Iterable[object]

Iterable of formula strings.

required
properties Sequence[str]

Descriptor controls passed to composition_fingerprint.

DEFAULT_ELEMENTAL_PROPERTIES
statistics Sequence[str]

Descriptor controls passed to composition_fingerprint.

DEFAULT_ELEMENTAL_PROPERTIES
errors str

"raise" stops at the first invalid formula. "coerce" returns rows filled with NaN and records failures in failed.

'raise'
cache_dir str | Path | None

Optional directory for persistent row-level JSON cache files. Cache keys are deterministic hashes of the normalized formula and descriptor settings.

None
Source code in matgpr/inorganic_fingerprints.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def featurize_compositions(
    formulas: Iterable[object],
    *,
    properties: Sequence[str] = DEFAULT_ELEMENTAL_PROPERTIES,
    statistics: Sequence[str] = DEFAULT_COMPOSITION_STATISTICS,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> CompositionFingerprintResult:
    """Featurize a sequence of inorganic formulas.

    Parameters
    ----------
    formulas
        Iterable of formula strings.
    properties, statistics
        Descriptor controls passed to ``composition_fingerprint``.
    errors
        ``"raise"`` stops at the first invalid formula. ``"coerce"`` returns
        rows filled with ``NaN`` and records failures in ``failed``.
    cache_dir
        Optional directory for persistent row-level JSON cache files. Cache keys
        are deterministic hashes of the normalized formula and descriptor
        settings.
    """
    if errors not in {"raise", "coerce"}:
        raise ValueError("errors must be either 'raise' or 'coerce'")

    rows: list[Mapping[str, float]] = []
    failures: list[dict[str, object]] = []
    cache_keys: list[str] = []
    cache_hits: list[bool] = []
    feature_names = [
        f"{property_name}_{statistic}" for property_name in properties for statistic in statistics
    ]
    cache_parameters = {
        "properties": list(properties),
        "statistics": list(statistics),
    }

    for index, formula in enumerate(formulas):
        normalized_formula = clean_formula(formula)
        cache_key = fingerprint_cache_key(
            namespace="composition",
            value=normalized_formula,
            parameters=cache_parameters,
        )
        cache_keys.append(cache_key)

        try:
            cached_features = _read_cached_feature_row(
                cache_dir,
                namespace="composition",
                cache_key=cache_key,
                feature_names=feature_names,
            )
            if cached_features is not None:
                rows.append(cached_features)
                cache_hits.append(True)
                continue

            features = composition_fingerprint(
                formula,
                properties=properties,
                statistics=statistics,
            )
            rows.append(features)
            cache_hits.append(False)
            _write_cached_feature_row(
                cache_dir,
                namespace="composition",
                cache_key=cache_key,
                input_value=normalized_formula,
                features=features,
                metadata=cache_parameters,
            )
        except Exception as exc:
            if errors == "raise":
                raise ValueError(
                    f"Could not featurize formula at position {index}: {formula!r}"
                ) from exc
            failures.append(
                {
                    "index": index,
                    "formula": formula,
                    "cache_key": cache_key,
                    "error": str(exc),
                }
            )
            rows.append({name: np.nan for name in feature_names})
            cache_hits.append(False)

    return CompositionFingerprintResult(
        features=pd.DataFrame(rows, columns=feature_names),
        failed=pd.DataFrame(failures, columns=["index", "formula", "cache_key", "error"]),
        cache_keys=pd.Series(cache_keys, name="composition_cache_key"),
        cache_hit=pd.Series(cache_hits, name="composition_cache_hit"),
    )

append_composition_fingerprints(data, *, formula_column='composition', drop_formula_column=False, errors='raise', cache_dir=None)

Append inorganic composition descriptors to a dataframe.

The returned dataframe preserves the original index and columns, then adds descriptor columns produced by featurize_compositions.

Source code in matgpr/inorganic_fingerprints.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def append_composition_fingerprints(
    data: pd.DataFrame,
    *,
    formula_column: str = "composition",
    drop_formula_column: bool = False,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> pd.DataFrame:
    """Append inorganic composition descriptors to a dataframe.

    The returned dataframe preserves the original index and columns, then adds
    descriptor columns produced by ``featurize_compositions``.
    """
    if formula_column not in data.columns:
        raise KeyError(f"Formula column '{formula_column}' not found")

    result = featurize_compositions(data[formula_column], errors=errors, cache_dir=cache_dir)
    features = result.features.set_index(data.index)
    base = data.drop(columns=[formula_column]) if drop_formula_column else data.copy()
    return pd.concat([base, features], axis=1)

append_element_fractions(data, *, formula_column='composition', elements=None, drop_formula_column=False, errors='raise', cache_dir=None)

Append element-fraction columns to a dataframe.

Source code in matgpr/inorganic_fingerprints.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
def append_element_fractions(
    data: pd.DataFrame,
    *,
    formula_column: str = "composition",
    elements: Sequence[str] | None = None,
    drop_formula_column: bool = False,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> pd.DataFrame:
    """Append element-fraction columns to a dataframe."""
    if formula_column not in data.columns:
        raise KeyError(f"Formula column '{formula_column}' not found")

    result = featurize_element_fractions(
        data[formula_column],
        elements=elements,
        errors=errors,
        cache_dir=cache_dir,
    )
    features = result.features.set_index(data.index)
    base = data.drop(columns=[formula_column]) if drop_formula_column else data.copy()
    return pd.concat([base, features], axis=1)

Crystal Structure Fingerprints

matgpr.structure_fingerprints

StructureFingerprintResult dataclass

Result returned by structure featurization helpers.

Attributes:

Name Type Description
features DataFrame

Numeric structure descriptors indexed like the input structures.

failed DataFrame

Structures that could not be parsed or featurized. Empty when errors="raise" and no exception was raised.

cache_keys Series | None

Deterministic cache keys for each input row.

cache_hit Series | None

Boolean flags indicating whether each row was loaded from cache.

Source code in matgpr/structure_fingerprints.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@dataclass(frozen=True)
class StructureFingerprintResult:
    """Result returned by structure featurization helpers.

    Attributes
    ----------
    features
        Numeric structure descriptors indexed like the input structures.
    failed
        Structures that could not be parsed or featurized. Empty when
        ``errors="raise"`` and no exception was raised.
    cache_keys
        Deterministic cache keys for each input row.
    cache_hit
        Boolean flags indicating whether each row was loaded from cache.
    """

    features: pd.DataFrame
    failed: pd.DataFrame
    cache_keys: pd.Series | None = None
    cache_hit: pd.Series | None = None

structure_fingerprint(structure, *, features=DEFAULT_STRUCTURE_FEATURES)

Create lightweight global structure descriptors for one crystal structure.

Parameters:

Name Type Description Default
structure object

A pymatgen.core.Structure object, a path to a structure file, or a CIF string.

required
features Sequence[str]

Descriptor names to return. Defaults are global lattice-shape and packing descriptors intended for small tabular GPR workflows.

DEFAULT_STRUCTURE_FEATURES
Source code in matgpr/structure_fingerprints.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def structure_fingerprint(
    structure: object,
    *,
    features: Sequence[str] = DEFAULT_STRUCTURE_FEATURES,
) -> dict[str, float]:
    """Create lightweight global structure descriptors for one crystal structure.

    Parameters
    ----------
    structure
        A ``pymatgen.core.Structure`` object, a path to a structure file, or a
        CIF string.
    features
        Descriptor names to return. Defaults are global lattice-shape and
        packing descriptors intended for small tabular GPR workflows.
    """
    feature_names = _validate_structure_feature_names(features)
    structure_object = _coerce_structure(structure)
    values = _structure_feature_values(structure_object)
    return {name: float(values[name]) for name in feature_names}

featurize_structures(structures, *, features=DEFAULT_STRUCTURE_FEATURES, errors='raise', cache_dir=None)

Featurize a sequence of crystal structures.

Parameters:

Name Type Description Default
structures Iterable[object]

Iterable of pymatgen.Structure objects, structure-file paths, or CIF strings.

required
features Sequence[str]

Descriptor names passed to structure_fingerprint.

DEFAULT_STRUCTURE_FEATURES
errors str

"raise" stops at the first invalid structure. "coerce" returns rows filled with NaN and records failures in failed.

'raise'
cache_dir str | Path | None

Optional directory for persistent row-level JSON cache files.

None
Source code in matgpr/structure_fingerprints.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def featurize_structures(
    structures: Iterable[object],
    *,
    features: Sequence[str] = DEFAULT_STRUCTURE_FEATURES,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> StructureFingerprintResult:
    """Featurize a sequence of crystal structures.

    Parameters
    ----------
    structures
        Iterable of ``pymatgen.Structure`` objects, structure-file paths, or CIF
        strings.
    features
        Descriptor names passed to ``structure_fingerprint``.
    errors
        ``"raise"`` stops at the first invalid structure. ``"coerce"`` returns
        rows filled with ``NaN`` and records failures in ``failed``.
    cache_dir
        Optional directory for persistent row-level JSON cache files.
    """
    if errors not in {"raise", "coerce"}:
        raise ValueError("errors must be either 'raise' or 'coerce'")

    feature_names = _validate_structure_feature_names(features)
    cache_parameters = {"features": list(feature_names)}
    rows: list[Mapping[str, float]] = []
    failures: list[dict[str, object]] = []
    cache_keys: list[str] = []
    cache_hits: list[bool] = []

    for index, structure in enumerate(structures):
        cache_key = fingerprint_cache_key(
            namespace="structure",
            value=_structure_cache_value(structure),
            parameters=cache_parameters,
        )
        cache_keys.append(cache_key)

        try:
            cached_features = _read_cached_structure_row(
                cache_dir,
                cache_key=cache_key,
                feature_names=feature_names,
            )
            if cached_features is not None:
                rows.append(cached_features)
                cache_hits.append(True)
                continue

            row = structure_fingerprint(structure, features=feature_names)
            rows.append(row)
            cache_hits.append(False)
            _write_cached_structure_row(
                cache_dir,
                cache_key=cache_key,
                input_value=_structure_cache_value(structure),
                features=row,
                metadata=cache_parameters,
            )
        except Exception as exc:
            if errors == "raise":
                raise ValueError(f"Could not featurize structure at position {index}") from exc
            failures.append(
                {
                    "index": index,
                    "structure": _short_structure_label(structure),
                    "cache_key": cache_key,
                    "error": str(exc),
                }
            )
            rows.append({name: np.nan for name in feature_names})
            cache_hits.append(False)

    return StructureFingerprintResult(
        features=pd.DataFrame(rows, columns=feature_names),
        failed=pd.DataFrame(failures, columns=["index", "structure", "cache_key", "error"]),
        cache_keys=pd.Series(cache_keys, name="structure_cache_key"),
        cache_hit=pd.Series(cache_hits, name="structure_cache_hit"),
    )

append_structure_fingerprints(data, *, structure_column='structure', features=DEFAULT_STRUCTURE_FEATURES, drop_structure_column=False, errors='raise', cache_dir=None)

Append lightweight structure descriptors to a dataframe.

Source code in matgpr/structure_fingerprints.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def append_structure_fingerprints(
    data: pd.DataFrame,
    *,
    structure_column: str = "structure",
    features: Sequence[str] = DEFAULT_STRUCTURE_FEATURES,
    drop_structure_column: bool = False,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> pd.DataFrame:
    """Append lightweight structure descriptors to a dataframe."""
    if structure_column not in data.columns:
        raise KeyError(f"Structure column '{structure_column}' not found")

    result = featurize_structures(
        data[structure_column],
        features=features,
        errors=errors,
        cache_dir=cache_dir,
    )
    structure_features = result.features.set_index(data.index)
    base = data.drop(columns=[structure_column]) if drop_structure_column else data.copy()
    return pd.concat([base, structure_features], axis=1)

structure_feature_names(features=DEFAULT_STRUCTURE_FEATURES, *, column_prefix=None)

Return validated structure descriptor names with an optional prefix.

Source code in matgpr/structure_fingerprints.py
190
191
192
193
194
195
196
197
198
199
def structure_feature_names(
    features: Sequence[str] = DEFAULT_STRUCTURE_FEATURES,
    *,
    column_prefix: str | None = None,
) -> list[str]:
    """Return validated structure descriptor names with an optional prefix."""
    names = list(_validate_structure_feature_names(features))
    if column_prefix is None:
        return names
    return [f"{column_prefix}_{name}" for name in names]

Organic And Polymer Fingerprints

matgpr.organic_fingerprints

SmilesFingerprintResult dataclass

Result returned by featurize_smiles.

Attributes:

Name Type Description
features DataFrame

Numeric fingerprint or descriptor features.

canonical_smiles Series

Canonical SMILES used for fingerprinting. For polymers, this is the canonicalized cyclic trimer surrogate built from the two [*] ends.

failed DataFrame

Rows that could not be parsed or featurized. Empty when errors="raise" and no exception was raised.

cache_keys Series | None

Deterministic cache keys for each input row.

cache_hit Series | None

Boolean flags indicating whether each row was loaded from cache.

Source code in matgpr/organic_fingerprints.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass(frozen=True)
class SmilesFingerprintResult:
    """Result returned by ``featurize_smiles``.

    Attributes
    ----------
    features
        Numeric fingerprint or descriptor features.
    canonical_smiles
        Canonical SMILES used for fingerprinting. For polymers, this is the
        canonicalized cyclic trimer surrogate built from the two ``[*]`` ends.
    failed
        Rows that could not be parsed or featurized. Empty when
        ``errors="raise"`` and no exception was raised.
    cache_keys
        Deterministic cache keys for each input row.
    cache_hit
        Boolean flags indicating whether each row was loaded from cache.
    """

    features: pd.DataFrame
    canonical_smiles: pd.Series
    failed: pd.DataFrame
    cache_keys: pd.Series | None = None
    cache_hit: pd.Series | None = None

canonicalize_molecule_smiles(smiles)

Canonicalize a molecule SMILES string with RDKit.

Source code in matgpr/organic_fingerprints.py
68
69
70
71
def canonicalize_molecule_smiles(smiles: object) -> str:
    """Canonicalize a molecule SMILES string with RDKit."""
    molecule = _mol_from_smiles(smiles)
    return _chem().MolToSmiles(molecule, canonical=True)

canonicalize_polymer_smiles(smiles, *, repeat_units=3, join_ends=True)

Canonicalize a polymer repeat-unit SMILES string.

By default, polymer repeat units must contain exactly two dummy atoms represented as [*]. The repeat unit is expanded to a trimer, neighboring atoms at adjacent [*] ends are connected using the dummy-end bond order, the trimer is closed into a loop, and the dummy atoms are removed before RDKit canonicalization.

Source code in matgpr/organic_fingerprints.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def canonicalize_polymer_smiles(
    smiles: object,
    *,
    repeat_units: int = 3,
    join_ends: bool = True,
) -> str:
    """Canonicalize a polymer repeat-unit SMILES string.

    By default, polymer repeat units must contain exactly two dummy atoms
    represented as ``[*]``. The repeat unit is expanded to a trimer, neighboring
    atoms at adjacent ``[*]`` ends are connected using the dummy-end bond order,
    the trimer is closed into a loop, and the dummy atoms are removed before
    RDKit canonicalization.
    """
    molecule = _mol_from_smiles(smiles)
    endpoint_info = _polymer_endpoint_info(molecule)

    if join_ends:
        molecule = _make_cyclic_polymer_oligomer(
            molecule,
            endpoint_info=endpoint_info,
            repeat_units=repeat_units,
        )
    return _chem().MolToSmiles(molecule, canonical=True)

fingerprint_smiles(smiles, *, smiles_type='molecule', fingerprint_type='morgan', n_bits=1024, radius=2, descriptors=DEFAULT_RDKIT_DESCRIPTORS)

Fingerprint one molecule or polymer SMILES string.

Parameters:

Name Type Description Default
smiles object

Molecule SMILES, or polymer repeat-unit SMILES containing exactly two [*] dummy atoms when smiles_type="polymer".

required
smiles_type str

"molecule" or "polymer".

'molecule'
fingerprint_type str

"morgan", "rdkit", "maccs", "descriptors", or one of "morgan+descriptors", "rdkit+descriptors", and "maccs+descriptors".

'morgan'
n_bits int

Fingerprint length for Morgan and RDKit fingerprints.

1024
radius int

Morgan fingerprint radius.

2
descriptors Sequence[str]

RDKit descriptor names to include for descriptor-based features.

DEFAULT_RDKIT_DESCRIPTORS
Source code in matgpr/organic_fingerprints.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def fingerprint_smiles(
    smiles: object,
    *,
    smiles_type: str = "molecule",
    fingerprint_type: str = "morgan",
    n_bits: int = 1024,
    radius: int = 2,
    descriptors: Sequence[str] = DEFAULT_RDKIT_DESCRIPTORS,
) -> tuple[np.ndarray, str, list[str]]:
    """Fingerprint one molecule or polymer SMILES string.

    Parameters
    ----------
    smiles
        Molecule SMILES, or polymer repeat-unit SMILES containing exactly two
        ``[*]`` dummy atoms when ``smiles_type="polymer"``.
    smiles_type
        ``"molecule"`` or ``"polymer"``.
    fingerprint_type
        ``"morgan"``, ``"rdkit"``, ``"maccs"``, ``"descriptors"``, or one of
        ``"morgan+descriptors"``, ``"rdkit+descriptors"``, and
        ``"maccs+descriptors"``.
    n_bits
        Fingerprint length for Morgan and RDKit fingerprints.
    radius
        Morgan fingerprint radius.
    descriptors
        RDKit descriptor names to include for descriptor-based features.
    """
    canonical_smiles = _canonicalize_by_type(smiles, smiles_type)
    molecule = _mol_from_smiles(canonical_smiles)
    fingerprint_type = fingerprint_type.lower()

    if "+" in fingerprint_type:
        parts = [part.strip() for part in fingerprint_type.split("+")]
        arrays: list[np.ndarray] = []
        names: list[str] = []
        for part in parts:
            array, part_names = _fingerprint_array(
                molecule,
                fingerprint_type=part,
                n_bits=n_bits,
                radius=radius,
                descriptors=descriptors,
            )
            arrays.append(array)
            names.extend(part_names)
        return np.concatenate(arrays).astype(float), canonical_smiles, names

    array, names = _fingerprint_array(
        molecule,
        fingerprint_type=fingerprint_type,
        n_bits=n_bits,
        radius=radius,
        descriptors=descriptors,
    )
    return array.astype(float), canonical_smiles, names

featurize_smiles(smiles_values, *, smiles_type='molecule', fingerprint_type='morgan', n_bits=1024, radius=2, descriptors=DEFAULT_RDKIT_DESCRIPTORS, column_prefix=None, errors='raise', cache_dir=None)

Featurize a sequence of molecule or polymer SMILES strings.

Source code in matgpr/organic_fingerprints.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def featurize_smiles(
    smiles_values: Iterable[object],
    *,
    smiles_type: str = "molecule",
    fingerprint_type: str = "morgan",
    n_bits: int = 1024,
    radius: int = 2,
    descriptors: Sequence[str] = DEFAULT_RDKIT_DESCRIPTORS,
    column_prefix: str | None = None,
    errors: str = "raise",
    cache_dir: str | Path | None = None,
) -> SmilesFingerprintResult:
    """Featurize a sequence of molecule or polymer SMILES strings."""
    if errors not in {"raise", "coerce"}:
        raise ValueError("errors must be either 'raise' or 'coerce'")

    prefix = column_prefix or _default_prefix(smiles_type, fingerprint_type)
    feature_names: list[str] | None = None
    rows: list[np.ndarray] = []
    canonical_smiles: list[str | float] = []
    failures: list[dict[str, object]] = []
    cache_keys: list[str] = []
    cache_hits: list[bool] = []
    cache_parameters = {
        "smiles_type": smiles_type.lower(),
        "fingerprint_type": fingerprint_type.lower(),
        "n_bits": n_bits,
        "radius": radius,
        "descriptors": list(descriptors),
        "polymer_repeat_units": 3,
        "polymer_join_ends": True,
    }

    for index, smiles in enumerate(smiles_values):
        normalized_smiles = _smiles_cache_value(smiles)
        cache_key = fingerprint_cache_key(
            namespace="smiles",
            value=normalized_smiles,
            parameters=cache_parameters,
        )
        cache_keys.append(cache_key)

        try:
            if feature_names is None:
                feature_names = _feature_names(
                    fingerprint_type=fingerprint_type,
                    n_bits=n_bits,
                    descriptors=descriptors,
                    prefix=prefix,
                )

            cached_row = _read_cached_smiles_row(
                cache_dir,
                namespace="smiles",
                cache_key=cache_key,
                n_features=len(feature_names),
            )
            if cached_row is not None:
                rows.append(cached_row["array"])
                canonical_smiles.append(cached_row["canonical_smiles"])
                cache_hits.append(True)
                continue

            array, canonical, names = fingerprint_smiles(
                smiles,
                smiles_type=smiles_type,
                fingerprint_type=fingerprint_type,
                n_bits=n_bits,
                radius=radius,
                descriptors=descriptors,
            )
            generated_feature_names = [f"{prefix}_{name}" for name in names]
            if feature_names != generated_feature_names:
                feature_names = generated_feature_names
            rows.append(array)
            canonical_smiles.append(canonical)
            cache_hits.append(False)
            _write_cached_smiles_row(
                cache_dir,
                namespace="smiles",
                cache_key=cache_key,
                input_value=normalized_smiles,
                array=array,
                canonical_smiles=canonical,
                metadata=cache_parameters,
            )
        except Exception as exc:
            if errors == "raise":
                raise ValueError(
                    f"Could not featurize SMILES at position {index}: {smiles!r}"
                ) from exc
            if feature_names is None:
                feature_names = _feature_names(
                    fingerprint_type=fingerprint_type,
                    n_bits=n_bits,
                    descriptors=descriptors,
                    prefix=prefix,
                )
            rows.append(np.full(len(feature_names), np.nan, dtype=float))
            canonical_smiles.append(np.nan)
            cache_hits.append(False)
            failures.append(
                {
                    "index": index,
                    "smiles": smiles,
                    "cache_key": cache_key,
                    "smiles_type": smiles_type,
                    "fingerprint_type": fingerprint_type,
                    "error": str(exc),
                }
            )

    if feature_names is None:
        feature_names = _feature_names(
            fingerprint_type=fingerprint_type,
            n_bits=n_bits,
            descriptors=descriptors,
            prefix=prefix,
        )

    return SmilesFingerprintResult(
        features=pd.DataFrame(rows, columns=feature_names),
        canonical_smiles=pd.Series(canonical_smiles, name=f"{prefix}_canonical_smiles"),
        failed=pd.DataFrame(
            failures,
            columns=["index", "smiles", "cache_key", "smiles_type", "fingerprint_type", "error"],
        ),
        cache_keys=pd.Series(cache_keys, name=f"{prefix}_cache_key"),
        cache_hit=pd.Series(cache_hits, name=f"{prefix}_cache_hit"),
    )

append_smiles_features(data, *, smiles_column, smiles_type='molecule', fingerprint_type='morgan', n_bits=1024, radius=2, descriptors=DEFAULT_RDKIT_DESCRIPTORS, column_prefix=None, errors='raise', include_canonical_smiles=True, cache_dir=None)

Append RDKit features for a SMILES column to a dataframe.

Source code in matgpr/organic_fingerprints.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
def append_smiles_features(
    data: pd.DataFrame,
    *,
    smiles_column: str,
    smiles_type: str = "molecule",
    fingerprint_type: str = "morgan",
    n_bits: int = 1024,
    radius: int = 2,
    descriptors: Sequence[str] = DEFAULT_RDKIT_DESCRIPTORS,
    column_prefix: str | None = None,
    errors: str = "raise",
    include_canonical_smiles: bool = True,
    cache_dir: str | Path | None = None,
) -> pd.DataFrame:
    """Append RDKit features for a SMILES column to a dataframe."""
    if smiles_column not in data.columns:
        raise KeyError(f"SMILES column '{smiles_column}' not found")

    result = featurize_smiles(
        data[smiles_column],
        smiles_type=smiles_type,
        fingerprint_type=fingerprint_type,
        n_bits=n_bits,
        radius=radius,
        descriptors=descriptors,
        column_prefix=column_prefix,
        errors=errors,
        cache_dir=cache_dir,
    )
    features = result.features.set_index(data.index)
    output = data.copy()
    if include_canonical_smiles:
        output[result.canonical_smiles.name] = result.canonical_smiles.set_axis(data.index)
    return pd.concat([output, features], axis=1)

Fingerprint Cache

matgpr.fingerprint_cache

fingerprint_cache_key(*, namespace, value, parameters)

Return a deterministic cache key for one fingerprint input.

The key is based on a versioned JSON payload containing the fingerprint namespace, the input value, and all featurization parameters. The same input and settings produce the same SHA-256 hash across Python sessions.

Source code in matgpr/fingerprint_cache.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def fingerprint_cache_key(
    *,
    namespace: str,
    value: object,
    parameters: Mapping[str, object],
) -> str:
    """Return a deterministic cache key for one fingerprint input.

    The key is based on a versioned JSON payload containing the fingerprint
    namespace, the input value, and all featurization parameters. The same
    input and settings produce the same SHA-256 hash across Python sessions.
    """
    payload = {
        "schema_version": FINGERPRINT_CACHE_SCHEMA_VERSION,
        "namespace": namespace,
        "value": _json_ready(value),
        "parameters": _json_ready(parameters),
    }
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False)
    return hashlib.sha256(encoded.encode("utf-8")).hexdigest()

fingerprint_cache_path(cache_dir, *, namespace, cache_key)

Return the JSON file path for a cache key.

Source code in matgpr/fingerprint_cache.py
47
48
49
50
51
52
53
54
def fingerprint_cache_path(
    cache_dir: str | Path,
    *,
    namespace: str,
    cache_key: str,
) -> Path:
    """Return the JSON file path for a cache key."""
    return Path(cache_dir) / _safe_namespace(namespace) / f"{cache_key}.json"

read_fingerprint_cache_record(cache_dir, *, namespace, cache_key)

Read a cached fingerprint record, returning None for misses.

Source code in matgpr/fingerprint_cache.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def read_fingerprint_cache_record(
    cache_dir: str | Path | None,
    *,
    namespace: str,
    cache_key: str,
) -> dict[str, Any] | None:
    """Read a cached fingerprint record, returning ``None`` for misses."""
    if cache_dir is None:
        return None

    path = fingerprint_cache_path(cache_dir, namespace=namespace, cache_key=cache_key)
    if not path.exists():
        return None

    try:
        with path.open("r", encoding="utf-8") as handle:
            record = json.load(handle)
    except (OSError, json.JSONDecodeError):
        return None

    if not isinstance(record, dict):
        return None
    if record.get("schema_version") != FINGERPRINT_CACHE_SCHEMA_VERSION:
        return None
    if record.get("namespace") != namespace:
        return None
    if record.get("cache_key") != cache_key:
        return None
    return record

write_fingerprint_cache_record(cache_dir, *, namespace, cache_key, record)

Write one fingerprint cache record as JSON.

Writes are atomic within the cache directory: data are first written to a temporary sibling file, then moved into place.

Source code in matgpr/fingerprint_cache.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def write_fingerprint_cache_record(
    cache_dir: str | Path | None,
    *,
    namespace: str,
    cache_key: str,
    record: Mapping[str, object],
) -> None:
    """Write one fingerprint cache record as JSON.

    Writes are atomic within the cache directory: data are first written to a
    temporary sibling file, then moved into place.
    """
    if cache_dir is None:
        return

    path = fingerprint_cache_path(cache_dir, namespace=namespace, cache_key=cache_key)
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = {
        "schema_version": FINGERPRINT_CACHE_SCHEMA_VERSION,
        "namespace": namespace,
        "cache_key": cache_key,
        **dict(record),
    }
    temporary_path = path.with_suffix(f".{os.getpid()}.tmp")
    with temporary_path.open("w", encoding="utf-8") as handle:
        json.dump(
            _json_ready(payload), handle, sort_keys=True, separators=(",", ":"), allow_nan=False
        )
    temporary_path.replace(path)