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

1"""Offset data contains the Z-offset calibration data.""" 

2from __future__ import annotations 

3 

4import logging 

5import math 

6from dataclasses import dataclass, field 

7from functools import cached_property 

8 

9from klipper_utils.klipper_utils import KlipperUtils 

10from zooc.dsp.surface_extrapolator import SurfaceExtrapolator, create 

11 

12from .offset_temp import OffsetTemp 

13from .temps import Temps 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18@dataclass(frozen=True) 

19class OffsetData: 

20 """Collection of Z-offsets (OffsetTemp) at certain temperatures with interpolation functions. 

21 

22 :param offset_temps: List of OffsetTemp instances containing bed and extruder temperatures and z-offsets measurement. 

23 """ 

24 

25 @staticmethod 

26 def build_text(list_lines: list[str]) -> OffsetData: 

27 """Create OffsetData from the list of key-value pairs. 

28 

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) 

42 

43 offset_temps: list[OffsetTemp] = field(default_factory=list) 

44 

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)) 

48 

49 def is_valid(self) -> bool: 

50 """Check whether the data is valid. 

51 

52 :return: True when valid. 

53 """ 

54 return len(self.offset_temps) > 0 

55 

56 def get_z_ref(self, temps: Temps) -> float: 

57 """Get the absolute z-offset for the given temperatures. 

58 

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 

68 

69 def calculate_z_offset(self, temps_ref: Temps, temps: Temps) -> float: 

70 """Calculate relative Z-offset to reference point. 

71 

72 Positive value indicates the nozzle should be lifted to keep the distance to bed constant at given temperature. 

73 

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 

80 

81 @cached_property 

82 def get_interp(self) -> SurfaceExtrapolator: 

83 """Create and get interpolator for the calibration data. 

84 

85 :return: Interpolator function. 

86 """ 

87 return create(self.list_be, self.list_z) 

88 

89 def to_string(self) -> str: 

90 r"""Convert offset data to key-value multiline string. 

91 

92 See :py:meth:`.offset_temp.OffsetTemp.to_string` 

93 

94 :return: 

95 Lines of:: 

96 

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]) 

102 

103 @cached_property 

104 def list_z(self) -> list[float]: 

105 """Get the list of z-offsets. 

106 

107 :return: Entries as a list of z-offsets. 

108 """ 

109 return [lc.z_offset for lc in self.offset_temps] 

110 

111 @cached_property 

112 def list_be(self) -> list[tuple[float, float]]: 

113 """Get the list of bed and extruder temperatures. 

114 

115 :return: Bed and extruder temperatures as list. 

116 """ 

117 return [(lc.bed_temp, lc.extruder_temp) for lc in self.offset_temps] 

118 

119 def __str__(self) -> str: 

120 """Get data values in a user-friendly format. 

121 

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