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
2 changes: 1 addition & 1 deletion VERSION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
# ========================================================================================

major = 0
minor = 2
minor = 3
patch = 0
2 changes: 1 addition & 1 deletion core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dv"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = [ "Alex Tacescu <alextac98@gmail.com>",]
description = "Core Rust library for DimensionalVariable, a multi-language library for handling physical quantities with units."
Expand Down
104 changes: 82 additions & 22 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,24 @@ impl DimensionalVariable {
.map_err(|e| format!("Failed to parse unit '{}': {}", unit_str, e))?;

// Check if the units are compatible
if self.unit != unit {
if !self.check_compatibility(unit) {
return Err(format!("Incompatible unit conversion to: {}", unit_str));
}

return Ok(self.value / conversion_factor);
}

pub fn check_compatibility(&self, other_unit: [f64; units::BASE_UNITS_SIZE]) -> bool {

// If the angle unit == 1, then allow compatibility to linear units
if self.unit[7] == 1.0 {
// Compare only the first 7 base units (ignore the angle unit)
return &self.unit[..units::BASE_UNITS_SIZE - 1] == &other_unit[..units::BASE_UNITS_SIZE - 1];
}

return self.unit == other_unit;
}

/// Returns the base unit array of this DimensionalVariable.
pub fn unit(&self) -> [f64; units::BASE_UNITS_SIZE] {
return self.unit;
Expand All @@ -62,15 +73,15 @@ impl DimensionalVariable {

/// Fallible add with unit compatibility check.
pub fn try_add(&self, other: &DimensionalVariable) -> Result<DimensionalVariable, String> {
if self.unit != other.unit {
if !self.check_compatibility(other.unit) {
return Err("Incompatible units for addition".to_string());
}
return Ok(DimensionalVariable { value: self.value + other.value, unit: self.unit });
}

/// Fallible subtraction with unit compatibility check.
pub fn try_sub(&self, other: &DimensionalVariable) -> Result<DimensionalVariable, String> {
if self.unit != other.unit {
if !self.check_compatibility(other.unit) {
return Err("Incompatible units for subtraction".to_string());
}
return Ok(DimensionalVariable { value: self.value - other.value, unit: self.unit });
Expand Down Expand Up @@ -136,30 +147,66 @@ impl DimensionalVariable {
self.unit[units::BASE_UNITS_SIZE - 1] == 1.0
}

/// Sine function. Requires angle (radians) or unitless.
/// Sine function. Requires angle (radians).
pub fn sin(&self) -> Result<f64, String> {
if !self.is_unitless() && !self.is_angle() {
return Err("sin requires an angle or unitless quantity".to_string());
if !self.is_angle() {
return Err("sin requires an angle quantity".to_string());
}
Ok(self.value.sin())
}

/// Cosine function. Requires angle (radians) or unitless.
/// Cosine function. Requires angle (radians).
pub fn cos(&self) -> Result<f64, String> {
if !self.is_unitless() && !self.is_angle() {
return Err("cos requires an angle or unitless quantity".to_string());
if !self.is_angle() {
return Err("cos requires an angle quantity".to_string());
}
Ok(self.value.cos())
}

/// Tangent function. Requires angle or unitless.
/// Tangent function. Requires angle (radians).
pub fn tan(&self) -> Result<f64, String> {
if !self.is_unitless() && !self.is_angle() {
return Err("tan requires an angle or unitless quantity".to_string());
if !self.is_angle() {
return Err("tan requires an angle quantity".to_string());
}
Ok(self.value.tan())
}

/// Arcsine function. Requires unitless value in [-1, 1]. Returns angle in radians.
pub fn asin(&self) -> Result<DimensionalVariable, String> {
if !self.is_unitless() {
return Err("asin requires a unitless quantity".to_string());
}
if self.value < -1.0 || self.value > 1.0 {
return Err("asin requires a value in the range [-1, 1]".to_string());
}
let mut unit = [0.0; units::BASE_UNITS_SIZE];
unit[units::BASE_UNITS_SIZE - 1] = 1.0; // radians
Ok(DimensionalVariable { value: self.value.asin(), unit })
}

/// Arccosine function. Requires unitless value in [-1, 1]. Returns angle in radians.
pub fn acos(&self) -> Result<DimensionalVariable, String> {
if !self.is_unitless() {
return Err("acos requires a unitless quantity".to_string());
}
if self.value < -1.0 || self.value > 1.0 {
return Err("acos requires a value in the range [-1, 1]".to_string());
}
let mut unit = [0.0; units::BASE_UNITS_SIZE];
unit[units::BASE_UNITS_SIZE - 1] = 1.0; // radians
Ok(DimensionalVariable { value: self.value.acos(), unit })
}

/// Arctangent function. Requires unitless value. Returns angle in radians.
pub fn atan(&self) -> Result<DimensionalVariable, String> {
if !self.is_unitless() {
return Err("atan requires a unitless quantity".to_string());
}
let mut unit = [0.0; units::BASE_UNITS_SIZE];
unit[units::BASE_UNITS_SIZE - 1] = 1.0; // radians
Ok(DimensionalVariable { value: self.value.atan(), unit })
}

// ---- Scalar helpers on single values ----
/// Negate the value, keeping the same unit.
pub fn neg(&self) -> DimensionalVariable {
Expand All @@ -173,6 +220,21 @@ impl DimensionalVariable {

}

/// Arcsin function for f64 input, returns DimensionalVariable in radians.
pub fn asin(x: f64) -> Result<DimensionalVariable, String> {
return DimensionalVariable::new(x, "").unwrap().asin();
}

/// Arccos function for f64 input, returns DimensionalVariable in radians.
pub fn acos(x: f64) -> Result<DimensionalVariable, String> {
return DimensionalVariable::new(x, "").unwrap().acos();
}

/// Arctan function for f64 input, returns DimensionalVariable in radians.
pub fn atan(x: f64) -> Result<DimensionalVariable, String> {
return DimensionalVariable::new(x, "").unwrap().atan();
}

/// Convert a unit string like "m/s^2" or "kg-m/s^2" into base unit exponents and a conversion factor.
/// Returns an error if the unit string is invalid or contains unknown units.
fn unit_str_to_base_unit(units_str: &str) -> Result<([f64; units::BASE_UNITS_SIZE], f64), String> {
Expand Down Expand Up @@ -304,8 +366,7 @@ use std::cmp::Ordering;
impl<'a, 'b> Add<&'b DimensionalVariable> for &'a DimensionalVariable {
type Output = DimensionalVariable;
fn add(self, rhs: &'b DimensionalVariable) -> Self::Output {
assert!(self.unit == rhs.unit, "Incompatible units for addition: {:?} vs {:?}", self.unit, rhs.unit);
DimensionalVariable { value: self.value + rhs.value, unit: self.unit }
return self.try_add(rhs).expect("Incompatible units for addition");
}
}

Expand Down Expand Up @@ -334,8 +395,7 @@ impl<'a> Add<DimensionalVariable> for &'a DimensionalVariable {
impl<'a, 'b> Sub<&'b DimensionalVariable> for &'a DimensionalVariable {
type Output = DimensionalVariable;
fn sub(self, rhs: &'b DimensionalVariable) -> Self::Output {
assert!(self.unit == rhs.unit, "Incompatible units for subtraction: {:?} vs {:?}", self.unit, rhs.unit);
DimensionalVariable { value: self.value - rhs.value, unit: self.unit }
return self.try_sub(rhs).expect("Incompatible units for subtraction");
}
}

Expand Down Expand Up @@ -419,15 +479,13 @@ impl<'a> Div<DimensionalVariable> for &'a DimensionalVariable {
// Assignment ops: implement only for &DimensionalVariable RHS. Owned RHS will autoref.
impl AddAssign<&DimensionalVariable> for DimensionalVariable {
fn add_assign(&mut self, rhs: &DimensionalVariable) {
assert!(self.unit == rhs.unit, "Incompatible units for addition assignment: {:?} vs {:?}", self.unit, rhs.unit);
self.value += rhs.value;
*self = self.try_add(rhs).expect("Incompatible units for addition assignment");
}
}

impl SubAssign<&DimensionalVariable> for DimensionalVariable {
fn sub_assign(&mut self, rhs: &DimensionalVariable) {
assert!(self.unit == rhs.unit, "Incompatible units for subtraction assignment: {:?} vs {:?}", self.unit, rhs.unit);
self.value -= rhs.value;
*self = self.try_sub(rhs).expect("Incompatible units for subtraction assignment");
}
}

Expand Down Expand Up @@ -533,14 +591,16 @@ impl Neg for DimensionalVariable {
// ---- Comparisons: equalities and ordering ----
impl PartialEq for DimensionalVariable {
fn eq(&self, other: &Self) -> bool {
if self.unit != other.unit { return false; }
if !self.check_compatibility(other.unit) {
return false;
}
self.value == other.value
}
}

impl PartialOrd for DimensionalVariable {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.unit != other.unit { return None; }
if !self.check_compatibility(other.unit) { return None; }
self.value.partial_cmp(&other.value)
}
}
Expand Down
55 changes: 55 additions & 0 deletions core/tests/operator_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dv_rs::DimensionalVariable as dv;
use dv_rs::{asin, acos, atan};

const FAIL_MSG: &str = "Failed to create DimensionalVariable";

Expand Down Expand Up @@ -420,4 +421,58 @@ fn absolute_value() {
let a = m.abs();
assert_eq!(a.value(), 5.0);
assert_eq!(a.unit(), [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
}

#[test]
fn inverse_trig_functions() {
use std::f64::consts::PI;

// asin on unitless value
let half = dv::new(0.5, "").expect(FAIL_MSG);
let angle = half.asin().expect(FAIL_MSG);
assert!((angle.value() - 0.5_f64.asin()).abs() < 1e-12);
assert_eq!(angle.unit(), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]); // radians

assert!(asin(half.value()).unwrap().value() - angle.value() < 1e-12);

// acos on unitless value
let angle2 = half.acos().expect(FAIL_MSG);
assert!((angle2.value() - 0.5_f64.acos()).abs() < 1e-12);
assert_eq!(angle2.unit(), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]);

assert!(acos(half.value()).unwrap().value() - angle2.value() < 1e-12);

// atan on unitless value
let one = dv::new(1.0, "").expect(FAIL_MSG);
let angle3 = one.atan().expect(FAIL_MSG);
assert!((angle3.value() - PI / 4.0).abs() < 1e-12);
assert_eq!(angle3.unit(), [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]);

assert!(atan(one.value()).unwrap().value() - angle3.value() < 1e-12);

// asin/acos edge cases at -1 and 1
let neg_one = dv::new(-1.0, "").expect(FAIL_MSG);
let pos_one = dv::new(1.0, "").expect(FAIL_MSG);
assert!((neg_one.asin().unwrap().value() - (-PI / 2.0)).abs() < 1e-12);
assert!((pos_one.asin().unwrap().value() - (PI / 2.0)).abs() < 1e-12);
assert!((neg_one.acos().unwrap().value() - PI).abs() < 1e-12);
assert!((pos_one.acos().unwrap().value() - 0.0).abs() < 1e-12);

// Round-trip: sin(asin(x)) == x
let val = dv::new(0.7, "").expect(FAIL_MSG);
let angle4 = val.asin().expect(FAIL_MSG);
assert!((angle4.sin().unwrap() - 0.7).abs() < 1e-12);

// Error: asin/acos on non-unitless
let m = dv::new(0.5, "m").expect(FAIL_MSG);
assert!(m.asin().is_err());
assert!(m.acos().is_err());
assert!(m.atan().is_err());

// Error: asin/acos out of range
let out_of_range = dv::new(2.0, "").expect(FAIL_MSG);
assert!(out_of_range.asin().is_err());
assert!(out_of_range.acos().is_err());
// atan has no domain restriction
assert!(out_of_range.atan().is_ok());
}
12 changes: 12 additions & 0 deletions core/tests/units_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ fn angle_conversions() {
assert!((arcsec.value_in("deg").expect(FAIL_MSG) - 1.0).abs() < 1e-10);
}

#[test]
fn angle_to_linear_conversions() {
let torque = dv::new(10.0, "N-m").expect(FAIL_MSG);
let angular_speed = dv::new(2.0, "rad/s").expect(FAIL_MSG);

let power = torque * angular_speed;

// Power = Torque * Angular Speed
assert_eq!(power.unit(), [2.0, 1.0, -3.0, 0.0, 0.0, 0.0, 0.0, 1.0], "Incorrect unit for angular power.");
assert!((power.value_in("W").expect(FAIL_MSG) - 20.0).abs() < 1e-10);
}

#[test]
fn unit_str_to_unit_simple_simple() {
let dv = dv::new(4.6483, "m/s").expect(FAIL_MSG);
Expand Down
56 changes: 56 additions & 0 deletions cpp/capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,59 @@ pub extern "C" fn dv_var_sqrt(a: *const dv_var) -> *mut dv_var {
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

#[no_mangle]
pub extern "C" fn dv_var_asin(a: *const dv_var) -> *mut dv_var {
if a.is_null() { set_last_error("null operand".to_string()); return ptr::null_mut(); }
let a = unsafe { &(*a) };
match a.inner.asin() {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

#[no_mangle]
pub extern "C" fn dv_var_acos(a: *const dv_var) -> *mut dv_var {
if a.is_null() { set_last_error("null operand".to_string()); return ptr::null_mut(); }
let a = unsafe { &(*a) };
match a.inner.acos() {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

#[no_mangle]
pub extern "C" fn dv_var_atan(a: *const dv_var) -> *mut dv_var {
if a.is_null() { set_last_error("null operand".to_string()); return ptr::null_mut(); }
let a = unsafe { &(*a) };
match a.inner.atan() {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

// Free-standing trigonometric functions that take raw f64 and return angle in radians

#[no_mangle]
pub extern "C" fn dv_asin(x: c_double) -> *mut dv_var {
match dv_rs::asin(x) {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

#[no_mangle]
pub extern "C" fn dv_acos(x: c_double) -> *mut dv_var {
match dv_rs::acos(x) {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}

#[no_mangle]
pub extern "C" fn dv_atan(x: c_double) -> *mut dv_var {
match dv_rs::atan(x) {
Ok(v) => Box::into_raw(Box::new(dv_var { inner: v })),
Err(e) => { set_last_error(e); ptr::null_mut() }
}
}
14 changes: 14 additions & 0 deletions cpp/include/dv.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,29 @@ class DV {
DV powf(double e) const { return from_new(dv_var_powf(cptr(), e)); }
DV sqrt() const { return from_new(dv_var_sqrt(cptr())); }

// Inverse trigonometric functions (return angle in radians)
DV asin() const { return from_new(dv_var_asin(cptr())); }
DV acos() const { return from_new(dv_var_acos(cptr())); }
DV atan() const { return from_new(dv_var_atan(cptr())); }

private:
static DV from_new(dv_var* p) {
if (!p) throw std::runtime_error(last_error());
DV v; v.ptr_ = p; return v;
}

dv_var* ptr_;

friend DV asin(double x);
friend DV acos(double x);
friend DV atan(double x);
};

inline size_t base_units_size() { return dv_base_units_size(); }

// Free-standing inverse trigonometric functions (take raw double, return angle in radians)
inline DV asin(double x) { return DV::from_new(dv_asin(x)); }
inline DV acos(double x) { return DV::from_new(dv_acos(x)); }
inline DV atan(double x) { return DV::from_new(dv_atan(x)); }

} // namespace dv
Loading