Source code for ionize.Solution

from __future__ import division
import copy
from collections import OrderedDict
import contextlib
import numpy as np

from ..Ion import BaseIon
from ..Solvent import Aqueous
from ..Database import Database
from ..serialize import _serialize
from ..constants import reference_temperature

database = Database()


[docs]class Solution(object): """Represent an aqueous Solution. Solutions automatically compute their equilibrium state, and store it as pH and ionic_strength. :param ions: An iterable of ion objects, or strings. Each string is converted to an ion object using the ionize database. If ion objects are used, new copies are stored inside the Solution. Each ion inside the Solution has the Solution as their context by default. However, the original version of the ion retains its original context. :param concentrations: An iterable of concentrations, in moles per liter. Example: ``sol = ionize.Solution(['chloride', 'tris'], [0.02, 0.05]) sol.pH, sol.ionic_strength 'tris' in sol # True sol.concentration('chloride') # 0.02 with sol.temperature(35): sol.conductivity() sol.serialize() sol['tris'] # Ion('tris')`` """ _solvent = Aqueous # TODO: Create a _solvent_components member. _hydronium = None _hydroxide = None _pH = 7. # Normal pH units. _ionic_strength = 0. # Expected in molar. _temperature = reference_temperature # Temperature in C # Ions and concentrations fully represent state. # class uses _contents internally to enforce consistency # and help with lookups _state = ('ions', 'concentrations') _contents = OrderedDict() @property def _name_lookup(self): name_lookup = dict() for ion in self.ions: name_lookup[ion.name] = ion try: for alias in ion.alias: name_lookup[alias] = ion except (AttributeError, TypeError): pass name_lookup['H+'] = self._hydronium name_lookup['OH-'] = self._hydroxide name_lookup['hydronium'] = self._hydronium name_lookup['hydroxide'] = self._hydroxide return name_lookup @property def ions(self): """Return a tuple of the ions in the solution.""" return tuple(self._contents.keys()) @property def concentrations(self): """Return a numpy array of the ion concentrations in the solution.""" return np.array(list(self._contents.values())) @property def pH(self): """The pH of the solution.""" return self._pH @property def ionic_strength(self): """The ionic strength of the solution.""" return self._ionic_strength def __init__(self, ions=[], concentrations=[]): """Initialize a solution object.""" if isinstance(ions, (str, BaseIon)): ions = (ions,) try: len(concentrations) except: concentrations = (concentrations,) assert len(ions) == len(concentrations), \ 'There must be an ion for each concentration.' self._contents = OrderedDict() for ion, concentration in zip(ions, concentrations): if isinstance(ion, str): ion = database.load(ion) else: ion = copy.copy(ion) ion.context(self) if concentration == 0: continue elif concentration <0: raise ValueError('Concentrations must be positive.') else: self._contents[ion] = concentration self._hydronium = database['hydronium'] self._hydroxide = database['hydroxide'] self._hydronium.context(self) self._hydroxide.context(self) for solvent_component in self._hydroxide, self._hydronium: assert solvent_component not in self.ions, \ "Solvent components cannot be manually added to Solution." self._equilibrate()
[docs] def temperature(self, temperature=None): """Set or get the temperature of the solution. :param temperature: The new temperature. If temperature is None, return the current temperature. Otherwise, sets the temperature and returns a context manager that reverts to the original temperature. """ if temperature is None: return self._temperature else: old_temperature = self._temperature if temperature != old_temperature: self._temperature = float(temperature) self._equilibrate() @contextlib.contextmanager def manage_temperature(): yield self.temperature(old_temperature) return manage_temperature()
def _cH(self): """Return the concentration of protons in solution.""" cH = 10**(-self.pH)/self._solvent.activity(1, self.ionic_strength, self.temperature()) return cH def _cOH(self): """Return the concentration of hydroxyls in solution.""" cOH = (self._solvent.dissociation(self.ionic_strength, self.temperature()) / self._cH() / self._solvent.activity(1, self.ionic_strength, self.temperature()) ** 2.) return cOH
[docs] def concentration(self, ion): """Return the concentration of the ion. The input may be an ion object or an ion name as a string. """ if ion in ('H+', self._hydronium): return self._cH() elif ion in ('OH-', self._hydroxide): return self._cOH() else: ion = self._name_lookup.get(ion, ion) return self._contents.get(ion, 0)
def __add__(self, other): if isinstance(other, Solution): ions = list(set(self.ions + other.ions)) return Solution(ions, [self.concentration(ion) + other.concentration(ion) for ion in ions] ) else: try: ion, concentration = other new_contents = dict(self._contents) new_contents[ion] = self.concentration(ion) + concentration return Solution(new_contents.keys(), new_contents.values()) except: raise TypeError('Solutions add to other Solutions or to an' '(Ion, concentration) iterable pair.') __radd__ = __add__ def __sub__(self, other): if isinstance(other, Solution): ions = list(set(self.ions + other.ions)) return Solution(ions, [self.concentration(ion) - other.concentration(ion) for ion in ions] ) else: try: ion, concentration = other new_contents = dict(self._contents) new_contents[ion] = self.concentration(ion) - concentration if new_contents[ion]==0: del new_contents[ion] return Solution(new_contents.keys(), new_contents.values()) except: raise TypeError('Solutions add to other Solutions or to an' '(Ion, concentration) iterable pair.') def __mul__(self, other): if other >= 0: return Solution(self.ions, [c * other for c in self.concentrations]) else: raise TypeError __rmul__ = __mul__ def __truediv__(self, other): if other > 0: return Solution(self.ions, [c / other for c in self.concentrations]) else: raise TypeError def __str__(self): """Return a string representing the Solution.""" return "Solution(pH={:.3g}, I={:.3g} M)".format(self.pH, self.ionic_strength) def __repr__(self): """Return a representation of the Solution.""" template = "Solution(ions={}, concentrations={})" return template.format([ion.name for ion in self.ions], self.concentrations.tolist()) def __len__(self): return len(self.ions) def __eq__(self, other): try: return self.serialize() == other.serialize() except: return False def __hash__(self): return hash(self.serialize()) def __contains__(self, other): return other in self.ions or other in self._name_lookup.keys() def __iter__(self): return (ion for ion in self.ions) def __getitem__(self, item): try: if hasattr(item, 'name'): return self._name_lookup[item.name] else: return self._name_lookup[item] except: raise KeyError(item)
[docs] def safe(self): """Return True if the solution has a safe pH. We define moderate pH as the regime in which the conductivity contributed by H+ and OH- is less than 10% the sum of charge contributed by other ions. """ diss = sum([self.concentration(ion) * self[ion].molar_conductivity() for ion in ('H+', 'OH-')]) other = sum([self.concentration(ion) * ion.molar_conductivity() for ion in self.ions]) return 10. * diss < other
[docs] def moderate(self): """Return True if the solution has a moderate pH. We define moderate pH as the regime in which the sum of charge contributed by H+ and OH- is less than 10% the sum of charge contributed by other ions. """ diss = sum([abs(self.concentration(ion) * self[ion].charge()) for ion in ('H+', 'OH-')]) other = sum([abs(self.concentration(ion) * ion.charge()) for ion in self.ions]) return 10. * diss < other
[docs] def serialize(self, nested=False, compact=False): """Return a JSON-formatted serialization of the object.""" serial = {'__solution__': True} serial['concentrations'] = self.concentrations serial['ions'] = self.ions return _serialize(serial, nested, compact)
[docs] def save(self, filename): """Save the solution to file.""" with open(filename, 'w') as file: file.write(self.serialize())
from .equilibrium import _equilibrate from .conductivity import conductivity, hydroxide_conductivity, \ hydronium_conductivity from .titrate import titrate, buffering_capacity, \ equilibrate_CO2, displace from .debye import debye from .transference import transference, zone_transfer from .conservation import kohlrausch, alberty, jovin, gas