Skip to content
Merged
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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 hand-written companions, not generated dimensions.** Decibel scales (`SoundPressureLevel`, `SoundIntensityLevel`, `SoundPowerLevel`, `DirectionalityIndex`, the audio-engineering `Decibels`) and `PH` don't obey linear arithmetic, so they live as self-contained `readonly record struct`s that convert to and from their linear generated counterparts (`SoundPressure`, `SoundIntensity`, `SoundPower`, `Ratio`, `Concentration`). Adding a new log-scale quantity means writing such a companion, not a `dimensions.json` entry.

### Physical constants

Expand Down
85 changes: 85 additions & 0 deletions Semantics.Quantities/Acoustics/DirectionalityIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Semantics.Quantities;

using System.Globalization;
using System.Numerics;

/// <summary>
/// Represents a directivity index (DI) in decibels — how much more intense a
/// source or receiver is on-axis than its spherical average.
/// </summary>
/// <remarks>
/// <c>DI = 10·log10(I_axis / I_average)</c>. Like all decibel scales it is a
/// hand-written companion rather than a generated linear quantity.
/// </remarks>
/// <typeparam name="T">The floating-point storage type.</typeparam>
/// <param name="Value">The index in decibels.</param>
public readonly record struct DirectionalityIndex<T>(T Value) : IComparable<DirectionalityIndex<T>>
where T : struct, INumber<T>
{
/// <summary>Gets the index of an omnidirectional source (0 dB).</summary>
public static DirectionalityIndex<T> Omnidirectional => new(T.Zero);

/// <summary>
/// Creates an index from a raw decibel value.
/// </summary>
/// <param name="decibels">The index in decibels.</param>
/// <returns>A new <see cref="DirectionalityIndex{T}"/>.</returns>
public static DirectionalityIndex<T> FromDecibels(T decibels) => new(decibels);

/// <summary>
/// Creates an index from the linear on-axis-to-average intensity ratio using <c>DI = 10·log10(ratio)</c>.
/// </summary>
/// <param name="ratio">The intensity ratio.</param>
/// <returns>A new <see cref="DirectionalityIndex{T}"/>.</returns>
public static DirectionalityIndex<T> FromIntensityRatio(Ratio<T> ratio)
{
ArgumentNullException.ThrowIfNull(ratio);
double linear = double.CreateChecked(ratio.Value);
return new(T.CreateChecked(10.0 * Math.Log10(linear)));
}

/// <summary>
/// Converts this index to the linear intensity ratio using <c>ratio = 10^(DI/10)</c>.
/// </summary>
/// <returns>The on-axis-to-average intensity <see cref="Ratio{T}"/>.</returns>
public Ratio<T> ToIntensityRatio()
{
double db = double.CreateChecked(Value);
return Ratio<T>.Create(T.CreateChecked(Math.Pow(10.0, db / 10.0)));
}

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

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

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

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

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

/// <summary>Returns a culture-invariant string representation of this index.</summary>
/// <returns>The index formatted with a <c> dB</c> suffix.</returns>
public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"{Value} dB");
}
109 changes: 109 additions & 0 deletions Semantics.Quantities/Acoustics/SoundIntensityLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Semantics.Quantities;

using System.Globalization;
using System.Numerics;

/// <summary>
/// Represents a sound intensity level (SIL) in decibels relative to the
/// 10⁻¹² W/m² threshold of hearing.
/// </summary>
/// <remarks>
/// SIL is a logarithmic power quantity: <c>SIL = 10·log10(I / I₀)</c> with
/// <c>I₀ = 10⁻¹² W/m²</c>. Logarithmic scales are hand-written companions to the
/// linear generated quantities — see <see cref="SoundIntensity{T}"/>.
/// </remarks>
/// <typeparam name="T">The floating-point storage type.</typeparam>
/// <param name="Value">The level in decibels.</param>
public readonly record struct SoundIntensityLevel<T>(T Value) : IComparable<SoundIntensityLevel<T>>
where T : struct, INumber<T>
{
/// <summary>
/// Creates a level from a raw decibel value.
/// </summary>
/// <param name="decibels">The level in dB re 10⁻¹² W/m².</param>
/// <returns>A new <see cref="SoundIntensityLevel{T}"/>.</returns>
public static SoundIntensityLevel<T> FromDecibels(T decibels) => new(decibels);

/// <summary>
/// Creates a level from a linear sound intensity using <c>SIL = 10·log10(I / I₀)</c>.
/// </summary>
/// <param name="intensity">The sound intensity.</param>
/// <returns>A new <see cref="SoundIntensityLevel{T}"/>. Zero intensity maps to negative infinity.</returns>
public static SoundIntensityLevel<T> FromSoundIntensity(SoundIntensity<T> intensity)
{
ArgumentNullException.ThrowIfNull(intensity);
double i = double.CreateChecked(intensity.Value);
double i0 = PhysicalConstants.Generic.ReferenceSoundIntensity<double>();
return new(T.CreateChecked(10.0 * Math.Log10(i / i0)));
}

/// <summary>
/// Converts this level to the equivalent linear sound intensity using <c>I = I₀·10^(SIL/10)</c>.
/// </summary>
/// <returns>The <see cref="SoundIntensity{T}"/>.</returns>
public SoundIntensity<T> ToSoundIntensity()
{
double db = double.CreateChecked(Value);
double i0 = PhysicalConstants.Generic.ReferenceSoundIntensity<double>();
return SoundIntensity<T>.Create(T.CreateChecked(i0 * Math.Pow(10.0, db / 10.0)));
}

/// <summary>Adds two levels in decibel space.</summary>
/// <param name="left">The first level.</param>
/// <param name="right">The second level.</param>
/// <returns>The summed level.</returns>
public static SoundIntensityLevel<T> operator +(SoundIntensityLevel<T> left, SoundIntensityLevel<T> right) => new(left.Value + right.Value);

/// <summary>Subtracts one level from another in decibel space.</summary>
/// <param name="left">The level to subtract from.</param>
/// <param name="right">The level to subtract.</param>
/// <returns>The difference level.</returns>
public static SoundIntensityLevel<T> operator -(SoundIntensityLevel<T> left, SoundIntensityLevel<T> right) => new(left.Value - right.Value);

/// <summary>Adds two levels (friendly alternate for <c>operator +</c>).</summary>
/// <param name="left">The first level.</param>
/// <param name="right">The second level.</param>
/// <returns>The summed level.</returns>
public static SoundIntensityLevel<T> Add(SoundIntensityLevel<T> left, SoundIntensityLevel<T> right) => left + right;

/// <summary>Subtracts one level from another (friendly alternate for <c>operator -</c>).</summary>
/// <param name="left">The level to subtract from.</param>
/// <param name="right">The level to subtract.</param>
/// <returns>The difference level.</returns>
public static SoundIntensityLevel<T> Subtract(SoundIntensityLevel<T> left, SoundIntensityLevel<T> right) => left - right;

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

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

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

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

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

/// <summary>Returns a culture-invariant string representation of this level.</summary>
/// <returns>The level formatted with a <c> dB SIL</c> suffix.</returns>
public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"{Value} dB SIL");
}
109 changes: 109 additions & 0 deletions Semantics.Quantities/Acoustics/SoundPowerLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) ktsu.dev
// All rights reserved.
// Licensed under the MIT license.

