Source code for vibesketch.document.solids

"""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