Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Using Include="ktsu.Semantics.Quantities.Phase&lt;decimal&gt;" Alias="Phase" />
<Using Include="ktsu.Semantics.Quantities.PhaseVelocity&lt;decimal&gt;" Alias="PhaseVelocity" />
<Using Include="ktsu.Semantics.Quantities.Pitch&lt;decimal&gt;" Alias="Pitch" />
<Using Include="ktsu.Semantics.Quantities.POH&lt;decimal&gt;" Alias="POH" />
<Using Include="ktsu.Semantics.Quantities.Position3D&lt;decimal&gt;" Alias="Position3D" />
<Using Include="ktsu.Semantics.Quantities.PotentialEnergy&lt;decimal&gt;" Alias="PotentialEnergy" />
<Using Include="ktsu.Semantics.Quantities.Power&lt;decimal&gt;" Alias="Power" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Using Include="ktsu.Semantics.Quantities.Phase&lt;double&gt;" Alias="Phase" />
<Using Include="ktsu.Semantics.Quantities.PhaseVelocity&lt;double&gt;" Alias="PhaseVelocity" />
<Using Include="ktsu.Semantics.Quantities.Pitch&lt;double&gt;" Alias="Pitch" />
<Using Include="ktsu.Semantics.Quantities.POH&lt;double&gt;" Alias="POH" />
<Using Include="ktsu.Semantics.Quantities.Position3D&lt;double&gt;" Alias="Position3D" />
<Using Include="ktsu.Semantics.Quantities.PotentialEnergy&lt;double&gt;" Alias="PotentialEnergy" />
<Using Include="ktsu.Semantics.Quantities.Power&lt;double&gt;" Alias="Power" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Using Include="ktsu.Semantics.Quantities.Phase&lt;float&gt;" Alias="Phase" />
<Using Include="ktsu.Semantics.Quantities.PhaseVelocity&lt;float&gt;" Alias="PhaseVelocity" />
<Using Include="ktsu.Semantics.Quantities.Pitch&lt;float&gt;" Alias="Pitch" />
<Using Include="ktsu.Semantics.Quantities.POH&lt;float&gt;" Alias="POH" />
<Using Include="ktsu.Semantics.Quantities.Position3D&lt;float&gt;" Alias="Position3D" />
<Using Include="ktsu.Semantics.Quantities.PotentialEnergy&lt;float&gt;" Alias="PotentialEnergy" />
<Using Include="ktsu.Semantics.Quantities.Power&lt;float&gt;" Alias="Power" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Using Include="ktsu.Semantics.Quantities.Phase&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="Phase" />
<Using Include="ktsu.Semantics.Quantities.PhaseVelocity&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="PhaseVelocity" />
<Using Include="ktsu.Semantics.Quantities.Pitch&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="Pitch" />
<Using Include="ktsu.Semantics.Quantities.POH&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="POH" />
<Using Include="ktsu.Semantics.Quantities.Position3D&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="Position3D" />
<Using Include="ktsu.Semantics.Quantities.PotentialEnergy&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="PotentialEnergy" />
<Using Include="ktsu.Semantics.Quantities.Power&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="Power" />
Expand Down
4 changes: 2 additions & 2 deletions Semantics.Quantities/Chemistry/PH.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public readonly partial record struct PH<T>
/// <summary>
/// Gets the complementary pOH at 25 °C using <c>pOH = 14 − pH</c>.
/// </summary>
/// <returns>The pOH as a <see cref="PH{T}"/> scale value.</returns>
public PH<T> ToPOH() => new(T.CreateChecked(14) - Value);
/// <returns>The pOH as a <see cref="POH{T}"/> scale value.</returns>
public POH<T> ToPOH() => POH<T>.Create(T.CreateChecked(14) - Value);

