From ff41c06d1e873eeec16259d3160d9e76f16d957c Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Wed, 14 Oct 2026 08:21:35 +0100 Subject: [PATCH] units: make the angle a dimension, so 1 Hz is 2*pi rad/s The radian was 1 and the hertz 1/second, so 1 rad/s equalled 1 Hz. The radian is now a base unit (RADIAN, dimension 'angle', in the common set), collected with SI_BASE_UNITS in the new BASE_UNITS. The hertz is a cycle per second (2*pi*radian/second), the steradian radian**2, and the degree, arcminute, arcsecond, lumen and lux are expressed in it. 2*pi*f rad/s now equals f Hz; 1 rad vs 1 and 1 Hz vs 1/s differ in dimension. Co-Authored-By: Claude Opus 5.5 --- tools/parity/expected_changes.json | 874 ++++++++++++++++-- units/CHANGELOG.md | 1 + units/README.md | 8 +- units/src/lambdafeedback/units/__init__.py | 4 + units/src/lambdafeedback/units/_conversion.py | 8 +- units/src/lambdafeedback/units/data.py | 40 +- units/src/lambdafeedback/units/quantity.py | 11 +- units/tests/test_api.py | 29 +- units/tests/test_regressions.py | 17 +- 9 files changed, 876 insertions(+), 116 deletions(-) diff --git a/tools/parity/expected_changes.json b/tools/parity/expected_changes.json index b540324..0cdbed3 100644 --- a/tools/parity/expected_changes.json +++ b/tools/parity/expected_changes.json @@ -631,6 +631,57 @@ "value_latex": "\\frac{gram \\cdot kilo \\cdot mega \\cdot metre^{2}}{A \\cdot a \\cdot d \\cdot f \\cdot s^{4}}" } }, + "quantity.parse|legacy|10 \"": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "\"", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 '": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "'", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 Hz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "Hz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|legacy|10 R": { "reason": "units: roentgen is 2.58e-4 C/kg (was kelvin/kg)", "value": { @@ -648,15 +699,49 @@ "value_latex": "10" } }, + "quantity.parse|legacy|10 angleminute": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "angleminute", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 anglesecond": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "anglesecond", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|legacy|10 deg": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "deg", "unit_factor": "pi/180", @@ -666,14 +751,14 @@ } }, "quantity.parse|legacy|10 degree": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "degree", "unit_factor": "pi/180", @@ -682,6 +767,23 @@ "value_latex": "10" } }, + "quantity.parse|legacy|10 hertz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "hertz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|legacy|10 kg m/s^2 + 10 kg m/s^2": { "reason": "units: FeedbackTag removed; reverted-unit facts now exposed as plain RevertedUnit(before, marked, after) records, not (id, tag, inputs) triples", "value": { @@ -856,6 +958,74 @@ "value_latex": "10" } }, + "quantity.parse|legacy|10 lm": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lm", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 lumen": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lumen", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 lux": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lux", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|legacy|10 lx": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lx", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|legacy|10 pint": { "reason": "units: imperial pint is 0.00056826125 m^3 (was truncated to 0.00056826)", "value": { @@ -891,14 +1061,14 @@ } }, "quantity.parse|legacy|10 r": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "r", "unit_factor": "1", @@ -908,14 +1078,14 @@ } }, "quantity.parse|legacy|10 radian": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "radian", "unit_factor": "1", @@ -1126,14 +1296,14 @@ } }, "quantity.parse|legacy|10 sr": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "sr", "unit_factor": "1", @@ -1143,14 +1313,14 @@ } }, "quantity.parse|legacy|10 steradian": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "steradian", "unit_factor": "1", @@ -1352,14 +1522,14 @@ } }, "quantity.parse|legacy|5 °": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "5 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "5~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "5*pi/180", "unit": "°", "unit_factor": "pi/180", @@ -1784,6 +1954,23 @@ "value_latex": "\\frac{gram \\cdot kilo \\cdot mega \\cdot metre^{2}}{A \\cdot a \\cdot d \\cdot f \\cdot s^{4}}" } }, + "quantity.parse|natural_si|10 Hz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "Hz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural_si|10 Np": { "reason": "units: FeedbackTag removed; reverted-unit facts now exposed as plain RevertedUnit(before, marked, after) records, not (id, tag, inputs) triples", "value": { @@ -1955,6 +2142,23 @@ "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" }, + "quantity.parse|natural_si|10 hertz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "hertz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural_si|10 hour": { "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" @@ -2158,6 +2362,74 @@ "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" }, + "quantity.parse|natural_si|10 lm": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lm", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural_si|10 lumen": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lumen", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural_si|10 lux": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lux", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural_si|10 lx": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lx", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural_si|10 metricton": { "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" @@ -3106,6 +3378,57 @@ "value_latex": "\\frac{gram \\cdot kilo \\cdot mega \\cdot metre^{2}}{A \\cdot a \\cdot d \\cdot f \\cdot s^{4}}" } }, + "quantity.parse|natural|10 \"": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "\"", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 '": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "'", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 Hz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "Hz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural|10 R": { "reason": "units: roentgen is 2.58e-4 C/kg (was kelvin/kg)", "value": { @@ -3123,15 +3446,49 @@ "value_latex": "10" } }, + "quantity.parse|natural|10 angleminute": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "angleminute", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 anglesecond": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "anglesecond", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural|10 deg": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "deg", "unit_factor": "pi/180", @@ -3141,14 +3498,14 @@ } }, "quantity.parse|natural|10 degree": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "degree", "unit_factor": "pi/180", @@ -3157,6 +3514,23 @@ "value_latex": "10" } }, + "quantity.parse|natural|10 hertz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "hertz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural|10 kg m/s^2 + 10 kg m/s^2": { "reason": "units: FeedbackTag removed; reverted-unit facts now exposed as plain RevertedUnit(before, marked, after) records, not (id, tag, inputs) triples", "value": { @@ -3331,6 +3705,74 @@ "value_latex": "10" } }, + "quantity.parse|natural|10 lm": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lm", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 lumen": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lumen", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 lux": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lux", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|natural|10 lx": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lx", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|natural|10 pint": { "reason": "units: imperial pint is 0.00056826125 m^3 (was truncated to 0.00056826)", "value": { @@ -3366,14 +3808,14 @@ } }, "quantity.parse|natural|10 r": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "r", "unit_factor": "1", @@ -3383,14 +3825,14 @@ } }, "quantity.parse|natural|10 radian": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "radian", "unit_factor": "1", @@ -3601,14 +4043,14 @@ } }, "quantity.parse|natural|10 sr": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "sr", "unit_factor": "1", @@ -3618,14 +4060,14 @@ } }, "quantity.parse|natural|10 steradian": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "steradian", "unit_factor": "1", @@ -3827,14 +4269,14 @@ } }, "quantity.parse|natural|5 °": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "5 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "5~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "5*pi/180", "unit": "°", "unit_factor": "pi/180", @@ -4357,6 +4799,57 @@ "value_latex": "1" } }, + "quantity.parse|strict_all|10 \"": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "\"", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 '": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "'", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 Hz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "Hz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_all|10 R": { "reason": "units: roentgen is 2.58e-4 C/kg (was kelvin/kg)", "value": { @@ -4374,15 +4867,49 @@ "value_latex": "10" } }, + "quantity.parse|strict_all|10 angleminute": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 angleminute", + "dimension": "pi*angle/10800", + "expanded_unit": "pi*radian/10800", + "latex": "10~\\mathrm{angleminute}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/10800", + "unit": "angleminute", + "unit_factor": "pi/10800", + "unit_latex": "\\mathrm{angleminute}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 anglesecond": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 anglesecond", + "dimension": "pi*angle/648000", + "expanded_unit": "pi*radian/648000", + "latex": "10~\\mathrm{anglesecond}", + "messages": [], + "standard_unit": "radian", + "standard_value": "10*pi/648000", + "unit": "anglesecond", + "unit_factor": "pi/648000", + "unit_latex": "\\mathrm{anglesecond}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_all|10 deg": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "deg", "unit_factor": "pi/180", @@ -4392,14 +4919,14 @@ } }, "quantity.parse|strict_all|10 degree": { - "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute)", + "reason": "units: angles in radians (radian = 1); degree is pi/180 (was 1/360 of a turn, inconsistent with angleminute); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 degree", - "dimension": "pi/180", - "expanded_unit": "pi/180", + "dimension": "pi*angle/180", + "expanded_unit": "pi*radian/180", "latex": "10~\\mathrm{degree}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*pi/180", "unit": "degree", "unit_factor": "pi/180", @@ -4484,6 +5011,23 @@ "value_latex": "\\frac{10 \\cdot c \\cdot d \\cdot e \\cdot gram \\cdot n \\cdot o}{metres}" } }, + "quantity.parse|strict_all|10 hertz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "hertz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_all|10 kg m/s^2 + 10 kg m/s^2": { "reason": "units: FeedbackTag removed; reverted-unit facts now exposed as plain RevertedUnit(before, marked, after) records, not (id, tag, inputs) triples", "value": { @@ -4814,6 +5358,74 @@ "value_latex": "\\frac{10 \\cdot c \\cdot e \\cdot gram \\cdot kilo \\cdot metres \\cdot n \\cdot o}{d^{2}}" } }, + "quantity.parse|strict_all|10 lm": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lm", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 lumen": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lumen", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 lux": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lux", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_all|10 lx": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lx", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_all|10 pint": { "reason": "units: imperial pint is 0.00056826125 m^3 (was truncated to 0.00056826)", "value": { @@ -4849,14 +5461,14 @@ } }, "quantity.parse|strict_all|10 r": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "r", "unit_factor": "1", @@ -4866,14 +5478,14 @@ } }, "quantity.parse|strict_all|10 radian": { - "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn)", + "reason": "units: angles in radians; radian is 1 (was 1/(2*pi), a turn); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 radian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle", + "expanded_unit": "radian", "latex": "10~\\mathrm{radian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian", "standard_value": "10*1", "unit": "radian", "unit_factor": "1", @@ -5069,14 +5681,14 @@ } }, "quantity.parse|strict_all|10 sr": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "sr", "unit_factor": "1", @@ -5086,14 +5698,14 @@ } }, "quantity.parse|strict_all|10 steradian": { - "reason": "units: steradian is 1 (was 1/(4*pi))", + "reason": "units: steradian is 1 (was 1/(4*pi)); units: the angle is a dimension (radian base unit, sr = rad^2)", "value": { "content": "10 steradian", - "dimension": "1", - "expanded_unit": "1", + "dimension": "angle**2", + "expanded_unit": "radian**2", "latex": "10~\\mathrm{steradian}", "messages": [], - "standard_unit": "1", + "standard_unit": "radian**2", "standard_value": "10*1", "unit": "steradian", "unit_factor": "1", @@ -6080,6 +6692,23 @@ "value_latex": "1" } }, + "quantity.parse|strict_si|10 Hz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "Hz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_si|10 Np": { "reason": "units: FeedbackTag removed; reverted-unit facts now exposed as plain RevertedUnit(before, marked, after) records, not (id, tag, inputs) triples", "value": { @@ -6327,6 +6956,23 @@ "value_latex": "\\frac{10 \\cdot e \\cdot c \\cdot d \\cdot gram \\cdot n \\cdot o}{metres}" } }, + "quantity.parse|strict_si|10 hertz": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 hertz", + "dimension": "2*pi*angle/time", + "expanded_unit": "2*pi*radian/second", + "latex": "10~\\mathrm{hertz}", + "messages": [], + "standard_unit": "radian/second", + "standard_value": "10*2*pi", + "unit": "hertz", + "unit_factor": "2*pi", + "unit_latex": "\\mathrm{hertz}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_si|10 hour": { "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" @@ -6686,6 +7332,74 @@ "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" }, + "quantity.parse|strict_si|10 lm": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lm", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_si|10 lumen": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lumen", + "dimension": "angle**2*luminous_intensity", + "expanded_unit": "candela*radian**2", + "latex": "10~\\mathrm{lumen}", + "messages": [], + "standard_unit": "candela*radian**2", + "standard_value": "10*1", + "unit": "lumen", + "unit_factor": "1", + "unit_latex": "\\mathrm{lumen}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_si|10 lux": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lux", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, + "quantity.parse|strict_si|10 lx": { + "reason": "units: the angle is a dimension (radian base unit, sr = rad^2, lm = cd sr); Hz is a cycle per second, 2*pi rad/s", + "value": { + "content": "10 lux", + "dimension": "angle**2*luminous_intensity/length**2", + "expanded_unit": "candela*radian**2/metre**2", + "latex": "10~\\mathrm{lux}", + "messages": [], + "standard_unit": "candela*radian**2/metre**2", + "standard_value": "10*1", + "unit": "lx", + "unit_factor": "1", + "unit_latex": "\\mathrm{lux}", + "value": "10", + "value_latex": "10" + } + }, "quantity.parse|strict_si|10 metricton": { "reason": "units: a unit spelled out from a unit set that isn't selected (10 hour, 2 litres with only SI) raises DisallowedUnitError; v0.1 read most as letters (10*h*o*u*r) and raised for a few by accident", "value": "ERROR" diff --git a/units/CHANGELOG.md b/units/CHANGELOG.md index fd09cec..7e6a8aa 100644 --- a/units/CHANGELOG.md +++ b/units/CHANGELOG.md @@ -15,6 +15,7 @@ The first public release. This is a further tidy-up after the 0.2.0 extraction: - A unit spelled out from a unit set that isn't selected raises the new `DisallowedUnitError` (a `QuantityParseError`, with `.unit` and `.unit_sets`): with only `SI`, `10 litre`, `3 feet` and `5 °` are rejected, naming the set that has the unit. Before, most were read as letters in the value, so `10 litre` was `10*E*i*l*r*t` with no unit (the `e` read as Euler's number), and a few raised an unrelated error. Names, alternative spellings and plurals are checked, with or without a prefix; symbols (`L`, `h`) are still read as variables. - **Angles are in radians.** The radian and the steradian are now 1, and the degree is `pi/180`. Before, the radian was `1/(2*pi)`, the steradian `1/(4*pi)` and the degree `1/360` (fractions of a turn), while the arcminute and the arcsecond were already in radians, so `60 angleminute` didn't equal `1 degree`. This changes `standard_value`, `unit_factor`, `expanded_unit` and `dimension` for every angle unit. +- **The angle is a dimension, and the hertz is a cycle per second.** The radian is a base unit (`RADIAN`, in the `common` set) with the dimension `angle`; the new `BASE_UNITS` holds it with `SI_BASE_UNITS`. The degree, arcminute and arcsecond are multiples of it, the steradian is `radian**2`, the lumen `candela*radian**2` and the lux `candela*radian**2/metre**2`. The hertz is `2*pi*radian/second`, so `f Hz` equals `2*pi*f rad/s`. Before, the radian was 1 and the hertz `1/second`, so `1 rad/s` equalled `1 Hz`. `1 rad` and `1`, and `1 Hz` and `1/s` (or `1 Bq`), now have different dimensions (`same_dimension` is false). - `parse_quantity` raises only `QuantityError`s. A parser error (`slr_parsing.ParseError`/`ScanError`, e.g. for `2 m)`) and a value SymPy can't parse (`ExpressionParsingError`, e.g. `2 x;y`) are now raised as `QuantityParseError`, with the original error as `__cause__`. Every `QuantityError` carries `expression` and `name`. - `QuantityParams` validates `unit_sets`: an unknown name (such as `"si"`) raises `QuantityParamsError`. Before, it was silently ignored, and no units were recognised. `from_dict` splits `units_string` on spaces and commas instead of testing for substrings, so `"SIcommon"` is rejected rather than read as both sets. - An unknown `strictness` raises `QuantityParamsError` (with `.field`). It is still an `ExpressionParsingError`, so existing handlers keep working. diff --git a/units/README.md b/units/README.md index 14f32f6..63630c2 100644 --- a/units/README.md +++ b/units/README.md @@ -69,13 +69,17 @@ parse_quantity(text, params) # PhysicalQuantity(value='2', unit='micrometre') | `standard_unit` | The unit in SI base units, without its numeric factor. | | `expanded_unit` | The unit in SI base units, with its factor. | | `unit_factor` | The factor between the unit and SI base units. | -| `base_dimension` | The unit's dimension in terms of `length`, `mass`, `time`, `electric_current`, `temperature`, `amount_of_substance` and `luminous_intensity` (`km/h` gives `length/time`). `1` for a quantity without a unit, and for angles. Compare two quantities with `same_dimension(a, b)`. | +| `base_dimension` | The unit's dimension in terms of `length`, `mass`, `time`, `electric_current`, `temperature`, `amount_of_substance`, `luminous_intensity` and `angle` (`km/h` gives `length/time`, `Hz` gives `angle/time`). `1` for a quantity without a unit. Compare two quantities with `same_dimension(a, b)`. | | `dimension` | The same, but keeping the unit's factor: `km/h` gives `10**3*length/((3600*time))`. | | `value_latex`, `unit_latex`, `latex` | LaTeX previews. | | `reverted_units` | `RevertedUnit(before, marked, after)` records for unit-like text that was read as part of the value (e.g. `kg` in `2 kg + 3`). | Mass is in **grams**, so that SI prefixes apply uniformly: `kilogram` expands -to `1000*gram`. Angles are dimensionless, with the radian equal to 1. +to `1000*gram`. **Angles have a dimension**, `angle`, with the radian as +its base unit: the degree is `pi/180*radian` and the steradian `radian**2`. +The hertz is a cycle per second, `2*pi*radian/second`, so `f Hz` equals +`2*pi*f rad/s`, while `1 Hz` and `1/s` (or `1 Bq`) have different +dimensions, as do `1 rad` and `1`. ## Parameters diff --git a/units/src/lambdafeedback/units/__init__.py b/units/src/lambdafeedback/units/__init__.py index d11501a..1bc4060 100644 --- a/units/src/lambdafeedback/units/__init__.py +++ b/units/src/lambdafeedback/units/__init__.py @@ -19,9 +19,11 @@ from ._split import MAX_UNIT_EXPONENT from .data import ( ALL_UNITS, + BASE_UNITS, COMMON_UNITS, CONVERSION_TO_BASE_SI, IMPERIAL_UNITS, + RADIAN, SI_BASE_UNITS, SI_DERIVED_UNITS, SI_PREFIXES, @@ -55,12 +57,14 @@ __all__ = [ "ALL_UNITS", + "BASE_UNITS", "COMMON_UNITS", "CONVERSION_TO_BASE_SI", "IMPERIAL_UNITS", "MAX_INPUT_LENGTH", "MAX_LEGACY_LENGTH", "MAX_UNIT_EXPONENT", + "RADIAN", "SI_BASE_UNITS", "SI_DERIVED_UNITS", "SI_PREFIXES", diff --git a/units/src/lambdafeedback/units/_conversion.py b/units/src/lambdafeedback/units/_conversion.py index ed13c2a..2f578ff 100644 --- a/units/src/lambdafeedback/units/_conversion.py +++ b/units/src/lambdafeedback/units/_conversion.py @@ -16,16 +16,16 @@ ) from lambdafeedback.slr_parsing import ExprNode, SLRParser -from .data import CONVERSION_TO_BASE_SI, SI_BASE_UNITS, unsplittable_names +from .data import BASE_UNITS, CONVERSION_TO_BASE_SI, unsplittable_names from .errors import UnitConversionError from .params import QuantityParams from .parser import Label -_BASE_UNIT_NAMES = frozenset(unit.name for unit in SI_BASE_UNITS) +_BASE_UNIT_NAMES = frozenset(unit.name for unit in BASE_UNITS) -_BASE_UNIT_DIMENSIONS = [(unit.name, unit.dimension) for unit in SI_BASE_UNITS] +_BASE_UNIT_DIMENSIONS = [(unit.name, unit.dimension) for unit in BASE_UNITS] -_DIMENSION_NAMES = frozenset(unit.dimension for unit in SI_BASE_UNITS) +_DIMENSION_NAMES = frozenset(unit.dimension for unit in BASE_UNITS) def _without_factor(dimension: Basic) -> Basic: diff --git a/units/src/lambdafeedback/units/data.py b/units/src/lambdafeedback/units/data.py index d3b561e..4e5f492 100644 --- a/units/src/lambdafeedback/units/data.py +++ b/units/src/lambdafeedback/units/data.py @@ -1,9 +1,11 @@ """Units and prefixes, and their conversion to SI base units. -Remarks: kelvin is the only temperature scale; angle measures, the bel and -the neper are all treated as dimensionless, with the radian and the -steradian equal to 1 (so ``degree`` is ``pi/180``). The gram (not the -kilogram) is the base unit of mass, so that SI prefixes apply uniformly. +Remarks: kelvin is the only temperature scale. The angle is a dimension, with +the radian as its base unit (:data:`RADIAN`): ``degree`` is ``pi/180*radian``, +the steradian is ``radian**2``, and the hertz is a cycle per second, +``2*pi*radian/second`` (so ``1 Hz`` is not ``1/s``, unlike the becquerel). The +bel and the neper are dimensionless. The gram (not the kilogram) is the base +unit of mass, so that SI prefixes apply uniformly. Every unit set's symbols are recognised, and any SI prefix can be attached to any unit (``kft`` is a kilofoot). Where a prefixed form is also the symbol of @@ -86,10 +88,16 @@ class Unit(NamedTuple): BaseUnit("candela", "cd", "luminous_intensity", ("Candela",), ("candelas", "Candelas")), ) +# 'r', not 'rad', to avoid a collision with the rad. In the common set, not SI. +RADIAN = BaseUnit("radian", "r", "angle", (), ("radians",)) + +BASE_UNITS: tuple[BaseUnit, ...] = (*SI_BASE_UNITS, RADIAN) +"""Every base unit: the SI ones and the radian. Unit expansions are in these units.""" + # Table 3 of https://physics.nist.gov/cuu/Units/units.html. Radian and steradian are in # VERY_COMMON_UNITS (fewer collisions when substituting); degree Celsius is omitted. SI_DERIVED_UNITS: tuple[Unit, ...] = ( - Unit("hertz", "Hz", "(second**(-1))", ("Hertz",)), + Unit("hertz", "Hz", "(2*pi*radian*second**(-1))", ("Hertz",)), Unit("newton", "N", "(metre*kilogram*second**(-2))", ("Newton",), ("newtons", "Newtons")), Unit("pascal", "Pa", "(metre**(-1)*kilogram*second**(-2))", ("Pascal",), ("pascals", "Pascals")), Unit("joule", "J", "(metre**2*kilogram*second**(-2))", ("Joule",), ("joules", "Joules")), @@ -108,8 +116,8 @@ class Unit(NamedTuple): Unit("weber", "Wb", "(metre**2*kilogram*second**(-2)*ampere**(-1))", ("Weber",), ("webers", "Webers")), Unit("tesla", "T", "(kilogram*second**(-2)*ampere**(-1))", ("Tesla",), ("teslas", "Teslas")), Unit("henry", "H", "(metre**2*kilogram*second**(-2)*ampere**(-2))", ("Henry",), ("henrys", "Henrys")), - Unit("lumen", "lm", "(candela)", (), ("lumens",)), - Unit("lux", "lx", "(metre**(-2)*candela)"), + Unit("lumen", "lm", "(candela*radian**2)", (), ("lumens",)), + Unit("lux", "lx", "(metre**(-2)*candela*radian**2)"), Unit("becquerel", "Bq", "(second**(-1))", ("Becquerel",), ("becquerels", "Becquerels")), Unit("gray", "Gy", "(metre**2*second**(-2))", ("Gray",), ("grays", "Grays")), Unit("sievert", "Sv", "(metre**2*second**(-2))", ("Sievert",), ("sieverts", "Sieverts")), @@ -118,12 +126,12 @@ class Unit(NamedTuple): # Tables 6 and 7 of https://physics.nist.gov/cuu/Units/outside.html: the subset whose # symbols are accepted. -VERY_COMMON_UNITS: tuple[Unit, ...] = ( - Unit("radian", "r", "(1)", (), ("radians",)), # 'r', not 'rad', to avoid a collision with the rad - Unit("steradian", "sr", "(1)", (), ("steradians",)), +VERY_COMMON_UNITS: tuple[BaseUnit | Unit, ...] = ( + RADIAN, + Unit("steradian", "sr", "(radian**2)", (), ("steradians",)), Unit("minute", "min", "(60*second)", (), ("minutes",)), Unit("hour", "h", "(3600*second)", (), ("hours",)), - Unit("degree", "deg", "(pi/180)", ("°",), ("degrees",)), + Unit("degree", "deg", "(pi/180*radian)", ("°",), ("degrees",)), Unit("litre", "L", "(10**(-3)*metre**3)", ("liter",), ("litres", "liters")), Unit("metricton", "t", "(10**3*kilogram)", ("tonne",), ("tonnes",)), Unit("neper", "Np", "(1)", ("Neper",), ("nepers", "Nepers")), @@ -138,8 +146,8 @@ class Unit(NamedTuple): # day, 'a' an are). COMMON_UNITS: tuple[Unit, ...] = ( Unit("day", "d", "(86400*second)", (), ("days",)), - Unit("angleminute", "'", "(pi/10800)"), - Unit("anglesecond", '"', "(pi/648000)"), + Unit("angleminute", "'", "(pi/10800*radian)"), + Unit("anglesecond", '"', "(pi/648000*radian)"), Unit("astronomical_unit", "au", "(149597870700*metre)", (), ("astronomical_units",)), Unit("nautical_mile", "nmi", "(1852*metre)", (), ("nautical_miles", "nauticalmiles")), # symbol from Wikipedia Unit("knot", "kn", "((1852/3600)*metre/second)", (), ("knots",)), # symbol from Wikipedia @@ -197,8 +205,8 @@ def unsplittable_names(unit_sets: frozenset[str] | set[str], all_forms: bool) -> alternative spellings and plurals (used for the value's preview) to the unit names (used for parsing). """ - names = {prefix.name for prefix in SI_PREFIXES} | {unit.name for unit in SI_BASE_UNITS} - names |= {unit.dimension for unit in SI_BASE_UNITS} + names = {prefix.name for prefix in SI_PREFIXES} | {unit.name for unit in BASE_UNITS} + names |= {unit.dimension for unit in BASE_UNITS} for unit in units_in(unit_sets): names |= {unit.name, unit.symbol, *unit.alternatives, *unit.plurals} if all_forms else {unit.name} return sorted(names) @@ -206,7 +214,7 @@ def unsplittable_names(unit_sets: frozenset[str] | set[str], all_forms: bool) -> def _conversion_to_base_si() -> dict[str, str]: unprefixed = {unit.name: unit.si_expansion for unit in ALL_UNITS if isinstance(unit, Unit)} - unprefixed.update({unit.name: unit.name for unit in SI_BASE_UNITS}) + unprefixed.update({unit.name: unit.name for unit in BASE_UNITS}) prefixed = { prefix.name + name: f"({prefix.factor}*{expansion})" for prefix in SI_PREFIXES diff --git a/units/src/lambdafeedback/units/quantity.py b/units/src/lambdafeedback/units/quantity.py index a5d82a6..f33cd0e 100644 --- a/units/src/lambdafeedback/units/quantity.py +++ b/units/src/lambdafeedback/units/quantity.py @@ -93,9 +93,9 @@ def _revert_value_text(value: ExprNode) -> tuple[RevertedUnit, ...]: class PhysicalQuantity: """A response read as a quantity: its value and unit, their LaTeX, and SI forms. - Use :func:`parse_quantity` to create one. SymPy results use the SI base - units of :data:`~lambdafeedback.units.SI_BASE_UNITS` as symbols; mass is - in grams (``kilogram`` expands to ``1000*gram``). + Use :func:`parse_quantity` to create one. SymPy results use the base + units of :data:`~lambdafeedback.units.BASE_UNITS` (the SI ones and the + radian) as symbols; mass is in grams (``kilogram`` expands to ``1000*gram``). Its attributes can't be set (it is frozen), but ``value``, ``unit`` and ``ast_root`` are ordinary parse trees. Quantities compare by identity. @@ -252,7 +252,8 @@ def same_dimension(first: PhysicalQuantity, second: PhysicalQuantity) -> bool: """Whether two quantities have the same dimension, whatever their units' factors. ``5 km/h`` and ``3 m/s`` have the same dimension (``length/time``); so do - ``1 L`` and ``1 m^3``. Quantities without units are dimensionless, as are - angles (``2 deg`` has the same dimension as ``2``). + ``1 L`` and ``1 m^3``. Quantities without units are dimensionless. Angles + are not (``2 deg`` is an ``angle``, not ``2``), so ``1 Hz`` (a cycle per + second) has the dimension of ``1 rad/s``, not of ``1/s``. """ return bool(simplify(first.base_dimension - second.base_dimension) == 0) diff --git a/units/tests/test_api.py b/units/tests/test_api.py index fa9deac..1f912f3 100644 --- a/units/tests/test_api.py +++ b/units/tests/test_api.py @@ -7,6 +7,7 @@ from lambdafeedback.expression_parsing import ExpressionParsingError from lambdafeedback.units import ( ALL_UNITS, + BASE_UNITS, CONVERSION_TO_BASE_SI, SI_BASE_UNITS, SI_PREFIXES, @@ -120,10 +121,10 @@ def test_unit_names_are_unique(self): assert len(names) == len(set(names)) @pytest.mark.parametrize("unit", ALL_UNITS, ids=lambda unit: unit.name) - def test_every_unit_converts_to_si_base_units(self, unit): + def test_every_unit_converts_to_base_units(self, unit): quantity = parse_quantity(f"1 {unit.name}", QuantityParams()) assert quantity.unit is not None - base_names = {base.name for base in SI_BASE_UNITS} + base_names = {base.name for base in BASE_UNITS} assert {str(symbol) for symbol in quantity.expanded_unit.free_symbols} <= base_names @@ -142,7 +143,9 @@ class TestDimensions: ("1 L", "length**3"), ("2 m^(1/2)", "sqrt(length)"), ("2 kΩ", "length**2*mass/(electric_current**2*time**3)"), - ("10 deg", "1"), + ("10 deg", "angle"), + ("1 sr", "angle**2"), + ("3 Hz", "angle/time"), ("2 x", "1"), ], ) @@ -163,9 +166,9 @@ def test_dimension_keeps_the_factor(self): ("1 dm^3", "2 gal"), ("1 N", "1 kg m/s^2"), ("1 J", "1 eV"), - ("1 Hz", "1/s"), + ("1 Hz", "1 r/s"), ("2 m^0.5", "3 m^(1/2)"), - ("10 deg", "2"), + ("10 deg", "2 r"), ("kg", "3 lb"), ], ) @@ -173,7 +176,18 @@ def test_same_dimension(self, left, right): params = QuantityParams() assert same_dimension(parse_quantity(left, params), parse_quantity(right, params)) - @pytest.mark.parametrize("left, right", [("1 m", "1 s"), ("1 J", "1 N"), ("1 m^2", "1 m"), ("1 kg", "2")]) + @pytest.mark.parametrize( + "left, right", + [ + ("1 m", "1 s"), + ("1 J", "1 N"), + ("1 m^2", "1 m"), + ("1 kg", "2"), + ("1 radian", "1"), + ("1 Hz", "1/s"), + ("1 Hz", "1 Bq"), + ], + ) def test_different_dimension(self, left, right): params = QuantityParams() assert not same_dimension(parse_quantity(left, params), parse_quantity(right, params)) @@ -186,7 +200,8 @@ class TestConversions: ("60 angleminute", "1 degree"), ("60 anglesecond", "1 angleminute"), ("180 degree", "pi radian"), - ("1 radian", "1"), + ("1 sr", "1 radian^2"), + ("1 Hz", "2 pi radian/s"), ("1 h", "60 min"), ("1 day", "24 hour"), ("1 L", "1 dm^3"), diff --git a/units/tests/test_regressions.py b/units/tests/test_regressions.py index 899d60c..b09cc2c 100644 --- a/units/tests/test_regressions.py +++ b/units/tests/test_regressions.py @@ -1,7 +1,7 @@ """Regression tests for bugs carried over from the v0.1 extraction.""" import pytest -from sympy import Basic, simplify +from sympy import Basic, pi, simplify from lambdafeedback.slr_parsing import GROUP_LABEL, ExprNode from lambdafeedback.units import ( @@ -39,9 +39,22 @@ def test_roentgen_is_charge_per_mass(self): def test_angle_units_share_one_base(self): # Degrees and radians were in turns but arcminutes in radians, so 60' != 1°. - assert parse("1 radian").unit_factor == 1 + assert str(parse("1 radian").standard_unit) == "radian" assert simplify(parse("60 angleminute").standard_values[0] - parse("1 degree").standard_values[0]) == 0 + def test_hertz_is_a_cycle_per_second(self): + # 1 rad/s was 1 Hz: the radian was 1 and the hertz 1/s, losing the 2*pi per cycle. + hertz, radian_per_second = parse("1 Hz"), parse("1 radian/second") + assert hertz.standard_unit == radian_per_second.standard_unit + assert simplify(hertz.standard_values[0] / radian_per_second.standard_values[0] - 2 * pi) == 0 + + @pytest.mark.parametrize("params", [NATURAL, QuantityParams(strictness="natural", elementary_functions=True)]) + def test_angular_frequency_matches_frequency(self, params): + # Upstream test_radians_to_frequency: 2*pi*f rad/s is f Hz. + angular, frequency = parse("2*pi*f radian/second", params), parse("f Hz", params) + assert angular.standard_unit == frequency.standard_unit + assert simplify(angular.standard_values[0] - frequency.standard_values[0]) == 0 + class TestParsing: @pytest.mark.parametrize("expr", ["(2 m) s", "(2 m) (s)", "(x m) s"])