diff --git a/CLAUDE.md b/CLAUDE.md
index 6053010e..dceb7dc1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -110,7 +110,7 @@ These are now baked into the generator and enforced by tests. **Do not reopen wi
2. **Dimensionless and angular quantities have both `Ratio` (V0) and `SignedRatio` (V1) bases.** Ratios that semantically must be non-negative (e.g. `RefractiveIndex`, `MachNumber`, `SpecificGravity`) are V0 overloads of `Ratio`.
3. **Semantic overloads widen implicitly to their base, narrow explicitly from it.** A `Weight` is implicitly a `ForceMagnitude`; the reverse requires `Weight.From(forceMagnitude)` or an explicit cast.
4. **Physical constraints are enforced structurally via the V0 (magnitude) form.** `Vector0` factories run `Vector0Guards.EnsureNonNegative` and throw `ArgumentException` on a negative value. That covers absolute zero (Temperature is V0, so Kelvin must be ≥ 0), non-negative frequency, non-negative absolute pressure, etc. A V0 *overload* can opt into a stricter rule by declaring `physicalConstraints: { "minExclusive": "0" }` in `dimensions.json` (#51); the generator then emits `Vector0Guards.EnsurePositive` and rejects zero too. Used today for `Wavelength`, `Period`, and `HalfLife` — quantities for which zero is unphysical.
-5. **Logarithmic-scale quantities are generated from `logarithmic.json`, not declared as dimensions.** Decibel scales (`Decibels`, `SoundPressureLevel`, `SoundIntensityLevel`, `SoundPowerLevel`, `DirectionalityIndex`), pitch intervals (`Cents`, `Semitones`), and `PH` don't obey linear arithmetic, so they are emitted by `LogarithmicScalesGenerator` as standalone `readonly partial record struct`s built around `scale = multiplier · log_base(linear / reference)`, converting to and from their linear generated counterparts (`Gain`, `Ratio`, `SoundPressure`, `SoundIntensity`, `SoundPower`, `Concentration`). Bespoke members (named constants like `PH.Neutral`, cross-scale conversions like `Cents`↔`Semitones`) live in hand-written partials next to the metadata-generated core. Adding a new log-scale quantity means adding a `logarithmic.json` entry, plus a partial only if it needs bespoke members.
+5. **Logarithmic-scale quantities are generated from `logarithmic.json`, not declared as dimensions.** Decibel scales (`Decibels`, `SoundPressureLevel`, `SoundIntensityLevel`, `SoundPowerLevel`, `DirectionalityIndex`), pitch intervals (`Cents`, `Semitones`), and `PH`/`POH` don't obey linear arithmetic, so they are emitted by `LogarithmicScalesGenerator` as standalone `readonly partial record struct`s built around `scale = multiplier · log_base(linear / reference)`, converting to and from their linear generated counterparts (`Gain`, `Ratio`, `SoundPressure`, `SoundIntensity`, `SoundPower`, `Concentration`). Bespoke members (named constants like `PH.Neutral`, cross-scale conversions like `Cents`↔`Semitones` and `PH`↔`POH`) live in hand-written partials next to the metadata-generated core. Adding a new log-scale quantity means adding a `logarithmic.json` entry, plus a partial only if it needs bespoke members.
### The C++ projection
diff --git a/Semantics.Quantities.Decimal/build/ktsu.Semantics.Quantities.Decimal.props b/Semantics.Quantities.Decimal/build/ktsu.Semantics.Quantities.Decimal.props
index 053b32a6..a0afbb69 100644
--- a/Semantics.Quantities.Decimal/build/ktsu.Semantics.Quantities.Decimal.props
+++ b/Semantics.Quantities.Decimal/build/ktsu.Semantics.Quantities.Decimal.props
@@ -145,6 +145,7 @@
+
diff --git a/Semantics.Quantities.Double/build/ktsu.Semantics.Quantities.Double.props b/Semantics.Quantities.Double/build/ktsu.Semantics.Quantities.Double.props
index f07b188e..20e3ea89 100644
--- a/Semantics.Quantities.Double/build/ktsu.Semantics.Quantities.Double.props
+++ b/Semantics.Quantities.Double/build/ktsu.Semantics.Quantities.Double.props
@@ -145,6 +145,7 @@
+
diff --git a/Semantics.Quantities.Float/build/ktsu.Semantics.Quantities.Float.props b/Semantics.Quantities.Float/build/ktsu.Semantics.Quantities.Float.props
index 66ab02e0..ef2a1ce4 100644
--- a/Semantics.Quantities.Float/build/ktsu.Semantics.Quantities.Float.props
+++ b/Semantics.Quantities.Float/build/ktsu.Semantics.Quantities.Float.props
@@ -145,6 +145,7 @@
+
diff --git a/Semantics.Quantities.Precise/build/ktsu.Semantics.Quantities.Precise.props b/Semantics.Quantities.Precise/build/ktsu.Semantics.Quantities.Precise.props
index 13a8a9d0..6b846d60 100644
--- a/Semantics.Quantities.Precise/build/ktsu.Semantics.Quantities.Precise.props
+++ b/Semantics.Quantities.Precise/build/ktsu.Semantics.Quantities.Precise.props
@@ -145,6 +145,7 @@
+
diff --git a/Semantics.Quantities/Chemistry/PH.cs b/Semantics.Quantities/Chemistry/PH.cs
index bf8332cb..1cf41525 100644
--- a/Semantics.Quantities/Chemistry/PH.cs
+++ b/Semantics.Quantities/Chemistry/PH.cs
@@ -17,8 +17,8 @@ public readonly partial record struct PH
///
/// Gets the complementary pOH at 25 °C using pOH = 14 − pH.
///
- /// The pOH as a scale value.
- public PH ToPOH() => new(T.CreateChecked(14) - Value);
+ /// The pOH as a scale value.
+ public POH ToPOH() => POH.Create(T.CreateChecked(14) - Value);
/// Gets whether this value describes an acid (pH below 7).
public bool IsAcidic => Value < PhysicalConstants.Generic.NeutralPH();
diff --git a/Semantics.Quantities/Chemistry/POH.cs b/Semantics.Quantities/Chemistry/POH.cs
new file mode 100644
index 00000000..b5a13398
--- /dev/null
+++ b/Semantics.Quantities/Chemistry/POH.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Quantities;
+
+using System.Numerics;
+
+///
+/// Bespoke members of ; the logarithmic core (including the
+/// conversions) is generated from logarithmic.json.
+///
+public readonly partial record struct POH
+ where T : struct, INumber
+{
+ ///
+ /// Gets the complementary pH at 25 °C using pH = 14 − pOH.
+ ///
+ /// The pH as a scale value.
+ public PH ToPH() => PH.Create(T.CreateChecked(14) - Value);
+}
diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.LogarithmicScalesGenerator/POH.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.LogarithmicScalesGenerator/POH.g.cs
new file mode 100644
index 00000000..15f56c62
--- /dev/null
+++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.LogarithmicScalesGenerator/POH.g.cs
@@ -0,0 +1,109 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+//
+
+#nullable enable
+
+namespace ktsu.Semantics.Quantities;
+
+using System;
+using System.Globalization;
+using System.Numerics;
+
+///
+/// Represents basicity on the pOH scale: pOH = −log10([OH⁻]) with the hydroxide-ion activity in mol/L.
+///
+///
+/// pOH is a logarithmic scale over the linear Concentration quantity (stored in the SI base mol/m³; 1 mol/L = 1000 mol/m³).
+/// Logarithmic scales don't obey linear arithmetic, so this type is generated as a
+/// standalone companion (from logarithmic.json) rather than a physical dimension.
+///
+/// Precision: the conversions to and from the linear counterparts compute in
+/// whatever is, so a value carried in a
+/// storage type wider than — , or
+/// PreciseNumber — is accurate to about fifteen significant digits once it has been
+/// through one. Arithmetic, comparison and the raw factory keep every digit
+/// holds; only the logarithm and its inverse do not.
+///
+///
+/// The floating-point storage type.
+/// The scale value.
+public readonly partial record struct POH(T Value) : IComparable>
+ where T : struct, INumber
+{
+ ///
+ /// Creates a value from the raw scale number.
+ ///
+ /// The raw scale value.
+ /// A new .
+ public static POH Create(T value) => new(value);
+
+ ///
+ /// Creates a pOH from a hydroxide-ion concentration using pOH = −log10([OH⁻] in mol/L).
+ ///
+ /// The linear .
+ /// A new . A linear value of zero maps to negative infinity where can hold it.
+ ///
+ /// Computes in whatever is: the value is
+ /// converted to , the logarithm or power is taken there, and the
+ /// result is converted back. A storage type wider than —
+ /// , or PreciseNumber — therefore keeps about fifteen
+ /// significant digits across this conversion, rather than the precision it is capable of.
+ ///
+ /// cannot hold the result, as for a linear value of zero with or integer storage.
+ public static POH FromHydroxideConcentration(Concentration linear)
+ {
+ double linearValue = double.CreateChecked(linear.Value);
+ double reference = 1000.0;
+ return new(LogarithmicStorage.FromLinear(-1.0 * Math.Log10(linearValue / reference), nameof(linear)));
+ }
+
+ ///
+ /// Converts this pOH to the equivalent hydroxide-ion concentration using [OH⁻] = 10^(−pOH) mol/L.
+ ///
+ /// The linear .
+ ///
+ /// Computes in whatever is: the value is
+ /// converted to , the logarithm or power is taken there, and the
+ /// result is converted back. A storage type wider than —
+ /// , or PreciseNumber — therefore keeps about fifteen
+ /// significant digits across this conversion, rather than the precision it is capable of.
+ ///
+ /// cannot hold the linear value of a level this high.
+ public Concentration ToHydroxideConcentration()
+ {
+ double scaleValue = double.CreateChecked(Value);
+ double reference = 1000.0;
+ return Concentration.Create(LogarithmicStorage.ToLinear(reference * Math.Pow(10.0, scaleValue / -1.0)));
+ }
+
+ ///
+ public int CompareTo(POH other) => Value.CompareTo(other.Value);
+
+ /// Determines whether one value is less than another.
+ /// The left value.
+ /// The right value.
+ /// if is less than .
+ public static bool operator <(POH left, POH right) => left.CompareTo(right) < 0;
+
+ /// Determines whether one value is greater than another.
+ /// The left value.
+ /// The right value.
+ /// if is greater than .
+ public static bool operator >(POH left, POH right) => left.CompareTo(right) > 0;
+
+ /// Determines whether one value is less than or equal to another.
+ /// The left value.
+ /// The right value.
+ /// if is less than or equal to .
+ public static bool operator <=(POH left, POH right) => left.CompareTo(right) <= 0;
+
+ /// Determines whether one value is greater than or equal to another.
+ /// The left value.
+ /// The right value.
+ /// if is greater than or equal to .
+ public static bool operator >=(POH left, POH right) => left.CompareTo(right) >= 0;
+
+ /// Returns a culture-invariant string representation of this value.
+ /// The formatted value.
+ public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"pOH {Value}");
+}
diff --git a/Semantics.SourceGenerators/Metadata/logarithmic.json b/Semantics.SourceGenerators/Metadata/logarithmic.json
index 75fa21c9..5ef95f60 100644
--- a/Semantics.SourceGenerators/Metadata/logarithmic.json
+++ b/Semantics.SourceGenerators/Metadata/logarithmic.json
@@ -151,6 +151,25 @@
"toSummary": "Converts this pH to the equivalent hydrogen-ion concentration using [H⁺] = 10^(−pH) mol/L."
}
]
+ },
+ {
+ "name": "POH",
+ "description": "Represents basicity on the pOH scale: pOH = −log10([OH⁻]) with the hydroxide-ion activity in mol/L.",
+ "remarks": "pOH is a logarithmic scale over the linear Concentration quantity (stored in the SI base mol/m³; 1 mol/L = 1000 mol/m³).",
+ "displayFormat": "pOH {0}",
+ "scalarFactory": "Create",
+ "arithmetic": false,
+ "conversions": [
+ {
+ "linear": "Concentration",
+ "multiplier": "-1",
+ "reference": { "value": "1000" },
+ "fromName": "FromHydroxideConcentration",
+ "toName": "ToHydroxideConcentration",
+ "fromSummary": "Creates a pOH from a hydroxide-ion concentration using pOH = −log10([OH⁻] in mol/L).",
+ "toSummary": "Converts this pOH to the equivalent hydroxide-ion concentration using [OH⁻] = 10^(−pOH) mol/L."
+ }
+ ]
}
]
}
diff --git a/Semantics.Test/Quantities/QuantityBackfillTests.cs b/Semantics.Test/Quantities/QuantityBackfillTests.cs
index f7475cf2..ba7a52e1 100644
--- a/Semantics.Test/Quantities/QuantityBackfillTests.cs
+++ b/Semantics.Test/Quantities/QuantityBackfillTests.cs
@@ -177,6 +177,27 @@ public void PH_Acidity_Classification_And_POH()
Assert.IsFalse(PH.Neutral.IsAcidic);
}
+ [TestMethod]
+ public void POH_Of_An_Acid_Is_A_POH_And_Converts_Back()
+ {
+ // #482: ToPOH returned a PH, so pH 3 reported its pOH as a basic "pH 11".
+ POH poh = PH.Create(3.0).ToPOH();
+ Assert.AreEqual(11.0, poh.Value, Tolerance);
+ Assert.StartsWith("pOH ", poh.ToString());
+ Assert.AreEqual(Concentration.FromMolar(1e-11).Value, poh.ToHydroxideConcentration().Value, 1e-20);
+ Assert.AreEqual(3.0, poh.ToPH().Value, Tolerance);
+ Assert.IsTrue(poh.ToPH().IsAcidic);
+ }
+
+ [TestMethod]
+ public void POH_From_Hydroxide_Concentration_And_Back()
+ {
+ POH poh = POH.FromHydroxideConcentration(Concentration.FromMolar(1e-4));
+ Assert.AreEqual(4.0, poh.Value, 1e-9);
+ Assert.AreEqual(10.0, poh.ToPH().Value, 1e-9);
+ Assert.AreEqual(Concentration.FromMolar(1e-4).Value, poh.ToHydroxideConcentration().Value, 1e-12);
+ }
+
[TestMethod]
public void Levels_Compare_And_Add_In_Decibel_Space()
{
diff --git a/docs/migration-guide-6.0.md b/docs/migration-guide-6.0.md
index 30a8186f..c041bddf 100644
--- a/docs/migration-guide-6.0.md
+++ b/docs/migration-guide-6.0.md
@@ -1,5 +1,29 @@
# Migrating from Semantics 5.x to 6.0
+## `PH.ToPOH()` returns a `POH`
+
+`ToPOH()` used to return its pOH inside a `PH`, so every pH member read it as a pH: the pOH
+of an acid reported `IsBasic == true`, printed as `pH 11`, and `ToHydrogenConcentration()`
+returned the hydroxide-ion concentration instead (#482).
+
+It now returns the new `POH` scale, generated from `logarithmic.json` like `PH`. `POH`
+has no `IsAcidic`, `IsBasic` or pH formatting; it prints as `pOH {0}`, converts to and from the
+hydroxide-ion `Concentration`, and `ToPH()` gives the complementary pH at 25 °C.
+
+```csharp
+// 5.x
+PH poh = PH.Create(3).ToPOH(); // "pH 11", IsBasic == true
+
+// 6.0
+POH poh = PH.Create(3).ToPOH(); // "pOH 11"
+PH ph = poh.ToPH(); // pH 3
+Concentration oh = poh.ToHydroxideConcentration(); // 1e-11 mol/L
+```
+
+Code that only read `.Value` keeps compiling if it uses `var`, and the number is unchanged.
+Code that declared the result as `PH` gets `CS0029`; declare it as `POH`, or call
+`.ToPH()` if a pH was what it wanted.
+
## A directory's `Name` is a `DirectoryName`
`AbsoluteDirectoryPath.Name` and `DirectoryPath.Name` used to return a `FileName`, while