Skip to content

Commit e88594c

Browse files
authored
Add abs(), diff() support to PhysicalValue (#262)
1 parent b406b5a commit e88594c

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed

crates/pcb-sch/src/physical.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,24 @@ impl PhysicalValue {
225225
self.fits_within(other, default_tolerance)
226226
}
227227

228+
/// Get the absolute value of this physical value
229+
pub fn abs(&self) -> PhysicalValue {
230+
PhysicalValue {
231+
value: self.value.abs(),
232+
tolerance: self.tolerance,
233+
unit: self.unit,
234+
}
235+
}
236+
237+
/// Get the absolute difference between two physical values
238+
/// Returns an error if units don't match
239+
pub fn diff(&self, other: &PhysicalValue) -> Result<PhysicalValue, PhysicalValueError> {
240+
// Use the subtraction operator which validates units
241+
let result = (*self - *other)?;
242+
// Return the absolute value
243+
Ok(result.abs())
244+
}
245+
228246
fn fields() -> SortedMap<String, Ty> {
229247
fn single_param_spec(param_type: Ty) -> ParamSpec {
230248
ParamSpec::new_parts([(ParamIsRequired::Yes, param_type)], [], None, [], None).unwrap()
@@ -234,6 +252,8 @@ impl PhysicalValue {
234252
let with_tolerance_param_spec = single_param_spec(Ty::union2(Ty::float(), Ty::string()));
235253
let with_value_param_spec = single_param_spec(Ty::union2(Ty::float(), Ty::int()));
236254
let with_unit_param_spec = single_param_spec(Ty::union2(Ty::string(), Ty::none()));
255+
let diff_param_spec = single_param_spec(PhysicalValue::get_type_starlark_repr());
256+
let abs_param_spec = single_param_spec(PhysicalValue::get_type_starlark_repr());
237257

238258
SortedMap::from_iter([
239259
("value".to_string(), Ty::float()),
@@ -264,6 +284,14 @@ impl PhysicalValue {
264284
PhysicalValue::get_type_starlark_repr(),
265285
),
266286
),
287+
(
288+
"abs".to_string(),
289+
Ty::callable(abs_param_spec, PhysicalValue::get_type_starlark_repr()),
290+
),
291+
(
292+
"diff".to_string(),
293+
Ty::callable(diff_param_spec, PhysicalValue::get_type_starlark_repr()),
294+
),
267295
])
268296
}
269297

@@ -1029,6 +1057,32 @@ fn physical_value_methods(methods: &mut MethodsBuilder) {
10291057
new_unit,
10301058
))
10311059
}
1060+
1061+
fn abs<'v>(
1062+
this: &PhysicalValue,
1063+
#[starlark(require = pos)] _arg: Value<'v>,
1064+
) -> starlark::Result<PhysicalValue> {
1065+
Ok(this.abs())
1066+
}
1067+
1068+
fn diff<'v>(
1069+
this: &PhysicalValue,
1070+
#[starlark(require = pos)] other: Value<'v>,
1071+
) -> starlark::Result<PhysicalValue> {
1072+
let other_pv = PhysicalValue::try_from(other).map_err(|_| {
1073+
PhysicalValueError::InvalidArgumentType {
1074+
unit: this.unit.quantity(),
1075+
}
1076+
})?;
1077+
this.diff(&other_pv).map_err(|err| {
1078+
PhysicalValueError::SubtractionError {
1079+
lhs_unit: this.unit.quantity(),
1080+
rhs_unit: other_pv.unit.quantity(),
1081+
error: err.to_string(),
1082+
}
1083+
.into()
1084+
})
1085+
}
10321086
}
10331087

