tooluniverse.degrees_of_unsaturation_tool 源代码

"""
Degrees of Unsaturation Calculator Tool

Calculate DoU from a molecular formula string or explicit atom counts.
DoU = (2C + 2 + N - H - X) / 2  where X = total halogens.

No external API calls. Pure Python with stdlib re only.
"""

import re
from typing import Any, Dict
from .base_tool import BaseTool
from .tool_registry import register_tool


_INTERPRETATION = [
    (0, 0, "No rings or pi bonds — fully saturated, open-chain compound."),
    (1, 1, "One ring OR one double bond (e.g. cyclopropane, alkene, ketone)."),
    (2, 2, "Two degrees: ring + double bond, two double bonds, or one triple bond."),
    (3, 3, "Three degrees: ring + two double bonds or a diene in a ring."),
    (4, 4, "Four degrees — consistent with a benzene ring (3 C=C + 1 ring)."),
]


def _parse_formula(formula: str) -> Dict[str, int]:
    """Parse molecular formula string into element counts."""
    tokens = re.findall(r"([A-Z][a-z]?)(\d*)", formula)
    counts: Dict[str, int] = {}
    for element, count_str in tokens:
        if not element:
            continue
        count = int(count_str) if count_str else 1
        counts[element] = counts.get(element, 0) + count
    return counts


def _interpret(dou: float) -> str:
    for lo, hi, msg in _INTERPRETATION:
        if lo <= dou <= hi:
            return msg
    if dou > 4:
        return f"{dou:.0f} degrees — likely contains multiple rings and/or aromatic systems."
    return "Negative DoU — check your formula."


[文档] @register_tool("DegreesOfUnsaturationTool") class DegreesOfUnsaturationTool(BaseTool): """Calculate degrees of unsaturation from a molecular formula."""
[文档] def __init__(self, tool_config: Dict[str, Any]): super().__init__(tool_config) self.parameter = tool_config.get("parameter", {}) self.required = self.parameter.get("required", [])
[文档] def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]: operation = arguments.get("operation", "calculate") if operation != "calculate": return { "status": "error", "error": f"Unknown operation: {operation}. Only 'calculate' is supported.", } try: return self._calculate(arguments) except Exception as e: return {"status": "error", "error": f"Calculation failed: {str(e)}"}
[文档] def _calculate(self, arguments: Dict[str, Any]) -> Dict[str, Any]: formula = arguments.get("formula") if formula: counts = _parse_formula(formula) C = counts.get("C", 0) H = counts.get("H", 0) N = counts.get("N", 0) n_O = counts.get("O", 0) S = counts.get("S", 0) F = counts.get("F", 0) Cl = counts.get("Cl", 0) Br = counts.get("Br", 0) n_I = counts.get("I", 0) else: C = arguments.get("C", 0) H = arguments.get("H", 0) N = arguments.get("N", 0) n_O = arguments.get("oxygen", arguments.get("O", 0)) S = arguments.get("S", 0) F = arguments.get("F", 0) Cl = arguments.get("Cl", 0) Br = arguments.get("Br", 0) n_I = arguments.get("iodine", arguments.get("I", 0)) if C <= 0: return {"status": "error", "error": "Number of carbon atoms must be > 0."} halogens = F + Cl + Br + n_I dou = (2 * C + 2 + N - H - halogens) / 2 # Build display formula parts = [] for sym, cnt in [ ("C", C), ("H", H), ("N", N), ("O", n_O), ("S", S), ("F", F), ("Cl", Cl), ("Br", Br), ("I", n_I), ]: if cnt > 0: parts.append(f"{sym}{cnt}" if cnt > 1 else sym) formula_display = "".join(parts) return { "status": "success", "data": { "formula": formula_display, "C": C, "H": H, "N": N, "O": n_O, "S": S, "halogens": halogens, "degrees_of_unsaturation": dou, "calculation": f"(2*{C} + 2 + {N} - {H} - {halogens}) / 2 = {dou}", "interpretation": _interpret(dou), "is_integer": dou == int(dou), }, }