Coverage for yaptide/converter/converter/common.py: 86%

51 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-07-01 12:55 +0000

1from pathlib import Path 

2from math import log10, ceil, isclose, sin, cos, radians 

3 

4 

5class Parser: 

6 """Abstract parser, the template for implementing other parsers.""" 

7 

8 def __init__(self) -> None: 

9 self.info = { 

10 'version': '', 

11 'label': '', 

12 'simulator': '', 

13 } 

14 

15 def parse_configs(self, json: dict) -> None: 

16 """Convert the json dict to the 4 config dataclasses.""" 

17 raise NotImplementedError 

18 

19 def save_configs(self, target_dir: str): 

20 """ 

21 Save the configs as text files in the target_dir. 

22 The files are: beam.dat, mat.dat, detect.dat and geo.dat. 

23 """ 

24 if not Path(target_dir).exists(): 

25 raise ValueError('Target directory does not exist.') 

26 

27 for file_name, content in self.get_configs_json().items(): 

28 with open(Path(target_dir, file_name), 'w') as conf_f: 

29 conf_f.write(content) 

30 

31 def get_configs_json(self) -> dict: 

32 """ 

33 Return a dict representation of the config files. Each element has 

34 the config files name as key and its content as value. 

35 """ 

36 configs_json = { 

37 'info.json': str(self.info), 

38 } 

39 return configs_json 

40 

41 

42def format_float(number: float, n: int) -> float: 

43 """ 

44 Format float to be up to n characters wide, as precise as possible and as short 

45 as possible (in descending priority). so for example given 12.333 for n=5 you will 

46 get 12.33, n=7 will be 12.333 

47 """ 

48 result = number 

49 # If number is zero we just want to get 0.0 (it would mess up the log10 operation below) 

50 if isclose(result, 0., rel_tol=1e-9): 

51 return 0. 

52 

53 length = n 

54 

55 # Adjust length for decimal separator ('.') 

56 length -= 1 

57 

58 # Sign messes up the log10 we use do determine how long the number is. We use 

59 # abs() to fix that, but we need to remember the sign and update `n` accordingly 

60 sign = 1 

61 

62 if number < 0: 

63 result = abs(number) 

64 sign = -1 

65 # Adjust length for the sign 

66 length -= 1 

67 

68 whole_length = ceil(log10(result)) 

69 

70 # Check if it will be possible to fit the number 

71 if whole_length > length - 1: 

72 raise ValueError(f'Number is to big to be formatted. Minimum length: {whole_length-sign+1} \ 

73 ,\ 

74requested length: {n}') 

75 

76 # Adjust n for the whole numbers, log returns reasonable outputs for values greater 

77 # than 1, for other values it returns nonpositive numbers, but we would like 1 

78 # to be returned. We solve that by taking the greater value between the returned and 

79 # and 1. 

80 length -= max(whole_length, 1) 

81 

82 result = float(sign * round(result, length)) 

83 

84 # Check if the round function truncated the number, warn the user if it did. 

85 if not isclose(result, number): 

86 print(f'WARN: number was truncated when converting: {number} -> {result}') 

87 

88 # Formatting negative numbers smaller than the desired precision could result in -0.0 or 0.0 randomly. 

89 # To avoid this we catch -0.0 and return 0.0. 

90 if isclose(result, 0., rel_tol=1e-9): 

91 return 0. 

92 

93 return result 

94 

95 

96def rotate(vector: list[float], angles: list[float], degrees: bool = True) -> list[float]: 

97 """ 

98 Rotate a vector in 3D around XYZ axes, assuming Euler angles. 

99 

100 Proper Euler angles uses z-x-z, x-y-x, y-z-y, z-y-z, x-z-x, y-x-y axis sequences, here we 

101 stick to other convention called Tait-Bryan angles. First we rotate vector around X axis by first 

102 angle, then around Y axis and finally around Z axis, The individual rotations are 

103 usually known as yaw, pitch and roll. 

104 

105 If degrees is True, then the given angle are assumed to be in degrees. Otherwise radians are used. 

106 """ 

107 # Convert angles to radians if degrees is True 

108 

109 rad_angles = [radians(angle) for angle in angles] if degrees else angles 

110 

111 x, y, z = vector 

112 new_x, new_y, new_z = 0, 0, 0 

113 

114 # Rotation around x-axis 

115 new_x = x 

116 new_y = y * cos(rad_angles[0]) - z * sin(rad_angles[0]) 

117 new_z = y * sin(rad_angles[0]) + z * cos(rad_angles[0]) 

118 

119 # Rotation around y-axis 

120 new_x2 = new_x * cos(rad_angles[1]) + new_z * sin(rad_angles[1]) 

121 new_y2 = new_y 

122 new_z2 = -new_x * sin(rad_angles[1]) + new_z * cos(rad_angles[1]) 

123 

124 # Rotation around z-axis 

125 new_x3 = new_x2 * cos(rad_angles[2]) - new_y2 * sin(rad_angles[2]) 

126 new_y3 = new_x2 * sin(rad_angles[2]) + new_y2 * cos(rad_angles[2]) 

127 new_z3 = new_z2 

128 

129 return [new_x3, new_y3, new_z3]