namespace ktsu.Semantics.Quantities;

using System.Globalization;
using System.Numerics;

/// <summary>
/// Represents a sound power level (SWL) in decibels relative to the 10⁻¹² W
/// reference sound power.
/// </summary>
/// <remarks>
/// SWL is a logarithmic power quantity: <c>SWL = 10·log10(P / P₀)</c> with
/// <c>P₀ = 10⁻¹² W</c>. Logarithmic scales are hand-written companions to the
/// linear generated quantities — see <see cref="SoundPower{T}"/>.
/// </remarks>
/// <typeparam name="T">The floating-point storage type.</typeparam>
/// <param name="Value">The level in decibels.</param>
public readonly record struct SoundPowerLevel<T>(T Value) : IComparable<SoundPowerLevel<T>>
where T : struct, INumber<T>
{
/// <summary>
/// Creates a level from a raw decibel value.
/// </summary>
/// <param name="decibels">The level in dB re 10⁻¹² W.</param>
/// <returns>A new <see cref="SoundPowerLevel{T}"/>.</returns>
public static SoundPowerLevel<T> FromDecibels(T decibels) => new(decibels);

/// <summary>
/// Creates a level from a linear sound power using <c>SWL = 10·log10(P / P₀)</c>.
/// </summary>
/// <param name="power">The sound power.</param>
/// <returns>A new <see cref="SoundPowerLevel{T}"/>. Zero power maps to negative infinity.</returns>
public static SoundPowerLevel<T> FromSoundPower(SoundPower<T> power)
{
ArgumentNullException.ThrowIfNull(power);
double p = double.CreateChecked(power.Value);
double p0 = PhysicalConstants.Generic.ReferenceSoundPower<double>();
return new(T.CreateChecked(10.0 * Math.Log10(p / p0)));
}

/// <summary>
/// Converts this level to the equivalent linear sound power using <c>P = P₀·10^(SWL/10)</c>.
/// </summary>
/// <returns>The <see cref="SoundPower{T}"/>.</returns>
public SoundPower<T> ToSoundPower()
{
double db = double.CreateChecked(Value);
double p0 = PhysicalConstants.Generic.ReferenceSoundPower<double>();
return SoundPower<T>.Create(T.CreateChecked(p0 * Math.Pow(10.0, db / 10.0)));
}

/// <summary>Adds two levels in decibel space.</summary>
/// <param name="left">The first level.</param>
/// <param name="right">The second level.</param>
/// <returns>The summed level.</returns>
public static SoundPowerLevel<T> operator +(SoundPowerLevel<T> left, SoundPowerLevel<T> right) => new(left.Value + right.Value);

/// <summary>Subtracts one level from another in decibel space.</summary>
/// <param name="left">The level to subtract from.</param>
/// <param name="right">The level to subtract.</param>
/// <returns>The difference level.</returns>
public static SoundPowerLevel<T> operator -(SoundPowerLevel<T> left, SoundPowerLevel<T> right) => new(left.Value - right.Value);

/// <summary>Adds two levels (friendly alternate for <c>operator +</c>).</summary>
/// <param name="left">The first level.</param>
/// <param name="right">The second level.</param>
/// <returns>The summed level.</returns>
public static SoundPowerLevel<T> Add(SoundPowerLevel<T> left, SoundPowerLevel<T> right) => left + right;

/// <summary>Subtracts one level from another (friendly alternate for <c>operator -</c>).</summary>
/// <param name="left">The level to subtract from.</param>
/// <param name="right">The level to subtract.</param>
/// <returns>The difference level.</returns>
public static SoundPowerLevel<T> Subtract(SoundPowerLevel<T> left, SoundPowerLevel<T> right) => left - right;

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

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

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

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

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

/// <summary>Returns a culture-invariant string representation of this level.</summary>
/// <returns>The level formatted with a <c> dB SWL</c> suffix.</returns>
public override string ToString() => string.Create(CultureInfo.InvariantCulture, $"{Value} dB SWL");
}
Loading
Loading