"""Solids given by their formula as participants of a drawn reaction (#142).
A text on the canvas that is a formula marked as a solid, ``C(s)``,
``Fe3−xCrxO4(s)`` or ``γ-Al2O3``, takes part in reactions as a molecule does:
on a side, joined by plus signs, with a coefficient before it, or at the
arrow as a reagent such as a catalyst. Derivation reads it as a species with
a :class:`~vibesketch.document.model.SolidFormula` record and no molecular
graph. Its molar mass takes part in stoichiometry and its atoms in the
balance check (:mod:`vibesketch.document.electrochem`) when every count is a
number; nothing is calculated for it.
"""
from __future__ import annotations
import dataclasses
from typing import Any
from vibesketch.chem.formula import Formula, is_solid_text
from vibesketch.document.ket import KetFragment, KetScene
from vibesketch.document.model import Species
__all__ = ["solid_data", "solid_refusal", "solid_runs", "with_solids"]
#: A solid's text box for reading the drawing: about a bond length per three characters.
_CHAR, _HEIGHT = 0.3, 0.5
[docs]
def with_solids(scene: KetScene) -> tuple[KetScene, dict[int, Formula]]:
"""``scene`` with each solid's text as a fragment of its own, and their formulas.
The texts leave the scene's texts, so they are read as participants, not
as conditions or annotations; the fragments come after the molecules.
"""
fragments = list(scene.fragments)
texts = []
solids: dict[int, Formula] = {}
for index, text in enumerate(scene.texts):
formula = is_solid_text(text.text)
if formula is None:
texts.append(text)
continue
x, y = text.centre
half = _CHAR * len(text.text.strip()) / 2
solids[len(fragments)] = formula
fragments.append(
KetFragment(
None,
f"text:{index}",
(x, y),
(x - half, y - _HEIGHT / 2, x + half, y + _HEIGHT / 2),
)
)
if not solids:
return scene, {}
return dataclasses.replace(scene, fragments=fragments, texts=texts), solids
[docs]
def solid_data(formula: Formula) -> dict[str, Any]:
"""The species' ``solid`` record for a formula."""
data: dict[str, Any] = {
"text": formula.text,
"formula": formula.canonical(),
"key": formula.key(),
"composition": formula.composition,
}
if formula.polymorph:
data["polymorph"] = formula.polymorph
if formula.state:
data["state"] = formula.state
mass = formula.molar_mass()
if mass is not None:
data["molar_mass_g_mol"] = round(mass, 4)
return data
[docs]
def solid_refusal(writer: str, species: Species) -> list[str]:
"""Why a calculation cannot take a solid given by its formula."""
if species.solid is None:
return []
return [
f"{writer} cannot calculate species {species.id}, the solid {species.solid.text}: it is "
"given by its formula and needs its structure, which crystal and slab models will give "
"(roadmap Phase 5)"
]
[docs]
def solid_runs(text: str) -> list[tuple[str, bool]] | None:
"""A solid's text as runs of (text, subscript), counts lowered; ``None`` for other text."""
import re
from vibesketch.chem.formula import _COUNT
if is_solid_text(text) is None:
return None
runs: list[tuple[str, bool]] = []
def put(part: str, low: bool) -> None:
if runs and runs[-1][1] == low:
runs[-1] = (runs[-1][0] + part, low)
else:
runs.append((part, low))
body = text.strip()
state = re.search(r"\(\s*(s|l|g|aq|ads)\s*\)\s*$", body)
tail = body[state.start() :] if state else ""
body = body[: state.start()] if state else body
prefix = re.match(r"^([A-Za-zα-ω]+)\s*[-‐]\s*", body)
if prefix and not prefix.group(1)[0].isupper():
put(body[: prefix.end()], False)
body = body[prefix.end() :]
index = 0
after_unit = False # a count follows an element or a closing bracket
while index < len(body):
char = body[index]
if after_unit and (match := _COUNT.match(body, index)) is not None and match.group(0):
put(match.group(0), True)
index = match.end()
after_unit = False
continue
put(char, False)
after_unit = char.isalpha() or char in ")]"
if char.isupper() and index + 1 < len(body) and body[index + 1].islower():
# A two-letter symbol: its second letter, unless it is a count variable.
nxt = body[index + 1]
if nxt not in "xyz" or body[index : index + 2] in ("Xe",):
put(nxt, False)
index += 1
index += 1
if tail:
put(tail, False)
return runs