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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +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.
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.

### Physical constants

Expand Down Expand Up @@ -152,6 +152,7 @@ var converted = sourceString.As<SourceType, TargetType>();
- **SEM002** — schema-level validation issue (missing `name`/`symbol`, empty `availableUnits`, duplicate type names, no vector forms declared).
- **SEM003** — a relationship's explicit `forms` list references a vector form not declared on a participating dimension. Use `forms` to constrain a relationship to specific vector forms (e.g. `crossProducts: [{ "other": "Length", "result": "Torque", "forms": [3] }]`); when omitted, the legacy "emit at every common form" behaviour is preserved.
- **SEM004** — a dimension's `availableUnits` array references a unit name that isn't declared anywhere in `units.json`. Without the diagnostic the generator silently emits an identity-conversion `From{Unit}` factory, which is wrong for any non-base unit; SEM004 catches the typo at build time.
- **SEM005** — schema-level validation issue in `logarithmic.json` (missing or duplicate scale names, a conversion with no linear type).
- See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through.

This file is the entry point. For deeper material:
Expand Down
72 changes: 3 additions & 69 deletions Semantics.Quantities/Acoustics/DirectionalityIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,16 @@

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.
/// Bespoke members of <see cref="DirectionalityIndex{T}"/>; the logarithmic core
/// is generated from <c>logarithmic.json</c>.
/// </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>>
public readonly partial record struct 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: 0 additions & 109 deletions Semantics.Quantities/Acoustics/SoundIntensityLevel.cs

This file was deleted.

109 changes: 0 additions & 109 deletions Semantics.Quantities/Acoustics/SoundPowerLevel.cs

This file was deleted.

Loading
Loading