Skip to content
216 changes: 216 additions & 0 deletions bindings/Sofa/package/Units/Core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import math

class Unit():
numerator : list
denumerator : list
ratio : float


def getKey(self):
key = {"num" : {}, "denum" : {}}
for unit in self.numerator:
if unit.abrev in key["num"]:
key["num"][unit.abrev] += 1
else:
key["num"][unit.abrev] = 1
for unit in self.denumerator:
if unit.abrev in key["denum"]:
key["denum"][unit.abrev] += 1
else:
key["denum"][unit.abrev] = 1
return key

def __eq__ (self, other):
if not isinstance(other, Unit):
return NotImplemented

if int(math.log10(self.ratio)) != int(math.log10(other.ratio)) :
return False

return self.getKey() == other.getKey()


def __mul__(self, other):
if isinstance(other, Unit):
return DerivedUnit(numerator=self.numerator + other.numerator, denumerator= self.denumerator + other.denumerator, ratio = self.ratio * other.ratio)
else:
return DimensionnedValue(other,self )


def __rmul__(self, other ):
return self.__mul__(other)

def __pow__(self, other : int):
if not isinstance(other, int):
raise ValueError

targetNum = []
targetDenum = []
targetRatio = 1.0

for i in range(abs(other)):
targetNum += self.numerator
targetDenum += self.denumerator
targetRatio *= self.ratio

if other < 0 :
return DerivedUnit(numerator=targetDenum, denumerator= targetNum, ratio = 1.0/targetRatio)
elif other > 0 :
return DerivedUnit(numerator=targetNum, denumerator= targetDenum, ratio = targetRatio)
else:
return NeutralUnit()

def __truediv__(self, other ):
if not isinstance(other, Unit):
return NotImplemented

return DerivedUnit(numerator=self.numerator + other.denumerator, denumerator= self.denumerator + other.numerator, ratio = self.ratio / other.ratio)

def toString(self, addRatio : bool = True):
self_key = self.getKey()

def side(units: dict) -> str:
return " * ".join(
k if exp == 1 else f"{k}^{exp}"
for k, exp in units.items()
)

num = side(self_key["num"])
denum = side(self_key["denum"])

num_s = f"( {num} ) " if num else "1"
denum_s = f"/ ( {denum} )" if denum else ""

prefix = f"{self.ratio} * " if addRatio else ""
return prefix + num_s + denum_s


def __str__(self):
return self.toString()

def __hash__(self):
key = self.getKey()
return hash((
frozenset(key["num"].items()),
frozenset(key["denum"].items()),
int(math.log10(self.ratio)),
))

class NeutralUnit(Unit):
def __init__(self):
self.numerator = []
self.denumerator = []
self.ratio = 1.0

def __str__(self):
return "1"


class PrimaryUnit(Unit):

abrev = str

def __init__(self, abrev : str):
self.abrev = abrev
self.numerator = [self]
self.denumerator = []
self.ratio = 1.0



class DerivedUnit(Unit):

def __init__(self, numerator : list[PrimaryUnit], denumerator : list[PrimaryUnit], ratio : float):
self.numerator = numerator
self.denumerator = denumerator
self.ratio = ratio

self.simplify()


def simplify(self):
futNum = []
for unit in self.numerator:
simplified = False
for i in range(len(self.denumerator)):
if self.denumerator[i].abrev == unit.abrev:
simplified = True
self.denumerator.pop(i)
break
if not(simplified):
futNum.append(unit)
self.numerator = futNum


class ScaledUnit(Unit):

def __init__(self, unit : Unit, ratio : float):
self.numerator = unit.numerator.copy()
self.denumerator = unit.denumerator.copy()
self.ratio = ratio


class DimensionnedValue():

value : float
unit : Unit

def __init__(self, value : float, unit : Unit):
self.value = value
self.unit = unit

def __eq__ (self, other):
if not isinstance(other, DimensionnedValue):
raise TypeError("Dimensionned values can only be compared to other dimensionned values")


if self.unit.getKey() != other.unit.getKey():
raise TypeError("Only values that share the same units can be compared")

return math.isclose(self.value * self.unit.ratio, other.value * other.unit.ratio)


def __mul__(self, other):
if isinstance(other, DimensionnedValue):
return DimensionnedValue(self.value * other.value,self.unit * other.unit)
elif isinstance(other, Unit) :
return DimensionnedValue(self.value ,self.unit * other)
else :
return DimensionnedValue(self.value * other,self.unit)

def __rmul__(self, other ):
return self.__mul__(other)

def __pow__(self, other : int):
if not isinstance(other, int):
raise ValueError

return DimensionnedValue(self.value ** other, self.unit**other)

def __truediv__(self, other):
if isinstance(other, DimensionnedValue):
return DimensionnedValue(self.value / other.value, self.unit / other.unit)
elif isinstance(other, Unit) :
return DimensionnedValue(self.value, self.unit / other)
else:
return DimensionnedValue(self.value / other, self.unit)

def __rtruediv__(self, other):
if isinstance(other, DimensionnedValue):
return DimensionnedValue(other.value / self.value, other.unit / self.unit)
elif isinstance(other, Unit):
return DimensionnedValue(1.0 / self.value, other / self.unit)
else:
return DimensionnedValue(other / self.value, self.unit ** -1)

def __str__(self):
return f"{self.value * self.unit.ratio} * " + self.unit.toString(False)

def __hash__(self):
key = self.unit.getKey()
return hash((
frozenset(key["num"].items()),
frozenset(key["denum"].items()),
round(self.value * self.unit.ratio, 9) # normalized magnitude
))

79 changes: 79 additions & 0 deletions bindings/Sofa/package/Units/Definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from .Core import *


