Units¶
The short answer¶
mg/L, per species, and stated rather than assumed.
Every loader returns mg/L, and a table you build yourself defaults to mg/L for every species. The mixing algebra does not care — it is linear within each species, so mixed units across columns are mathematically harmless. The conversions care, because they read the label to decide what to do.
Units are per species¶
A real analysis sheet mixes units routinely: concentrations in mg/L beside conductivity in µS/cm and a delta value in permil. One table-level label can only describe such a table by lying about part of it, so Mescla tracks a unit for each species.
import mescla as ms
# One string applies to everything.
samples = ms.WaterChemistry(data, species=["Ca", "Mg", "Cl"], units="mg/L")
# A mapping names only the exceptions; anything unlisted is mg/L.
samples = ms.WaterChemistry(
data, species=["Cl", "EC", "d18O"], units={"EC": "uS/cm", "d18O": "permil"}
)
samples.units_dict() # {'Cl': 'mg/L', 'EC': 'uS/cm', 'd18O': 'permil'}
samples.unit_of("EC") # 'uS/cm'
samples.common_unit # None -- the table mixes units
samples.is_homogeneous # False
with_units relabels without converting. Use it when a loader guessed wrong and the
numbers were always in the unit you are now stating:
samples = samples.with_units({"EC": "uS/cm"}) # says what the numbers are
The bundled Besòs end-members are the real case: twenty species in mg/L and one, EC, in
µS/cm. load_tubau_besos() declares that, so nothing downstream has to guess.
Converting¶
from mescla.prep.units import to_meq_per_l, to_mmol_per_l
Both convert only the species that are not already in the target unit, and relabel those species individually. A species that cannot be converted stops the call:
>>> to_meq_per_l(ms.WaterChemistry([[40.078, 500.0]], species=["Ca", "EC"]))
ValueError: cannot convert to meq/L:
'EC': not a recognised ion
These species carry no charge or are not in the ion table, so there is no factor to
apply. Choose deliberately:
on_unknown='skip' keep them, unconverted, with their own unit
on_unknown='drop' remove them from the returned table
.select_species(...) convert only the ions
Inspect the factors with mescla.prep.units.conversion_factors(table).
Refusing is deliberate. The alternative — passing the column through untouched while relabelling the table — returns something that looks converted and is not, which is the failure mode worth preventing. Choose the policy explicitly:
|
what happens |
when |
|---|---|---|
|
refuses, naming the species |
you expected everything to convert |
|
keeps them with their own unit |
the table legitimately holds EC, silica or isotopes |
|
removes those columns |
you want a purely ionic table |
"skip" is honest precisely because units are per species: the result says Ca is meq/L
and EC is still µS/cm.
>>> to_meq_per_l(samples, on_unknown="skip").units_dict()
{'Ca': 'meq/L', 'EC': 'uS/cm'}
charge_balance_report uses "skip" internally, because it deliberately considers only
the major ions.
Which species convert, and by what factor¶
Nothing is hidden. conversion_factors() returns exactly the table
the conversions read:
>>> from mescla.prep.units import conversion_factors
>>> conversion_factors(["Ca", "SO4", "EC"])
matched_ion molar_mass charge equivalent_weight factor target_unit convertible
species
Ca Ca 40.078 2.0 20.039 0.049903 meq/L True
SO4 SO4 96.060 -2.0 48.030 0.020820 meq/L True
EC NaN NaN NaN NaN NaN NaN False
Pass a table to describe its own species, and to="mmol/L" for the molar factors:
conversion_factors(samples) # what would happen to this table
conversion_factors(samples, to="mmol/L")
Read it whenever a converted number surprises you, or to check that a species was
recognised under the name you used. Matching ignores case and charge notation, so "Ca",
"Ca2+" and "calcium" are the same ion; ION_TABLE is the full
list.
meq/L divides by the equivalent weight (molar mass ÷ |charge|), so a neutral species has none — silica and dissolved oxygen cannot convert, and the error says so.
mmol/L divides by the molar mass, which exists for neutral species too, so more species convert.
Where the unit label actually matters¶
operation |
depends on the label? |
|---|---|
|
no — linear within each species |
|
yes — decides what is already converted |
|
yes — they convert internally |
So a mislabelled table will not corrupt a mixing ratio, but it will corrupt a charge balance. If you load data that is already in meq/L, say so:
samples = ms.WaterChemistry(data, species=[...], units="meq/L")
Isotopes¶
linearize() replaces a solute isotope column with δ × C, and marks
that column as linearised (permil x mg/L) while the others keep their own unit — see
08 · Isotope tracers, and the one that breaks linear mixing.