10341088
#[starlark_value(type = "PhysicalValue")]
@@ -3109,4 +3163,92 @@ mod tests {
31093163
assert_eq!(range.max, Decimal::from(10));
31103164
assert_eq!(range.nominal, Some(Decimal::from(5)));
31113165
}
3166+
3167+
#[test]
3168+
fn test_abs_positive_value() {
3169+
let pv = physical_value(3.3, 0.0, PhysicalUnit::Volts);
3170+
let result = pv.abs();
3171+
assert_eq!(result.value, Decimal::from_f64(3.3).unwrap());
3172+
assert_eq!(result.unit, PhysicalUnit::Volts.into());
3173+
assert_eq!(result.tolerance, Decimal::ZERO);
3174+
}
3175+
3176+
#[test]
3177+
fn test_abs_negative_value() {
3178+
let pv = physical_value(-3.3, 0.0, PhysicalUnit::Volts);
3179+
let result = pv.abs();
3180+
assert_eq!(result.value, Decimal::from_f64(3.3).unwrap());
3181+
assert_eq!(result.unit, PhysicalUnit::Volts.into());
3182+
assert_eq!(result.tolerance, Decimal::ZERO);
3183+
}
3184+
3185+
#[test]
3186+
fn test_abs_preserves_tolerance() {
3187+
let pv = physical_value(-5.0, 0.05, PhysicalUnit::Amperes);
3188+
let result = pv.abs();
3189+
assert_eq!(result.value, Decimal::from_f64(5.0).unwrap());
3190+
assert_eq!(result.unit, PhysicalUnit::Amperes.into());
3191+
assert_eq!(result.tolerance, Decimal::from_f64(0.05).unwrap());
3192+
}
3193+
3194+
#[test]
3195+
fn test_diff_positive_difference() {
3196+
let pv1 = physical_value(10.0, 0.0, PhysicalUnit::Volts);
3197+
let pv2 = physical_value(3.0, 0.0, PhysicalUnit::Volts);
3198+
let result = pv1.diff(&pv2).unwrap();
3199+
assert_eq!(result.value, Decimal::from_f64(7.0).unwrap());
3200+
assert_eq!(result.unit, PhysicalUnit::Volts.into());
3201+
assert_eq!(result.tolerance, Decimal::ZERO);
3202+
}
3203+
3204+
#[test]
3205+
fn test_diff_negative_difference_returns_positive() {
3206+
let pv1 = physical_value(3.0, 0.0, PhysicalUnit::Volts);
3207+
let pv2 = physical_value(10.0, 0.0, PhysicalUnit::Volts);
3208+
let result = pv1.diff(&pv2).unwrap();
3209+
assert_eq!(result.value, Decimal::from_f64(7.0).unwrap());
3210+
assert_eq!(result.unit, PhysicalUnit::Volts.into());
3211+
assert_eq!(result.tolerance, Decimal::ZERO);
3212+
}
3213+
3214+
#[test]
3215+
fn test_diff_unit_mismatch() {
3216+
let pv1 = physical_value(10.0, 0.0, PhysicalUnit::Volts);
3217+
let pv2 = physical_value(3.0, 0.0, PhysicalUnit::Amperes);
3218+
let result = pv1.diff(&pv2);
3219+
assert!(result.is_err());
3220+
assert!(matches!(
3221+
result.unwrap_err(),
3222+
PhysicalValueError::UnitMismatch { .. }
3223+
));
3224+
}
3225+
3226+
#[test]
3227+
fn test_diff_drops_tolerance() {
3228+
// Based on subtraction behavior, diff should drop tolerance
3229+
let pv1 = physical_value(10.0, 0.1, PhysicalUnit::Volts);
3230+
let pv2 = physical_value(3.0, 0.05, PhysicalUnit::Volts);
3231+
let result = pv1.diff(&pv2).unwrap();
3232+
assert_eq!(result.value, Decimal::from_f64(7.0).unwrap());
3233+
assert_eq!(result.tolerance, Decimal::ZERO);
3234+
}
3235+
3236+
#[test]
3237+
fn test_diff_with_string_conversion() {
3238+
// Test that diff works when the other value is parsed from a string
3239+
use starlark::values::Heap;
3240+
3241+
let heap = Heap::new();
3242+
let pv1 = heap.alloc(physical_value(3.3, 0.0, PhysicalUnit::Volts));
3243+
let pv2_str = heap.alloc("5V");
3244+
3245+
// Convert string to PhysicalValue
3246+
let pv2 = PhysicalValue::try_from(pv2_str).unwrap();
3247+
let pv1_val = PhysicalValue::try_from(pv1).unwrap();
3248+
3249+
// Test diff
3250+
let result = pv1_val.diff(&pv2).unwrap();
3251+
assert_eq!(result.value, Decimal::from_f64(1.7).unwrap());
3252+
assert_eq!(result.unit, PhysicalUnit::Volts.into());
3253+
}
31123254
}