### Primary units
DimensionLess = NeutralUnit()
s = PrimaryUnit("s") # time
m = PrimaryUnit("m") # length
kg = PrimaryUnit("kg") # mass
A = PrimaryUnit("A") # electric current
K = PrimaryUnit("K") # temperature
mol = PrimaryUnit("mol") # amount of substance
cd = PrimaryUnit("cd") # luminous intensity


### (some) Derived units
v = m/s # velocity
a = v/s # acceleration
N = kg*a # force (Newton)
Pa = N/(m**2) # pressure (Pascal)
tho = m*N # torque
J = kg*m**2/s**2 # energy (Joule)
W = J/s # power (Watt)


## Scaled primary units
nm = ScaledUnit(m, 1e-9)
µm = ScaledUnit(m, 1e-6)
mm = ScaledUnit(m, 1e-3)
cm = ScaledUnit(m, 1e-2)
dm = ScaledUnit(m, 1e-1)
km = ScaledUnit(m, 1e3)

ns = ScaledUnit(s, 1e-9)
µs = ScaledUnit(s, 1e-6)
ms = ScaledUnit(s, 1e-3)

µg = ScaledUnit(kg, 1e-9)
mg = ScaledUnit(kg, 1e-6)
g = ScaledUnit(kg, 1e-3)
t = ScaledUnit(kg, 1e3)

## Scaled derived units
nN = ScaledUnit(N, 1e-9)
µN = ScaledUnit(N, 1e-6)
mN = ScaledUnit(N, 1e-3)
cN = ScaledUnit(N, 1e-2)
dN = ScaledUnit(N, 1e-1)
kN = ScaledUnit(N, 1e3)
MN = ScaledUnit(N, 1e6)
GN = ScaledUnit(N, 1e9)


nPa = ScaledUnit(Pa, 1e-9)
µPa = ScaledUnit(Pa, 1e-6)
mPa = ScaledUnit(Pa, 1e-3)
cPa = ScaledUnit(Pa, 1e-2)
dPa = ScaledUnit(Pa, 1e-1)
kPa = ScaledUnit(Pa, 1e3)
MPa = ScaledUnit(Pa, 1e6)
GPa = ScaledUnit(Pa, 1e9)

mJ = ScaledUnit(J, 1e-3)
cJ = ScaledUnit(J, 1e-2)
dJ = ScaledUnit(J, 1e-1)
kJ = ScaledUnit(J, 1e3)
MJ = ScaledUnit(J, 1e6)
GJ = ScaledUnit(J, 1e9)

mW = ScaledUnit(W, 1e-3)
cW = ScaledUnit(W, 1e-2)
dW = ScaledUnit(W, 1e-1)
kW = ScaledUnit(W, 1e3)
MW = ScaledUnit(W, 1e6)
GW = ScaledUnit(W, 1e9)





87 changes: 87 additions & 0 deletions bindings/Sofa/package/Units/SimulationParameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from .Core import *
from .Definitions import DimensionLess, s, m, kg
import numpy as np

class BaseParameterSet():

units : dict

def __init__(self, *args, **kwargs):
self.units = {}
for arg in args:
self.setPrimaryUnit(arg)
for arg in kwargs:
self.setPrimaryUnit(kwargs[arg])

def setPrimaryUnit(self, unit):
if isinstance(unit, Unit):
if len(unit.numerator) == 1 and len(unit.denumerator) == 0:
if unit.numerator[0].abrev not in self.units:
self.units[unit.numerator[0].abrev] = unit
else:
raise ValueError("Only one primary unit of each type can be defined")
else:
raise TypeError("Only primary unit (with an optionnal ratio) can be defined by the user.")

def convert(self, value : float, unit: DerivedUnit):
u_key = unit.getKey()

reconstructedUnit = DimensionLess
for nkey in u_key["num"]:
try:
for _ in range(u_key["num"][nkey]):
reconstructedUnit *= self.units[nkey]
except:
raise RuntimeError(f"The unit {nkey} is not defined in the parameter set.")
for nkey in u_key["denum"]:
try:
for _ in range(u_key["denum"][nkey]):
reconstructedUnit /= self.units[nkey]
except:
raise RuntimeError(f"The unit {nkey} is not defined in the parameter set.")


return unit.ratio / reconstructedUnit.ratio * value


def __call__(self, *args):
if len(args) == 1:
if isinstance(args[0], np.ndarray):
convertedArray = np.empty(args[0].shape, dtype=np.float32)
for i in range(convertedArray.size):
convertedArray.flat[i] = self.convert(value=args[0].flat[i].value, unit= args[0].flat[i].unit)
return convertedArray
elif isinstance(args[0], list):
retList = [None] * len(args[0])
for i in range(len(retList)):
retList[i] = self.__call__(args[0][i])
return retList
else:
return self.convert(value=args[0].value, unit= args[0].unit)
elif len(args) == 2:
if isinstance(args[0], np.ndarray):
convertedArray = np.empty(args[0].shape, dtype=np.float32)
for i in range(convertedArray.size):
convertedArray.flat[i] = self.convert(value=args[0].flat[i], unit = args[1])
return convertedArray
elif isinstance(args[0], list):
retList = [None] * len(args[0])
for i in range(len(retList)):
retList[i] = self.__call__(args[0][i], args[1])
return retList

else:
return self.convert(value=args[0], unit= args[1])
else:
raise ValueError("This method requires either a DimensionnedValue as input or a float and a Unit.")






class SOFAParameters(BaseParameterSet):
def __init__(self, time = s, length = m, mass = kg ):
BaseParameterSet.__init__(self, time, length, mass)


1 change: 1 addition & 0 deletions bindings/Sofa/package/Units/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__=["Core", "Definitions", "SimulationParameters"]
Loading
Loading