Coverage for src / zooc / data / offset_data.py: 91%
49 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 21:45 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 21:45 +0000
1"""Offset data contains the Z-offset calibration data."""
2from __future__ import annotations
4import logging
5import math
6from dataclasses import dataclass, field
7from functools import cached_property
9from klipper_utils.klipper_utils import KlipperUtils
10from zooc.dsp.surface_extrapolator import SurfaceExtrapolator, create
12from .offset_temp import OffsetTemp
13from .temps import Temps
15logger = logging.getLogger(__name__)
18@dataclass(frozen=True)
19class OffsetData:
20 """Collection of Z-offsets (OffsetTemp) at certain temperatures with interpolation functions.
22 :param offset_temps: List of OffsetTemp instances containing bed and extruder temperatures and z-offsets measurement.
23 """
25 @staticmethod
26 def build_text(list_lines: list[str]) -> OffsetData:
27 """Create OffsetData from the list of key-value pairs.
29 :param list_lines: List of key-value pairs: bed_temp=<bed_temp>, extruder_temp=<extruder_temp>, z_offset=<z_offset>.
30 :return: OffsetData parsed from list_lines.
31 """
32 if list_lines is None: 32 ↛ 33line 32 didn't jump to line 33 because the condition on line 32 was never true
33 return OffsetData()
34 list_offset_temps: list[OffsetTemp] = []
35 # Parse each entry string to a Calib instance
36 for entry in list_lines:
37 if entry:
38 # Add the calibration result as a string of bed and extruder temp and z-offset value.
39 # entry: Calibration result as bed_temp=<bed_temp>, extruder_temp=<extruder_temp>, z_offset=<z_offset>
40 list_offset_temps.append(KlipperUtils.klipper_from_config_string(class_entry=OffsetTemp, string=entry))
41 return OffsetData(list_offset_temps)
43 offset_temps: list[OffsetTemp] = field(default_factory=list)
45 def __post_init__(self) -> None:
46 """Sort the list of calibration data by bed and extruder temperatures."""
47 self.offset_temps.sort(key=lambda x: (x.bed_temp, x.extruder_temp))
49 def is_valid(self) -> bool:
50 """Check whether the data is valid.
52 :return: True when valid.
53 """
54 return len(self.offset_temps) > 0
56 def get_z_ref(self, temps: Temps) -> float:
57 """Get the absolute z-offset for the given temperatures.
59 :param temps: The temperatures.
60 :return: Absolute Z-offset at the temperature <temps>.
61 :raises ValueError: If the z-offset is not defined for the given temperatures.
62 """
63 # Check the z-ref is within calibration points
64 z_ref = float(self.get_interp(temps.bed_temp, temps.extruder_temp))
65 if math.isnan(z_ref): 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 raise ValueError(f"Z-offset at {temps} are outside of the temperature range")
67 return z_ref
69 def calculate_z_offset(self, temps_ref: Temps, temps: Temps) -> float:
70 """Calculate relative Z-offset to reference point.
72 Positive value indicates the nozzle should be lifted to keep the distance to bed constant at given temperature.
74 :param temps_ref: Reference temperatures where relative Z-offset is zero.
75 :param temps: Temperatures to calculate the difference to temps_ref.
76 :return: Relative Z-offset from the reference point.
77 """
78 z_ref = self.get_z_ref(temps_ref)
79 return float(self.get_interp(temps.bed_temp, temps.extruder_temp)) - z_ref
81 @cached_property
82 def get_interp(self) -> SurfaceExtrapolator:
83 """Create and get interpolator for the calibration data.
85 :return: Interpolator function.
86 """
87 return create(self.list_be, self.list_z)
89 def to_string(self) -> str:
90 r"""Convert offset data to key-value multiline string.
92 See :py:meth:`.offset_temp.OffsetTemp.to_string`
94 :return:
95 Lines of::
97 <key_1a>=<value_1a>, <key_1b>=<value_1b>, ...
98 <key_2a>=<value_2a>, <key_2b>=<value_2b>, ...
99 ...
100 """
101 return '\n'.join([lc.to_string() for lc in self.offset_temps])
103 @cached_property
104 def list_z(self) -> list[float]:
105 """Get the list of z-offsets.
107 :return: Entries as a list of z-offsets.
108 """
109 return [lc.z_offset for lc in self.offset_temps]
111 @cached_property
112 def list_be(self) -> list[tuple[float, float]]:
113 """Get the list of bed and extruder temperatures.
115 :return: Bed and extruder temperatures as list.
116 """
117 return [(lc.bed_temp, lc.extruder_temp) for lc in self.offset_temps]
119 def __str__(self) -> str:
120 """Get data values in a user-friendly format.
122 :return: Multiline string with tab-separated values: <bed temp> <extruder temp> <z-offset>.
123 """
124 title = '\t'.join(OffsetTemp.get_title())
125 data = '\n'.join(['\t'.join([f"{value:.3f}" for value in lc.get_data().values()]) for lc in self.offset_temps])
126 return title + '\n' + data