Coverage for kwave/utils/data.py: 13%
96 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
« prev ^ index » next coverage.py v7.7.1, created at 2025-03-24 12:06 -0700
1from datetime import datetime
2from math import floor
4import numpy as np
5from beartype import beartype as typechecker
6from beartype.typing import Optional, Tuple, Union
8from kwave.data import Vector
9from kwave.utils.typing import NUMERIC_WITH_COMPLEX
12@typechecker
13def get_smallest_possible_type(
14 max_array_val: Union[NUMERIC_WITH_COMPLEX, Vector], target_type_group: str, default: Optional[str] = None
15) -> Union[str, None]:
16 """
17 Returns the smallest possible type for the given array.
18 Args:
19 max_array_val: The maximum value in the array.
20 target_type_group: The type group to search for the smallest possible type.
21 default: The default type to return if no type is found.
23 Returns:
24 The smallest possible type for the given array.
26 """
28 types = {"uint", "int"}
29 assert target_type_group in types
31 for bit_count in [8, 16, 32]:
32 type_ = f"{target_type_group}{bit_count}"
33 if max_array_val < intmax(type_):
34 return type_
36 type_ = default
37 return type_
40@typechecker
41def intmax(dtype: str) -> int:
42 """
43 Returns the maximum value for the given integer type.
45 Args:
46 dtype: The integer type.
48 Returns
49 The maximum value for the given integer type.
51 """
53 return np.iinfo(getattr(np, dtype)).max
56@typechecker
57def scale_time(seconds: Union[int, float]) -> str:
58 """
59 Converts an integer number of seconds into hours, minutes,
60 and seconds, and returns a string with this information.
62 Args:
63 seconds: number of seconds
65 Returns:
66 String of scaled time.
68 """
70 # switch to calculating years, weeks, and days if larger than 100 hours
71 if seconds > (60 * 60 * 100):
72 years = floor(seconds / (60 * 60 * 24 * 365))
73 seconds = seconds - years * 60 * 60 * 24 * 365
74 weeks = floor(seconds / (60 * 60 * 24 * 7))
75 seconds = seconds - weeks * 60 * 60 * 24 * 7
76 days = floor(seconds / (60 * 60 * 24))
77 seconds = seconds - days * 60 * 60 * 24
78 else:
79 years = 0
80 weeks = 0
81 days = 0
83 # calculate hours, minutes, and seconds
84 hours = floor(seconds / (60 * 60))
85 seconds = seconds - hours * 60 * 60
86 minutes = floor(seconds / 60)
87 seconds = seconds - minutes * 60
89 # write out as a string, to keep the output manageable, only the largest
90 # three units are written
91 if years > 0:
92 time = f"{years} years, {weeks} weeks, and {days} days"
93 elif weeks > 0:
94 time = f"{weeks} weeks, {days} days, and {hours} hours"
95 elif days > 0:
96 time = f"{days} days, {hours} hours, and {minutes} min"
97 elif hours > 0:
98 seconds = np.round(seconds, 4)
99 if np.abs(seconds - int(seconds)) < 1e-4:
100 seconds = int(seconds)
101 time = f"{hours}hours {minutes}min {seconds}s"
102 elif minutes > 0:
103 seconds = np.round(seconds, 4)
104 if np.abs(seconds - int(seconds)) < 1e-4:
105 seconds = int(seconds)
106 time = f"{minutes}min {seconds}s"
107 else:
108 precision = 10 # manually tuned number
109 seconds = round(seconds, precision)
110 time = f"{seconds}s"
111 return time
114@typechecker
115def scale_SI(x: Union[float, np.ndarray]) -> Tuple[str, Union[int, float], str, str]:
116 """
117 Scale a number to the nearest SI unit prefix.
119 Args:
120 x: The number to scale.
122 Returns:
123 A tuple containing a string of the scaled number, a numeric scaling factor, the prefix, and the unit.
125 """
127 # force the input to be a scalar
128 x = np.max(x)
130 # check for a negative input
131 if x < 0:
132 x = -x
133 negative = True
134 else:
135 negative = False
137 if x == 0:
138 # if x is zero, don't scale
139 x_sc = x
140 prefix = ""
141 prefix_fullname = ""
142 scale = 1
144 elif x < 1:
145 # update index and input
146 x_sc = x * 1e3
147 sym_index = 1
149 # find scaling parameter
150 while x_sc < 1 and sym_index < 8:
151 x_sc = x_sc * 1e3
152 sym_index = sym_index + 1
154 # define SI unit scalings
155 units = {
156 1: ("m", "milli", 1e3),
157 2: ("u", "micro", 1e6),
158 3: ("n", "nano", 1e9),
159 4: ("p", "pico", 1e12),
160 5: ("f", "femto", 1e15),
161 6: ("a", "atto", 1e18),
162 7: ("z", "zepto", 1e21),
163 8: ("y", "yocto", 1e24),
164 }
165 prefix, prefix_fullname, scale = units[sym_index]
167 elif x >= 1000:
168 # update index and input
169 x_sc = x * 1e-3
170 sym_index = 1
172 # find scaling parameter
173 while x_sc >= 1000 and sym_index < 8:
174 x_sc = x_sc * 1e-3
175 sym_index = sym_index + 1
177 # define SI unit scalings
178 units = {
179 1: ("k", "kilo", 1e-3),
180 2: ("M", "mega", 1e-6),
181 3: ("G", "giga", 1e-9),
182 4: ("T", "tera", 1e-12),
183 5: ("P", "peta", 1e-15),
184 6: ("E", "exa", 1e-18),
185 7: ("Z", "zetta", 1e-21),
186 8: ("Y", "yotta", 1e-24),
187 }
188 prefix, prefix_fullname, scale = units[sym_index]
190 else:
191 # if x is between 1 and 1000, don't scale
192 x_sc = x
193 prefix = ""
194 prefix_fullname = ""
195 scale = 1
197 # form scaling into a string
198 round_decimals = 6 # TODO this needs to be tuned
199 x_sc = x_sc.round(round_decimals)
200 if (x_sc - int(x_sc)) < (0.1**round_decimals):
201 # avoid values like X.0, instead have only X
202 x_sc = int(x_sc)
203 x_sc = f"-{x_sc}{prefix}" if negative else f"{x_sc}{prefix}"
204 return x_sc, scale, prefix, prefix_fullname
207def get_date_string() -> str:
208 return datetime.now().strftime("%d-%b-%Y-%H-%M-%S")