init
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
DEFAULT_FORCE_CALIBRATION_POINTS = [
|
||||
{"raw": 1000.0, "force_n": 0.0},
|
||||
{"raw": 10000.0, "force_n": 0.377},
|
||||
{"raw": 30000.0, "force_n": 1.377},
|
||||
{"raw": 62000.0, "force_n": 2.377},
|
||||
]
|
||||
|
||||
DEFAULT_NORMAL_FORCE_CALIBRATION = {
|
||||
"enabled": True,
|
||||
"method": "piecewise_linear",
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": [
|
||||
{"fnormal": 1000.0, "force_n": 0.0},
|
||||
{"fnormal": 10000.0, "force_n": 0.377},
|
||||
{"fnormal": 30000.0, "force_n": 1.377},
|
||||
{"fnormal": 62000.0, "force_n": 2.377},
|
||||
],
|
||||
}
|
||||
|
||||
DEFAULT_SHEAR_FORCE_CALIBRATION = {
|
||||
"enabled": True,
|
||||
"method": "piecewise_linear",
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
"points": DEFAULT_FORCE_CALIBRATION_POINTS,
|
||||
}
|
||||
|
||||
|
||||
def parse_calibration_point(point):
|
||||
if isinstance(point, dict):
|
||||
raw = point.get("fnormal", point.get("raw", point.get("x")))
|
||||
force_n = point.get("force_n", point.get("n", point.get("y")))
|
||||
return float(raw), float(force_n)
|
||||
if isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||
return float(point[0]), float(point[1])
|
||||
raise ValueError(f"invalid calibration point: {point!r}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ForceConverter:
|
||||
points: tuple
|
||||
unit: str = "N"
|
||||
enabled: bool = True
|
||||
extrapolate: bool = True
|
||||
clamp_output_min: float | None = 0.0
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, calibration, config_name):
|
||||
if calibration is None:
|
||||
calibration = DEFAULT_NORMAL_FORCE_CALIBRATION
|
||||
if not calibration.get("enabled", True):
|
||||
return cls(points=(), unit="raw", enabled=False)
|
||||
|
||||
method = str(calibration.get("method", "piecewise_linear")).lower()
|
||||
if method != "piecewise_linear":
|
||||
raise ValueError(f"unsupported {config_name} method: {method}")
|
||||
|
||||
points = tuple(sorted(
|
||||
parse_calibration_point(point)
|
||||
for point in calibration.get("points", [])
|
||||
))
|
||||
if len(points) < 2:
|
||||
raise ValueError(f"{config_name}.points must contain at least two points")
|
||||
|
||||
return cls(
|
||||
points=points,
|
||||
enabled=True,
|
||||
extrapolate=bool(calibration.get("extrapolate", True)),
|
||||
clamp_output_min=calibration.get("clamp_output_min", 0.0),
|
||||
)
|
||||
|
||||
def convert(self, raw):
|
||||
if not self.enabled:
|
||||
return float(raw)
|
||||
|
||||
raw = float(raw)
|
||||
points = self.points
|
||||
if raw <= points[0][0]:
|
||||
segment = (points[0], points[1])
|
||||
if not self.extrapolate:
|
||||
return self._clamp(points[0][1])
|
||||
elif raw >= points[-1][0]:
|
||||
segment = (points[-2], points[-1])
|
||||
if not self.extrapolate:
|
||||
return self._clamp(points[-1][1])
|
||||
else:
|
||||
segment = None
|
||||
for left, right in zip(points, points[1:]):
|
||||
if left[0] <= raw <= right[0]:
|
||||
segment = (left, right)
|
||||
break
|
||||
if segment is None:
|
||||
return self._clamp(points[-1][1])
|
||||
|
||||
(raw0, n0), (raw1, n1) = segment
|
||||
if raw1 == raw0:
|
||||
value = n0
|
||||
else:
|
||||
ratio = (raw - raw0) / (raw1 - raw0)
|
||||
value = n0 + ratio * (n1 - n0)
|
||||
return self._clamp(value)
|
||||
|
||||
def _clamp(self, value):
|
||||
if self.clamp_output_min is not None:
|
||||
value = max(float(self.clamp_output_min), value)
|
||||
return value
|
||||
|
||||
Reference in New Issue
Block a user