"""Solvent database for media concentration corrections.
Solvent molecular weights [g/mol] and densities [g/mL] at 20 °C, loaded from
solvents.json alongside this module. Each solvent entry defines a canonical name
and one or more lookup aliases (abbreviations, full names).
Public API:
solvents -- dict mapping alias (lowercase str) -> (mw, density) tuple
"""
import json
import os
def _load_solvents():
"""
Load the bundled solvents.json and build a flat mapping from each lowercase alias to its (molecular weight, density) tuple.
The JSON file is read from the same directory as this module. Each solvent entry's `aliases` list is expanded and normalized to lowercase.
Returns:
dict: Mapping where keys are lowercase alias strings and values are `(mw, density)` tuples — `mw` in g/mol and `density` in g/mL.
"""
json_path = os.path.join(os.path.dirname(__file__), 'solvents.json')
with open(json_path, 'r') as f:
data = json.load(f)
result = {}
for entry in data['solvents']:
value = (entry['mw'], entry['density'])
for alias in entry['aliases']:
result[alias.lower()] = value
return result
solvents = _load_solvents()
[docs]
def lookup_solvent(name):
"""Look up a solvent by alias and return its (mw, density).
Raises ValueError with a "did you mean ..." hint and a list of common
aliases if `name` is unknown. Use this to validate user input (e.g.
--media / --freespace) up-front; `compute_media_conc` and other call
sites can then assume the alias is valid.
"""
key = name.lower()
if key not in solvents:
import difflib
suggestions = difflib.get_close_matches(key, solvents.keys(), n=5, cutoff=0.5)
common = ['water', 'methanol', 'ethanol', 'acetone', 'dmso', 'dmf',
'thf', 'benzene', 'toluene', 'chloroform', 'dcm', 'acetonitrile']
msg = f"Unknown solvent {name!r}."
if suggestions:
msg += f" Did you mean: {', '.join(suggestions)}?"
msg += (f" Common aliases: {', '.join(common)} "
f"({len(solvents)} total in goodvibes/solvents.json).")
raise ValueError(msg)
return solvents[key]