xtalate 0.1.0.dev0__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.
- xtalate/__init__.py +10 -0
- xtalate/capabilities/__init__.py +19 -0
- xtalate/capabilities/registry.py +126 -0
- xtalate/cli/__init__.py +11 -0
- xtalate/cli/main.py +431 -0
- xtalate/cli/render.py +126 -0
- xtalate/conversion/__init__.py +48 -0
- xtalate/conversion/engine.py +560 -0
- xtalate/conversion/preflight.py +127 -0
- xtalate/conversion/report.py +74 -0
- xtalate/discovery/__init__.py +28 -0
- xtalate/discovery/engine.py +202 -0
- xtalate/discovery/report.py +50 -0
- xtalate/discovery/sniffer.py +95 -0
- xtalate/exporters/__init__.py +34 -0
- xtalate/exporters/extxyz.py +140 -0
- xtalate/exporters/poscar.py +180 -0
- xtalate/exporters/xyz.py +67 -0
- xtalate/parsers/__init__.py +31 -0
- xtalate/parsers/_common.py +57 -0
- xtalate/parsers/extxyz.py +452 -0
- xtalate/parsers/poscar.py +428 -0
- xtalate/parsers/xyz.py +286 -0
- xtalate/py.typed +0 -0
- xtalate/recovery/__init__.py +41 -0
- xtalate/recovery/engine.py +321 -0
- xtalate/recovery/scenarios.py +86 -0
- xtalate/registry.py +28 -0
- xtalate/schema/__init__.py +40 -0
- xtalate/schema/arrays.py +115 -0
- xtalate/schema/elements.py +150 -0
- xtalate/schema/models.py +283 -0
- xtalate/schema/paths.py +88 -0
- xtalate/schema/presence.py +152 -0
- xtalate/sdk/__init__.py +27 -0
- xtalate/sdk/capabilities.py +57 -0
- xtalate/sdk/plugins.py +72 -0
- xtalate/sdk/results.py +45 -0
- xtalate/validation/__init__.py +25 -0
- xtalate/validation/engine.py +585 -0
- xtalate/validation/report.py +48 -0
- xtalate/validation/rethreshold.py +122 -0
- xtalate/validation/tolerance.py +125 -0
- xtalate-0.1.0.dev0.dist-info/METADATA +161 -0
- xtalate-0.1.0.dev0.dist-info/RECORD +49 -0
- xtalate-0.1.0.dev0.dist-info/WHEEL +4 -0
- xtalate-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- xtalate-0.1.0.dev0.dist-info/licenses/LICENSE +201 -0
- xtalate-0.1.0.dev0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Recovery scenario catalog: hazard classes and per-scenario option computation (Part 4 §3).
|
|
2
|
+
|
|
3
|
+
Every conversion hazard the pre-flight diff detects (Part 3 §4.3) falls into exactly one of
|
|
4
|
+
three hazard classes (Part 4 §3.1), and the class governs what the Recovery Engine may do:
|
|
5
|
+
|
|
6
|
+
* **bulk reductive** — information the target cannot carry is dropped wholesale; no scientific
|
|
7
|
+
choice selects *which* survives. Never a recovery scenario: it is reported as ``removed`` and
|
|
8
|
+
(in permissive mode) proceeds without a choice.
|
|
9
|
+
* **selective reductive** — data is dropped but *which subset is kept changes the scientific
|
|
10
|
+
meaning*; Xtalate will not choose unrecorded. Requires an explicit choice, records an
|
|
11
|
+
``Assumption``, produces **no** ``supplied`` entry (the kept data is genuine source data).
|
|
12
|
+
* **fabricative** — information required/offered by the target does not exist in the source; it
|
|
13
|
+
must be *created*. Requires an explicit choice, records an ``Assumption`` **and** a ``supplied``
|
|
14
|
+
entry. Never automatic in any mode (**P4**).
|
|
15
|
+
|
|
16
|
+
The bright line is deliberately discrete (a three-value enum, not a risk scale) so no mode or
|
|
17
|
+
future convenience can make Xtalate invent scientific data or silently choose which real data
|
|
18
|
+
to discard (Part 4 §3.1, "Alternative rejected: a single risk-level scale").
|
|
19
|
+
|
|
20
|
+
**v0.1 scope (review §4.4 trim).** Two scenarios resolve: ``missing_lattice`` (fabricative) and
|
|
21
|
+
``frame_selection`` (selective reductive), preset-only. Option lists are **computed**, not static
|
|
22
|
+
(Part 4 §3.3): a choice that is not scientifically coherent for the concrete source/target pair,
|
|
23
|
+
or not implemented in v0.1, is honestly absent from ``available_options`` rather than offered and
|
|
24
|
+
then refused. ``missing_species``/``missing_velocities``/``missing_masses``/``missing_energy``/
|
|
25
|
+
``truncate_corrupt_tail``/``constraint_representation`` are catalogued in the spec but not
|
|
26
|
+
triggered by any v0.1 (source, target) pair; they attach at this same seam later (**P6**).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from enum import StrEnum
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class UnresolvedScenario:
|
|
37
|
+
"""A recovery the pre-flight diff detected but has not resolved (Part 4 §3).
|
|
38
|
+
|
|
39
|
+
Emitted by ``conversion.preflight`` (conversion → recovery is a legal downward import) and
|
|
40
|
+
consumed by ``RecoveryEngine.resolve``; it lives here, in the recovery layer, so both sides
|
|
41
|
+
share one descriptor. ``path`` is the canonical field a *fabricative* scenario would supply
|
|
42
|
+
(``None`` for a selective-reductive one like ``frame_selection``)."""
|
|
43
|
+
|
|
44
|
+
scenario: str # e.g. "missing_lattice" | "frame_selection".
|
|
45
|
+
path: str | None = None
|
|
46
|
+
detail: str | None = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HazardClass(StrEnum):
|
|
50
|
+
"""The three-way classification of Part 4 §3.1. Discrete by design."""
|
|
51
|
+
|
|
52
|
+
BULK_REDUCTIVE = "bulk_reductive"
|
|
53
|
+
SELECTIVE_REDUCTIVE = "selective_reductive"
|
|
54
|
+
FABRICATIVE = "fabricative"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# The v0.1 resolvable scenarios and their hazard class. A scenario absent here is not one the
|
|
58
|
+
# Recovery Engine resolves in v0.1 (it either does not arise for the implemented formats, or is a
|
|
59
|
+
# bulk-reductive drop handled by the mode table, not by a choice).
|
|
60
|
+
SCENARIO_HAZARD: dict[str, HazardClass] = {
|
|
61
|
+
"frame_selection": HazardClass.SELECTIVE_REDUCTIVE,
|
|
62
|
+
"missing_lattice": HazardClass.FABRICATIVE,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def available_options(scenario: str, *, target_can_be_nonperiodic: bool = False) -> list[str]:
|
|
67
|
+
"""The *computed* option list for ``scenario`` (Part 4 §3.3), honestly excluding choices not
|
|
68
|
+
offered for this pair or not implemented in v0.1.
|
|
69
|
+
|
|
70
|
+
``missing_lattice``: ``manual_input`` (user supplies a 3×3 lattice) and ``bounding_box``
|
|
71
|
+
(axis-aligned box of the selected frame's positions + ``padding_ang``). ``non_periodic`` is
|
|
72
|
+
offered only when the target can express ``pbc=(F,F,F)`` — never for POSCAR, whose ``cell.pbc``
|
|
73
|
+
is PARTIAL/fully-periodic-only (Part 3 §4.2). ``upload_reference`` (lattice from a second file)
|
|
74
|
+
needs a second input stream and is out of v0.1 scope, so it is honestly not listed.
|
|
75
|
+
|
|
76
|
+
``frame_selection``: ``first`` / ``last`` / ``index``. ``split_all`` (one file per frame)
|
|
77
|
+
needs a multi-file output mode absent from the v0.1 library API, so it is not listed.
|
|
78
|
+
"""
|
|
79
|
+
if scenario == "missing_lattice":
|
|
80
|
+
options = ["manual_input", "bounding_box"]
|
|
81
|
+
if target_can_be_nonperiodic:
|
|
82
|
+
options.append("non_periodic")
|
|
83
|
+
return options
|
|
84
|
+
if scenario == "frame_selection":
|
|
85
|
+
return ["first", "last", "index"]
|
|
86
|
+
return []
|
xtalate/registry.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""``default_registry()`` — the standard first-party registry (MASTER_SPEC Part 3 §7.1).
|
|
2
|
+
|
|
3
|
+
A convenience factory that registers every built-in parser and exporter into one
|
|
4
|
+
:class:`~xtalate.capabilities.Registry`, so callers (the CLI, examples, and any embedder)
|
|
5
|
+
get the full v0.1 format set without hand-wiring it. This is the "explicit first-party
|
|
6
|
+
registration list" the spec calls sufficient before entry-point plugin discovery exists
|
|
7
|
+
(Part 3 §7.1); third-party plugins attach at the same registry later (**P6**).
|
|
8
|
+
|
|
9
|
+
Lives at the package root, not in ``capabilities``: the registry *machinery* must not depend on
|
|
10
|
+
the concrete parsers/exporters (that would invert the import graph), so the wiring that pulls
|
|
11
|
+
them together sits above both — the same reason the CLI, not the engine, composes the pipeline.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from xtalate.capabilities import Registry
|
|
17
|
+
from xtalate.exporters import builtin_exporters
|
|
18
|
+
from xtalate.parsers import builtin_parsers
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_registry() -> Registry:
|
|
22
|
+
"""A fresh :class:`Registry` with all built-in parsers and exporters registered."""
|
|
23
|
+
registry = Registry()
|
|
24
|
+
for parser in builtin_parsers():
|
|
25
|
+
registry.register_parser(parser)
|
|
26
|
+
for exporter in builtin_exporters():
|
|
27
|
+
registry.register_exporter(exporter)
|
|
28
|
+
return registry
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Canonical Model — the single in-memory/serialized representation (MASTER_SPEC Part 2).
|
|
2
|
+
|
|
3
|
+
Depends on nothing else in the package (lowest layer of the P2 dependency graph, Part 1
|
|
4
|
+
§5.1). Implemented in M1.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from xtalate.schema.models import (
|
|
8
|
+
SCHEMA_VERSION,
|
|
9
|
+
AtomsBlock,
|
|
10
|
+
CanonicalObject,
|
|
11
|
+
Cell,
|
|
12
|
+
Constraint,
|
|
13
|
+
ConversionRecord,
|
|
14
|
+
Dynamics,
|
|
15
|
+
Electronic,
|
|
16
|
+
Frame,
|
|
17
|
+
Provenance,
|
|
18
|
+
SimulationMetadata,
|
|
19
|
+
TrajectoryMetadata,
|
|
20
|
+
UserMetadata,
|
|
21
|
+
)
|
|
22
|
+
from xtalate.schema.presence import PathPresence, PresenceMap
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SCHEMA_VERSION",
|
|
26
|
+
"AtomsBlock",
|
|
27
|
+
"CanonicalObject",
|
|
28
|
+
"Cell",
|
|
29
|
+
"Constraint",
|
|
30
|
+
"ConversionRecord",
|
|
31
|
+
"Dynamics",
|
|
32
|
+
"Electronic",
|
|
33
|
+
"Frame",
|
|
34
|
+
"PathPresence",
|
|
35
|
+
"PresenceMap",
|
|
36
|
+
"Provenance",
|
|
37
|
+
"SimulationMetadata",
|
|
38
|
+
"TrajectoryMetadata",
|
|
39
|
+
"UserMetadata",
|
|
40
|
+
]
|
xtalate/schema/arrays.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""NumPy array field types for the Canonical Model (MASTER_SPEC Part 2 §1, §3).
|
|
2
|
+
|
|
3
|
+
Numeric array fields (``positions``, ``lattice_vectors``, ``velocities``, ...) are
|
|
4
|
+
``np.ndarray`` of ``float64`` in memory — for zero-copy interop with the array-native
|
|
5
|
+
scientific libraries (ASE) the pipeline delegates to — and serialize to **nested JSON
|
|
6
|
+
lists**. This module provides the pydantic v2 custom types that enforce that contract.
|
|
7
|
+
|
|
8
|
+
Design (DECISIONS.md D8):
|
|
9
|
+
|
|
10
|
+
* In memory: ``np.ndarray``, dtype ``float64``, always a fresh copy (inputs are never
|
|
11
|
+
aliased, so a caller mutating the array it passed in cannot corrupt the model).
|
|
12
|
+
* Serialized (JSON mode): nested lists via ``ndarray.tolist()`` — Python's default
|
|
13
|
+
shortest-round-tripping float ``repr``, making ``float64 -> JSON -> float64`` lossless.
|
|
14
|
+
* Golden equality is defined as *deserialize-then-compare* (``np.array_equal`` on the
|
|
15
|
+
parsed values), never a comparison of serialized text — see the round-trip tests.
|
|
16
|
+
|
|
17
|
+
Each type validates only its **local** shape (rank and any fixed axis, e.g. the trailing
|
|
18
|
+
``3`` of an ``(N, 3)`` array). Cross-field agreement — that ``positions``,
|
|
19
|
+
``velocities`` and ``forces`` share one ``N``, that ``N`` is constant across frames — is a
|
|
20
|
+
*model*-level invariant enforced by validators on the models (§3.3, §3.2), not here: a
|
|
21
|
+
lone array cannot know the object's atom count.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Annotated, Any
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from numpy.typing import NDArray
|
|
31
|
+
from pydantic import GetCoreSchemaHandler
|
|
32
|
+
from pydantic_core import core_schema
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _ArraySpec:
|
|
37
|
+
"""pydantic-v2 metadata object describing a ``float64`` ndarray field.
|
|
38
|
+
|
|
39
|
+
Placed inside ``Annotated[...]``; pydantic calls ``__get_pydantic_core_schema__``
|
|
40
|
+
to obtain the validate/serialize schema for the annotated field.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Exact rank, or None for "any rank >= 1" (the custom-array case, Part 2 §3.10,
|
|
44
|
+
# whose trailing shape is arbitrary and whose first-axis == N/F is a model check).
|
|
45
|
+
ndim: int | None
|
|
46
|
+
# (axis_index, required_size) pairs, e.g. (1, 3) for the trailing 3 of an (N, 3) array.
|
|
47
|
+
fixed_axes: tuple[tuple[int, int], ...] = ()
|
|
48
|
+
|
|
49
|
+
def _shape_label(self) -> str:
|
|
50
|
+
if self.ndim is None:
|
|
51
|
+
return "(first-axis, ...)"
|
|
52
|
+
dims = ["?"] * self.ndim
|
|
53
|
+
for axis, size in self.fixed_axes:
|
|
54
|
+
dims[axis] = str(size)
|
|
55
|
+
return "(" + ", ".join(dims) + ")"
|
|
56
|
+
|
|
57
|
+
def _validate(self, value: Any) -> NDArray[np.float64]:
|
|
58
|
+
if isinstance(value, np.ndarray) and value.dtype == np.float64:
|
|
59
|
+
# Copy so a later mutation of the caller's array cannot reach into the model.
|
|
60
|
+
arr = value.copy()
|
|
61
|
+
else:
|
|
62
|
+
try:
|
|
63
|
+
arr = np.asarray(value, dtype=np.float64)
|
|
64
|
+
except (ValueError, TypeError) as exc:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"expected a real-valued array coercible to float64, got {value!r}"
|
|
67
|
+
) from exc
|
|
68
|
+
if self.ndim is None:
|
|
69
|
+
if arr.ndim < 1:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"expected an array of rank >= 1 {self._shape_label()}, got a 0-D array"
|
|
72
|
+
)
|
|
73
|
+
elif arr.ndim != self.ndim:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"expected a {self.ndim}-D array of shape {self._shape_label()}, "
|
|
76
|
+
f"got a {arr.ndim}-D array of shape {arr.shape}"
|
|
77
|
+
)
|
|
78
|
+
for axis, size in self.fixed_axes:
|
|
79
|
+
if arr.shape[axis] != size:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"expected axis {axis} of length {size} "
|
|
82
|
+
f"(shape {self._shape_label()}), got shape {arr.shape}"
|
|
83
|
+
)
|
|
84
|
+
if not np.all(np.isfinite(arr)):
|
|
85
|
+
raise ValueError("array contains non-finite values (nan/inf); refusing")
|
|
86
|
+
return arr
|
|
87
|
+
|
|
88
|
+
def __get_pydantic_core_schema__(
|
|
89
|
+
self, source_type: Any, handler: GetCoreSchemaHandler
|
|
90
|
+
) -> core_schema.CoreSchema:
|
|
91
|
+
return core_schema.no_info_plain_validator_function(
|
|
92
|
+
self._validate,
|
|
93
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
94
|
+
lambda arr: arr.tolist(),
|
|
95
|
+
when_used="json",
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- Concrete field types, named after their canonical shapes (Part 2 §3). -----------
|
|
101
|
+
|
|
102
|
+
#: ``(N, 3)`` — per-atom Cartesian triples: positions, velocities, forces.
|
|
103
|
+
ArrayN3 = Annotated[NDArray[np.float64], _ArraySpec(ndim=2, fixed_axes=((1, 3),))]
|
|
104
|
+
|
|
105
|
+
#: ``(N,)`` — per-atom scalars: masses, charges, magnetic moments.
|
|
106
|
+
ArrayN = Annotated[NDArray[np.float64], _ArraySpec(ndim=1)]
|
|
107
|
+
|
|
108
|
+
#: ``(3, 3)`` — lattice vectors (rows a, b, c) and the stress tensor.
|
|
109
|
+
Array33 = Annotated[NDArray[np.float64], _ArraySpec(ndim=2, fixed_axes=((0, 3), (1, 3)))]
|
|
110
|
+
|
|
111
|
+
#: First dimension ``N`` (per-atom), any trailing shape — ``user_metadata.custom_per_atom``.
|
|
112
|
+
ArrayNx = Annotated[NDArray[np.float64], _ArraySpec(ndim=None)]
|
|
113
|
+
|
|
114
|
+
#: First dimension ``F`` (per-frame), any trailing shape — ``user_metadata.custom_per_frame``.
|
|
115
|
+
ArrayFx = Annotated[NDArray[np.float64], _ArraySpec(ndim=None)]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Chemical element symbol <-> atomic number table (MASTER_SPEC Part 2 §3.3).
|
|
2
|
+
|
|
3
|
+
``AtomsBlock`` carries both ``symbols`` and ``atomic_numbers``; the numbers are derived
|
|
4
|
+
from the symbols at construction (§3.3) and validated for agreement. This module is the
|
|
5
|
+
single source of that mapping. The reserved pseudo-element ``"X"`` (unknown species,
|
|
6
|
+
atomic number 0) is valid, but a parser emitting it must accompany it with a warning
|
|
7
|
+
(§3.3) — that policy lives in the parsers, not here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Symbols indexed by atomic number: SYMBOLS[Z] == symbol. Index 0 is the reserved
|
|
13
|
+
# unknown-species marker "X" (§3.3). Covers Z = 1..118 (all IUPAC-named elements).
|
|
14
|
+
_SYMBOLS: tuple[str, ...] = (
|
|
15
|
+
"X",
|
|
16
|
+
"H",
|
|
17
|
+
"He",
|
|
18
|
+
"Li",
|
|
19
|
+
"Be",
|
|
20
|
+
"B",
|
|
21
|
+
"C",
|
|
22
|
+
"N",
|
|
23
|
+
"O",
|
|
24
|
+
"F",
|
|
25
|
+
"Ne",
|
|
26
|
+
"Na",
|
|
27
|
+
"Mg",
|
|
28
|
+
"Al",
|
|
29
|
+
"Si",
|
|
30
|
+
"P",
|
|
31
|
+
"S",
|
|
32
|
+
"Cl",
|
|
33
|
+
"Ar",
|
|
34
|
+
"K",
|
|
35
|
+
"Ca",
|
|
36
|
+
"Sc",
|
|
37
|
+
"Ti",
|
|
38
|
+
"V",
|
|
39
|
+
"Cr",
|
|
40
|
+
"Mn",
|
|
41
|
+
"Fe",
|
|
42
|
+
"Co",
|
|
43
|
+
"Ni",
|
|
44
|
+
"Cu",
|
|
45
|
+
"Zn",
|
|
46
|
+
"Ga",
|
|
47
|
+
"Ge",
|
|
48
|
+
"As",
|
|
49
|
+
"Se",
|
|
50
|
+
"Br",
|
|
51
|
+
"Kr",
|
|
52
|
+
"Rb",
|
|
53
|
+
"Sr",
|
|
54
|
+
"Y",
|
|
55
|
+
"Zr",
|
|
56
|
+
"Nb",
|
|
57
|
+
"Mo",
|
|
58
|
+
"Tc",
|
|
59
|
+
"Ru",
|
|
60
|
+
"Rh",
|
|
61
|
+
"Pd",
|
|
62
|
+
"Ag",
|
|
63
|
+
"Cd",
|
|
64
|
+
"In",
|
|
65
|
+
"Sn",
|
|
66
|
+
"Sb",
|
|
67
|
+
"Te",
|
|
68
|
+
"I",
|
|
69
|
+
"Xe",
|
|
70
|
+
"Cs",
|
|
71
|
+
"Ba",
|
|
72
|
+
"La",
|
|
73
|
+
"Ce",
|
|
74
|
+
"Pr",
|
|
75
|
+
"Nd",
|
|
76
|
+
"Pm",
|
|
77
|
+
"Sm",
|
|
78
|
+
"Eu",
|
|
79
|
+
"Gd",
|
|
80
|
+
"Tb",
|
|
81
|
+
"Dy",
|
|
82
|
+
"Ho",
|
|
83
|
+
"Er",
|
|
84
|
+
"Tm",
|
|
85
|
+
"Yb",
|
|
86
|
+
"Lu",
|
|
87
|
+
"Hf",
|
|
88
|
+
"Ta",
|
|
89
|
+
"W",
|
|
90
|
+
"Re",
|
|
91
|
+
"Os",
|
|
92
|
+
"Ir",
|
|
93
|
+
"Pt",
|
|
94
|
+
"Au",
|
|
95
|
+
"Hg",
|
|
96
|
+
"Tl",
|
|
97
|
+
"Pb",
|
|
98
|
+
"Bi",
|
|
99
|
+
"Po",
|
|
100
|
+
"At",
|
|
101
|
+
"Rn",
|
|
102
|
+
"Fr",
|
|
103
|
+
"Ra",
|
|
104
|
+
"Ac",
|
|
105
|
+
"Th",
|
|
106
|
+
"Pa",
|
|
107
|
+
"U",
|
|
108
|
+
"Np",
|
|
109
|
+
"Pu",
|
|
110
|
+
"Am",
|
|
111
|
+
"Cm",
|
|
112
|
+
"Bk",
|
|
113
|
+
"Cf",
|
|
114
|
+
"Es",
|
|
115
|
+
"Fm",
|
|
116
|
+
"Md",
|
|
117
|
+
"No",
|
|
118
|
+
"Lr",
|
|
119
|
+
"Rf",
|
|
120
|
+
"Db",
|
|
121
|
+
"Sg",
|
|
122
|
+
"Bh",
|
|
123
|
+
"Hs",
|
|
124
|
+
"Mt",
|
|
125
|
+
"Ds",
|
|
126
|
+
"Rg",
|
|
127
|
+
"Cn",
|
|
128
|
+
"Nh",
|
|
129
|
+
"Fl",
|
|
130
|
+
"Mc",
|
|
131
|
+
"Lv",
|
|
132
|
+
"Ts",
|
|
133
|
+
"Og",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
#: symbol -> atomic number (Z). "X" maps to 0.
|
|
137
|
+
SYMBOL_TO_Z: dict[str, int] = {sym: z for z, sym in enumerate(_SYMBOLS)}
|
|
138
|
+
|
|
139
|
+
#: The reserved symbol for an unidentified species (§3.3).
|
|
140
|
+
UNKNOWN_SYMBOL = "X"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def is_valid_symbol(symbol: str) -> bool:
|
|
144
|
+
"""True if ``symbol`` is an element symbol or the reserved ``"X"`` (§3.3)."""
|
|
145
|
+
return symbol in SYMBOL_TO_Z
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def atomic_number(symbol: str) -> int:
|
|
149
|
+
"""Atomic number for ``symbol``. Raises ``KeyError`` for an unknown symbol."""
|
|
150
|
+
return SYMBOL_TO_Z[symbol]
|