docs/pages/spec.mdx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,133 @@ main_rail = PowerRail("MAIN_RAIL",
815815

816816
**Returns:** A callable net type constructor that creates typed net instances
817817

818+
### builtin.physical_value(unit)
819+
820+
**Built-in function** that creates unit-specific physical value constructor types for electrical quantities with optional tolerances.
821+
822+
```python
823+
# Create physical value constructors for different units
824+
Voltage = builtin.physical_value("V")
825+
Current = builtin.physical_value("A")
826+
Resistance = builtin.physical_value("Ohm")
827+
Capacitance = builtin.physical_value("F")
828+
Inductance = builtin.physical_value("H")
829+
Frequency = builtin.physical_value("Hz")
830+
Temperature = builtin.physical_value("K")
831+
Time = builtin.physical_value("s")
832+
Power = builtin.physical_value("W")
833+
834+
# Use the constructors to create physical values
835+
supply = Voltage("3.3V")
836+
current = Current("100mA")
837+
resistor = Resistance("4k7") # 4.7kΩ using resistor notation
838+
839+
# With tolerance
840+
precise_voltage = Voltage("3.3V").with_tolerance("5%") # 3.3V ±5%
841+
loose_resistor = Resistance("10kOhm").with_tolerance(0.1) # ±10%
842+
```
843+
844+
**Parameters:**
845+
846+
- `unit`: String identifier for the physical unit (e.g., `"V"`, `"A"`, `"Ohm"`, `"F"`, `"H"`, `"Hz"`, `"K"`, `"s"`, `"W"`)
847+
848+
**Returns:** A `PhysicalValueType` that can be called to create `PhysicalValue` instances
849+
850+
**Standard Usage:**
851+
852+
This builtin is typically accessed through `@stdlib/units.zen`, which provides pre-defined constructors:
853+
854+
```python
855+
load("@stdlib/units.zen", "Voltage", "Current", "Resistance", "unit")
856+
857+
# Create physical values
858+
v = unit("3.3V", Voltage)
859+
i = unit("100mA", Current)
860+
861+
# Perform calculations - units are tracked automatically
862+
p = v * i # Power = Voltage × Current (330mW)
863+
r = v / i # Resistance = Voltage / Current (33Ω)
864+
```
865+
866+
**Physical Value Methods:**
867+
868+
Physical values support several methods for manipulation:
869+
870+
- `.with_tolerance(tolerance)` - Returns a new physical value with updated tolerance
871+
- `tolerance`: String like `"5%"` or decimal like `0.05`
872+
- `.with_value(value)` - Returns a new physical value with updated numeric value
873+
- `value`: Numeric value (int or float)
874+
- `.with_unit(unit)` - Returns a new physical value with updated unit (for unit conversion/casting)
875+
- `unit`: String unit identifier or `None` for dimensionless
876+
- `.abs()` - Returns the absolute value of the physical value, preserving unit and tolerance
877+
- No parameters required
878+
- `.diff(other)` - Returns the absolute difference between two physical values
879+
- `other`: Another PhysicalValue or string (e.g., `"5V"`) to compare against
880+
- Units must match or an error is raised
881+
- Always returns a positive value
882+
- Tolerance is dropped (consistent with subtraction behavior)
883+
884+
**Attributes:**
885+
886+
- `.value` - The numeric value as a float
887+
- `.tolerance` - The tolerance as a decimal fraction (e.g., 0.05 for 5%)
888+
- `.unit` - The unit string representation
889+
890+
**Mathematical Operations:**
891+
892+
Physical values support arithmetic with automatic unit tracking:
893+
894+
```python
895+
# Multiplication - units multiply dimensionally
896+
power = Voltage("3.3V") * Current("0.5A") # 1.65W
897+
898+
# Division - units divide dimensionally
899+
resistance = Voltage("5V") / Current("100mA") # 50Ω
900+
901+
# Addition - requires matching units
902+
total = Voltage("3.3V") + Voltage("5V") # 8.3V
903+
904+
# Subtraction - requires matching units
905+
delta = Voltage("5V") - Voltage("3.3V") # 1.7V
906+
907+
# Absolute value
908+
abs_voltage = Voltage("-3.3V").abs() # 3.3V
909+
910+
# Difference (always positive)
911+
diff = Voltage("3.3V").diff(Voltage("5V")) # 1.7V
912+
diff_str = Voltage("3.3V").diff("5V") # 1.7V (strings are auto-converted)
913+
```
914+
915+
**Note:** All arithmetic operations and methods automatically convert string arguments to physical values when possible. For example, `Voltage("3.3V") + "2V"` works and returns `5.3V`.
916+
917+
**Tolerance Handling:**
918+
919+
- Multiplication/Division: Tolerance preserved only for dimensionless scaling (e.g., `2 * 3.3V±1%` keeps 1%)
920+
- Addition/Subtraction: Tolerance is always dropped
921+
- `.abs()`: Tolerance is preserved
922+
- `.diff()`: Tolerance is dropped (consistent with subtraction)
923+
924+
**Parsing Support:**
925+
926+
Physical value constructors accept flexible string formats:
927+
928+
```python
929+
# Basic format with units
930+
Voltage("3.3V")
931+
Current("100mA")
932+
933+
# SI prefixes: m, μ/u, n, p, k, M, G
934+
Capacitance("100nF")
935+
Resistance("4.7kOhm")
936+
937+
# Resistor notation (4k7 = 4.7kΩ)
938+
Resistance("4k7")
939+
940+
# Temperature conversions
941+
Temperature("25C") # Converts to Kelvin
942+
Temperature("77F") # Converts to Kelvin
943+
```
944+
818945
### builtin.physical_range(unit)
819946

820947
**Built-in function** that creates unit-specific range constructor types for defining bounded physical value ranges.

0 commit comments

Comments
 (0)