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
« 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
5class Parser:
6 """Abstract parser, the template for implementing other parsers."""
8 def __init__(self) -> None:
9 self.info = {
10 'version': '',
11 'label': '',
12 'simulator': '',
13 }
15 def parse_configs(self, json: dict) -> None:
16 """Convert the json dict to the 4 config dataclasses."""
17 raise NotImplementedError
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.')
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)
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
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.
53 length = n
55 # Adjust length for decimal separator ('.')
56 length -= 1
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
62 if number < 0:
63 result = abs(number)
64 sign = -1
65 # Adjust length for the sign
66 length -= 1
68 whole_length = ceil(log10(result))
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}')
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)
82 result = float(sign * round(result, length))
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}')
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.
93 return result
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.
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.
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
109 rad_angles = [radians(angle) for angle in angles] if degrees else angles
111 x, y, z = vector
112 new_x, new_y, new_z = 0, 0, 0
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])
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])
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
129 return [new_x3, new_y3, new_z3]