/// <summary>Gets whether this value describes an acid (pH below 7).</summary>
public bool IsAcidic => Value < PhysicalConstants.Generic.NeutralPH<T>();
Expand Down
19 changes: 19 additions & 0 deletions Semantics.Quantities/Chemistry/POH.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Semantics.Quantities;

using System.Numerics;

/// <summary>
/// Bespoke members of <see cref="POH{T}"/>; the logarithmic core (including the
/// <see cref="Concentration{T}"/> conversions) is generated from <c>logarithmic.json</c>.
/// </summary>
public readonly partial record struct POH<T>
where T : struct, INumber<T>
{
/// <summary>
/// Gets the complementary pH at 25 °C using <c>pH = 14 − pOH</c>.
/// </summary>
/// <returns>The pH as a <see cref="PH{T}"/> scale value.</returns>
public PH<T> ToPH() => PH<T>.Create(T.CreateChecked(14) - Value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2023-2026 ktsu-dev contributors
// <auto-generated />

#nullable enable

namespace ktsu.Semantics.Quantities;

using System;
using System.Globalization;
using System.Numerics;

/// <summary>
/// Represents basicity on the pOH scale: pOH = −log10([OH⁻]) with the hydroxide-ion activity in mol/L.
/// </summary>
/// <remarks>
/// 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.
/// <para>
/// <b>Precision:</b> the conversions to and from the linear counterparts compute in
/// <see langword="double"/> whatever <typeparamref name="T"/> is, so a value carried in a
/// storage type wider than <see langword="double"/> — <see langword="decimal"/>, or
/// <c>PreciseNumber</c> — is accurate to about fifteen significant digits once it has been
/// through one. Arithmetic, comparison and the raw factory keep every digit
/// <typeparamref name="T"/> holds; only the logarithm and its inverse do not.
/// </para>
/// </remarks>
/// <typeparam name="T">The floating-point storage type.</typeparam>
/// <param name="Value">The scale value.</param>
public readonly partial record struct POH<T>(T Value) : IComparable<POH<T>>
where T : struct, INumber<T>
{
/// <summary>
/// Creates a value from the raw scale number.
/// </summary>
/// <param name="value">The raw scale value.</param>
/// <returns>A new <see cref="POH{T}"/>.</returns>
public static POH<T> Create(T value) => new(value);

/// <summary>
/// Creates a pOH from a hydroxide-ion concentration using pOH = −log10([OH⁻] in mol/L).
/// </summary>
/// <param name="linear">The linear <see cref="Concentration{T}"/>.</param>
/// <returns>A new <see cref="POH{T}"/>. A linear value of zero maps to negative infinity where <typeparamref name="T"/> can hold it.</returns>
/// <remarks>
/// Computes in <see langword="double"/> whatever <typeparamref name="T"/> is: the value is
/// converted to <see langword="double"/>, the logarithm or power is taken there, and the
/// result is converted back. A storage type wider than <see langword="double"/> —
/// <see langword="decimal"/>, or <c>PreciseNumber</c> — therefore keeps about fifteen
/// significant digits across this conversion, rather than the precision it is capable of.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException"><typeparamref name="T"/> cannot hold the result, as for a linear value of zero with <see langword="decimal"/> or integer storage.</exception>
public static POH<T> FromHydroxideConcentration(Concentration<T> linear)
{
double linearValue = double.CreateChecked(linear.Value);
double reference = 1000.0;
return new(LogarithmicStorage.FromLinear<T>(-1.0 * Math.Log10(linearValue / reference), nameof(linear)));
}

/// <summary>
/// Converts this pOH to the equivalent hydroxide-ion concentration using [OH⁻] = 10^(−pOH) mol/L.
/// </summary>
/// <returns>The linear <see cref="Concentration{T}"/>.</returns>
/// <remarks>
/// Computes in <see langword="double"/> whatever <typeparamref name="T"/> is: the value is
/// converted to <see langword="double"/>, the logarithm or power is taken there, and the
/// result is converted back. A storage type wider than <see langword="double"/> —
/// <see langword="decimal"/>, or <c>PreciseNumber</c> — therefore keeps about fifteen
/// significant digits across this conversion, rather than the precision it is capable of.
/// </remarks>
/// <exception cref="OverflowException"><typeparamref name="T"/> cannot hold the linear value of a level this high.</exception>
public Concentration<T> ToHydroxideConcentration()
{
double scaleValue = double.CreateChecked(Value);
double reference = 1000.0;
return Concentration<T>.Create(LogarithmicStorage.ToLinear<T>(reference * Math.Pow(10.0, scaleValue / -1.0)));
}

/// <inheritdoc/>
public int CompareTo(POH<T> other) => Value.CompareTo(other.Value);

/// <summary>Determines whether one value is less than another.</summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is less than <paramref name="right"/>.</returns>
public static bool operator <(POH<T> left, POH<T> right) => left.CompareTo(right) < 0;

/// <summary>Determines whether one value is greater than another.</summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is greater than <paramref name="right"/>.</returns>
public static bool operator >(POH<T> left, POH<T> right) => left.CompareTo(right) > 0;

/// <summary>Determines whether one value is less than or equal to another.</summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is less than or equal to <paramref name="right"/>.</returns>
public static bool operator <=(POH<T> left, POH<T> right) => left.CompareTo(right) <= 0;

/// <summary>Determines whether one value is greater than or equal to another.</summary>
/// <param name="left">The left value.</param>
/// <param name="right">The right value.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is greater than or equal to <paramref name="right"/>.</returns>
public static bool operator >=(POH<T> left, POH<T> right) => left.CompareTo(right) >= 0;

/// <summary>Returns a culture-invariant string representation of this value.</summary>
/// <returns>The formatted value.</returns>
public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"pOH {Value}");
}
19 changes: 19 additions & 0 deletions Semantics.SourceGenerators/Metadata/logarithmic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
]
}
21 changes: 21 additions & 0 deletions Semantics.Test/Quantities/QuantityBackfillTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,27 @@ public void PH_Acidity_Classification_And_POH()
Assert.IsFalse(PH<double>.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<double> poh = PH<double>.Create(3.0).ToPOH();
Assert.AreEqual(11.0, poh.Value, Tolerance);
Assert.StartsWith("pOH ", poh.ToString());
Assert.AreEqual(Concentration<double>.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<double> poh = POH<double>.FromHydroxideConcentration(Concentration<double>.FromMolar(1e-4));
Assert.AreEqual(4.0, poh.Value, 1e-9);
Assert.AreEqual(10.0, poh.ToPH().Value, 1e-9);
Assert.AreEqual(Concentration<double>.FromMolar(1e-4).Value, poh.ToHydroxideConcentration().Value, 1e-12);
}

[TestMethod]
public void Levels_Compare_And_Add_In_Decibel_Space()
{
Expand Down
24 changes: 24 additions & 0 deletions docs/migration-guide-6.0.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Migrating from Semantics 5.x to 6.0

## `PH<T>.ToPOH()` returns a `POH<T>`

`ToPOH()` used to return its pOH inside a `PH<T>`, 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<T>` scale, generated from `logarithmic.json` like `PH<T>`. `POH<T>`
has no `IsAcidic`, `IsBasic` or pH formatting; it prints as `pOH {0}`, converts to and from the
hydroxide-ion `Concentration<T>`, and `ToPH()` gives the complementary pH at 25 °C.

```csharp
// 5.x
PH<double> poh = PH<double>.Create(3).ToPOH(); // "pH 11", IsBasic == true

// 6.0
POH<double> poh = PH<double>.Create(3).ToPOH(); // "pOH 11"
PH<double> ph = poh.ToPH(); // pH 3
Concentration<double> 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<T>` gets `CS0029`; declare it as `POH<T>`, 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
Expand Down
Loading