hwcomponents 1.0.81__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ from hwcomponents.model import ComponentModel, action
2
+ from hwcomponents.find_models import get_models
3
+ import hwcomponents.scaling as scaling
4
+ from hwcomponents.select_models import (
5
+ get_area,
6
+ get_energy,
7
+ get_latency,
8
+ get_leak_power,
9
+ get_model,
10
+ )
@@ -0,0 +1,99 @@
1
+ import logging
2
+ import queue
3
+ from typing import List, Union
4
+ from logging.handlers import QueueHandler
5
+
6
+ logging.basicConfig(
7
+ format="%(levelname)-8s%(message)s",
8
+ )
9
+ if logging.getLogger().level == logging.NOTSET:
10
+ logging.getLogger().setLevel(logging.INFO)
11
+
12
+ LOG_QUEUES = {}
13
+ NAME2LOGGER = {}
14
+
15
+
16
+ def queue_from_logger(logger: Union[logging.Logger, str]) -> List[str]:
17
+ if isinstance(logger, str) and logger in LOG_QUEUES:
18
+ return LOG_QUEUES[logger]
19
+ for name, other_logger in NAME2LOGGER.items():
20
+ if other_logger is logger and name in LOG_QUEUES:
21
+ return LOG_QUEUES[name]
22
+ raise ValueError(f"Logger {logger} not found")
23
+
24
+
25
+ def messages_from_logger(logger: Union[logging.Logger, str]) -> List[str]:
26
+ return [m.getMessage() for m in queue_from_logger(logger).queue]
27
+
28
+
29
+ def get_logger(name: str) -> logging.Logger:
30
+ logger = logging.getLogger(name)
31
+ logger.propagate = False
32
+ NAME2LOGGER[name] = logger
33
+ if name not in LOG_QUEUES:
34
+ LOG_QUEUES[name] = queue.Queue()
35
+ logger.addHandler(QueueHandler(LOG_QUEUES[name]))
36
+ return logger
37
+
38
+
39
+ def move_queue_from_one_logger_to_another(
40
+ src: Union[logging.Logger, str], dest: Union[logging.Logger, str]
41
+ ):
42
+ src_queue = queue_from_logger(src)
43
+ dest_queue = queue_from_logger(dest)
44
+ if src_queue is dest_queue:
45
+ return
46
+ while not src_queue.empty():
47
+ dest_queue.put(src_queue.get())
48
+
49
+
50
+ def pop_all_messages(logger: Union[logging.Logger, str]) -> List[str]:
51
+ messages = messages_from_logger(logger)
52
+ queue_from_logger(logger).queue.clear()
53
+ return messages
54
+
55
+
56
+ def print_messages(logger: Union[logging.Logger, str]):
57
+ for message in messages_from_logger(logger):
58
+ print(message)
59
+
60
+
61
+ class ListLoggable:
62
+ def __init__(self, name: str = None):
63
+ self._init_logger(name)
64
+
65
+ def _init_logger(self, name: str = None):
66
+ if self.logger is not None:
67
+ return
68
+ if name is None:
69
+ if hasattr(self, "__name__"):
70
+ name = self.__name__
71
+ else:
72
+ name = self.__class__.__name__
73
+ self.logger = get_logger(name)
74
+
75
+ @property
76
+ def logger(self) -> logging.Logger:
77
+ if getattr(self, "_logger", None) is None:
78
+ if hasattr(self, "__name__"):
79
+ self._logger = get_logger(self.__name__)
80
+ else:
81
+ self._logger = get_logger(self.__class__.__name__)
82
+ self._logger.setLevel(logging.INFO)
83
+ return self._logger
84
+
85
+
86
+ def log_all_lines(logger_name, level, to_split):
87
+ logger = logging.getLogger(logger_name)
88
+ if isinstance(level, str):
89
+ logfunc = getattr(logger, level)
90
+ for s in to_split.splitlines():
91
+ logfunc(s)
92
+ else:
93
+ for s in to_split.splitlines():
94
+ logging.getLogger(logger_name).log(level, s)
95
+
96
+
97
+ def clear_logs():
98
+ for name, queue in LOG_QUEUES.items():
99
+ queue.queue.clear()
@@ -0,0 +1,461 @@
1
+ import inspect
2
+ import logging
3
+ from numbers import Number
4
+ from types import ModuleType
5
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
6
+ from .model import ComponentModel
7
+ from ._logging import (
8
+ move_queue_from_one_logger_to_another,
9
+ ListLoggable,
10
+ pop_all_messages,
11
+ )
12
+
13
+
14
+ class EstimatorError(Exception):
15
+ def __str__(self):
16
+ return self.message
17
+
18
+ def __repr__(self):
19
+ return str(self)
20
+
21
+ def __init__(self, message: str):
22
+ super().__init__(message)
23
+ self.message = message
24
+
25
+
26
+ class PrintableCall:
27
+ def __init__(
28
+ self,
29
+ name: str,
30
+ args: List[str] = (),
31
+ defaults: Dict[str, Any] = None,
32
+ ):
33
+ self.name = name
34
+ self.args = args
35
+ self.defaults = defaults or {}
36
+
37
+ def __str__(self):
38
+ n = self.name
39
+ args = [str(a) for a in self.args] + [
40
+ f"{k}={v}" for k, v in self.defaults.items()
41
+ ]
42
+
43
+ return f"{n}({', '.join(args)})"
44
+
45
+
46
+ class ModelQuery:
47
+ """A query to an ComponentModel."""
48
+
49
+ def __init__(
50
+ self,
51
+ component_name: str,
52
+ component_attributes: Dict[str, Any],
53
+ action_name: str = None,
54
+ action_arguments: Dict[str, Any] = None,
55
+ required_actions: List[str] = (),
56
+ ):
57
+ self.component_name = component_name
58
+ self.action_name = action_name
59
+ # Attributes and arguments are only included if they are not None
60
+ action_arguments = {} if action_arguments is None else action_arguments
61
+ self.action_arguments = {
62
+ k: v for k, v in action_arguments.items() if v is not None
63
+ }
64
+ self.component_attributes = {
65
+ k: v for k, v in component_attributes.items() if v is not None
66
+ }
67
+ self.required_actions = required_actions
68
+
69
+ def __str__(self):
70
+ attrs_stringified = ", ".join(
71
+ [f"{k}={v}" for k, v in self.component_attributes.items()]
72
+ )
73
+ s = f"{self.component_name}({attrs_stringified})"
74
+ if self.action_name:
75
+ args_stringified = ", ".join(
76
+ [f"{k}={v}" for k, v in self.action_arguments.items()]
77
+ )
78
+ s += f".{self.action_name}({args_stringified})"
79
+ return s
80
+
81
+
82
+ class Estimation:
83
+ value: Union[int, float, ComponentModel]
84
+ success: bool
85
+ messages: List[str]
86
+ model_name: Optional[str]
87
+
88
+ def __init__(
89
+ self,
90
+ value: Union[int, float, ComponentModel],
91
+ success: bool = True,
92
+ messages: List[str] = (),
93
+ model_name: Optional[str] = None,
94
+ ):
95
+ self.value = value
96
+ self.success = success
97
+ self.messages = list(messages)
98
+ self.model_name = model_name
99
+
100
+ def add_messages(self, messages: Union[List[str], str]):
101
+ """
102
+ Adds messages to the internal message list. The messages are reported
103
+ depending on model selections and verbosity level.
104
+ """
105
+ if isinstance(messages, str):
106
+ self.add_messages([messages])
107
+ else:
108
+ self.messages += messages
109
+
110
+ def fail(self, message: str):
111
+ """Marks this estimation as failed and adds the message."""
112
+ self.success = False
113
+ self.add_messages(message)
114
+
115
+ def lastmessage(self) -> str:
116
+ """Returns the last message in the message list. If no messages, returns
117
+ a default."""
118
+ if self.messages:
119
+ return self.messages[-1]
120
+ else:
121
+ return f"No messages found."
122
+
123
+
124
+ class FloatEstimation(Estimation):
125
+ def __init__(
126
+ self,
127
+ value: Union[int, float, tuple[Union[int, float], ...]],
128
+ success: bool = True,
129
+ messages: List[str] = [],
130
+ model_name: Optional[str] = None,
131
+ ):
132
+ super().__init__(value, success, messages, model_name)
133
+
134
+
135
+ class ModelEstimation(Estimation):
136
+ def __init__(
137
+ self,
138
+ value: Union[int, float],
139
+ success: bool = True,
140
+ messages: List[str] = [],
141
+ model_name: Optional[str] = None,
142
+ ):
143
+ super().__init__(value, success, messages, model_name)
144
+
145
+
146
+ class CallableFunction:
147
+ """Wrapper for a function to provide error checking and argument
148
+ matching."""
149
+
150
+ def __init__(
151
+ self,
152
+ function: Callable,
153
+ logger: logging.Logger,
154
+ force_name_override: str = None,
155
+ is_init: bool = False,
156
+ ):
157
+ if not isinstance(function, Callable):
158
+ raise TypeError(
159
+ f"Function {function.__name__} must be an instance of Callable, not {type(function)}"
160
+ )
161
+
162
+ self.function = function
163
+ self.additional_kwargs = getattr(function, "_additional_kwargs", set())
164
+ if is_init:
165
+ function = function.__init__
166
+ elif getattr(function, "_is_component_action", False):
167
+ function = function._original_function
168
+
169
+ args = function.__code__.co_varnames[1 : function.__code__.co_argcount]
170
+ default_length = (
171
+ len(function.__defaults__) if function.__defaults__ is not None else 0
172
+ )
173
+
174
+ self.function_name = function.__name__
175
+ if force_name_override is not None:
176
+ self.function_name = force_name_override
177
+ self.non_default_args = args[: len(args) - default_length]
178
+ self.default_args = args[len(args) - default_length :]
179
+ self.default_arg_values = (
180
+ function.__defaults__ if function.__defaults__ is not None else []
181
+ )
182
+ self.logger = logger
183
+
184
+ def get_error_message_for_name_match(self, name: str, component_name: str = ""):
185
+ if self.function_name != name:
186
+ return f"Function name {self.function_name} does not match my name {component_name}.{name}"
187
+ return None
188
+
189
+ def get_error_message_for_non_default_arg_match(
190
+ self, kwags: dict, component_name: str = ""
191
+ ) -> Optional[str]:
192
+ for arg in self.non_default_args:
193
+ if kwags.get(arg) is None:
194
+ return (
195
+ f"Argument for {component_name}.{self.function_name} is missing: {arg}. "
196
+ f'Arguments provided: {", ".join(kwags.keys())}'
197
+ )
198
+ return None
199
+
200
+ def get_call_error_message(
201
+ self, name: str, kwargs: dict, component_name: str = ""
202
+ ) -> Optional[str]:
203
+ name_error = self.get_error_message_for_name_match(name, component_name)
204
+ if name_error is not None:
205
+ return name_error
206
+ arg_error = self.get_error_message_for_non_default_arg_match(
207
+ kwargs, component_name
208
+ )
209
+ if arg_error is not None:
210
+ return arg_error
211
+ return None
212
+
213
+ def call(
214
+ self,
215
+ kwargs: dict,
216
+ component_name: str = "",
217
+ call_function_on_object: object = None,
218
+ ) -> Any:
219
+ kwags_included = {
220
+ k: v
221
+ for k, v in kwargs.items()
222
+ if k in self.non_default_args
223
+ or k in self.default_args
224
+ or k in self.additional_kwargs
225
+ }
226
+ self.logger.info(
227
+ f"Calling {self.function_name} with arguments {kwags_included}"
228
+ )
229
+ unneeded_args = [k for k in kwargs.keys() if k not in kwags_included]
230
+ if unneeded_args:
231
+ self.logger.warn(
232
+ f"Unused arguments for {component_name}.{self.function_name}: "
233
+ f'({", ".join(unneeded_args)}) '
234
+ f'Arguments used: ({", ".join(kwags_included.keys())})'
235
+ )
236
+
237
+ if call_function_on_object is not None:
238
+ return self.function(call_function_on_object, **kwags_included)
239
+ return self.function(**kwags_included)
240
+
241
+ def __str__(self):
242
+ return str(
243
+ PrintableCall(
244
+ self.function_name if self.function_name != "__init__" else "",
245
+ self.non_default_args,
246
+ {a: b for a, b in zip(self.default_args, self.default_arg_values)},
247
+ )
248
+ )
249
+
250
+
251
+ class ComponentModelWrapper(ListLoggable):
252
+ def __init__(self, model_cls: type, component_name: str):
253
+ check_for_valid_model_attrs(model_cls)
254
+ self.model_cls = model_cls
255
+ self.model_name = component_name
256
+ cls_component_name = model_cls._component_name()
257
+ if isinstance(cls_component_name, str):
258
+ cls_component_name = [cls_component_name]
259
+ if not isinstance(cls_component_name, list):
260
+ raise ValueError(
261
+ f"component_name must be a string or list of strings, not {type(cls_component_name)}"
262
+ )
263
+ self.component_name = [c.lower() for c in cls_component_name]
264
+ super().__init__(name=self.get_name())
265
+
266
+ self.priority = model_cls.priority
267
+ if self.priority < 0 or self.priority > 1:
268
+ raise ValueError(f"Priority must be between 0 and 1, not {self.priority}")
269
+ self.init_function = CallableFunction(model_cls, self.logger, is_init=True)
270
+
271
+ self.actions = [
272
+ CallableFunction(getattr(model_cls, a), self.logger)
273
+ for a in dir(model_cls)
274
+ if getattr(
275
+ getattr(model_cls, a),
276
+ "_is_component_action",
277
+ False,
278
+ )
279
+ ]
280
+ # self.actions.append(CallableFunction(model_cls.leak, self.logger))
281
+ logging.debug(
282
+ f"Added model {self.model_name} that for component {self.component_name}"
283
+ )
284
+
285
+ def get_action_names(self) -> List[str]:
286
+ return [a.function_name for a in self.actions]
287
+
288
+ def fail_missing(self, missing: str):
289
+ raise AttributeError(
290
+ f"Primitive component {self.component_name} " f"must have {missing}"
291
+ )
292
+
293
+ def is_component_supported(
294
+ self, query: ModelQuery, relaxed_component_name_selection: bool = False
295
+ ) -> bool:
296
+ if query.component_name.lower() in self.component_name:
297
+ pass
298
+ elif query.component_name.lower().replace("_", "") in self.component_name:
299
+ if relaxed_component_name_selection:
300
+ pass
301
+ else:
302
+ self.logger.error(
303
+ f"Component name is similar to supported component names, but not "
304
+ f"supported. Did you mean {self.component_name}?"
305
+ )
306
+ return False
307
+ else:
308
+ self.logger.error(
309
+ f"Component name {query.component_name} is not supported. "
310
+ f"Supported component names: {self.component_name}"
311
+ )
312
+ return False
313
+
314
+ init_error = self.init_function.get_call_error_message(
315
+ "__init__", query.component_attributes, self.component_name
316
+ )
317
+ if init_error is not None:
318
+ self.logger.error(init_error)
319
+ raise EstimatorError(init_error)
320
+ return True
321
+
322
+ def get_initialized_subclass(self, query: ModelQuery) -> ComponentModel:
323
+ self.logger.info(
324
+ f"Initializing {self.model_cls.__name__} from {self.model_cls.__module__}"
325
+ )
326
+ subclass = self.init_function.call(
327
+ query.component_attributes, self.component_name
328
+ )
329
+ subclass._init_logger()
330
+ return subclass
331
+
332
+ def get_matching_actions(self, query: ModelQuery) -> List[CallableFunction]:
333
+ # Find actions that match the name
334
+ name_matches = [a for a in self.actions if a.function_name == query.action_name]
335
+ if len(name_matches) == 0:
336
+ raise AttributeError(
337
+ f"No action with name {query.action_name} found in {self.component_name}. "
338
+ f'Actions supported: {", ".join(self.get_action_names())}'
339
+ )
340
+
341
+ # Find actions that match the arguments
342
+ matching_name_and_arg_actions = [
343
+ a
344
+ for a in name_matches
345
+ if a.get_call_error_message(query.action_name, query.action_arguments)
346
+ is None
347
+ ]
348
+ if len(matching_name_and_arg_actions) == 0:
349
+ matching_func_strings = [
350
+ (
351
+ f"{a.function_name}("
352
+ + ", ".join(
353
+ list(a.non_default_args)
354
+ + ["OPTIONAL " + b for b in a.default_args]
355
+ )
356
+ )
357
+ + ")"
358
+ for a in name_matches
359
+ ]
360
+ args_provided = (
361
+ query.action_arguments.keys() if query.action_arguments else ["<none>"]
362
+ )
363
+ raise AttributeError(
364
+ f"Action with name {query.action_name} found in {self.component_name}, "
365
+ f"but provided arguments do not match.\n\t"
366
+ f'Arguments provided: {", ".join(args_provided)}\n\t'
367
+ f"Possible actions:\n\t\t" + "\n\t\t".join(matching_func_strings)
368
+ )
369
+ return matching_name_and_arg_actions
370
+
371
+ def get_action_energy_latency(
372
+ self, query: ModelQuery, initialized_obj: ComponentModel = None
373
+ ) -> Estimation:
374
+ """Returns the energy and latency estimation for the given action."""
375
+ if initialized_obj is None:
376
+ initialized_obj = self.get_initialized_subclass(query)
377
+ move_queue_from_one_logger_to_another(initialized_obj.logger, self.logger)
378
+ supported_actions = self.get_matching_actions(query)
379
+ if len(supported_actions) == 0:
380
+ raise AttributeError(
381
+ f"No action with name {query.action_name} found in "
382
+ f"{self.component_name}. Actions supported: "
383
+ f"{', '.join(self.get_action_names())}"
384
+ )
385
+ try:
386
+ result = supported_actions[0].call(
387
+ query.action_arguments, self.component_name, initialized_obj
388
+ )
389
+ energy_value, latency_value = result
390
+ estimation = FloatEstimation(
391
+ value=(energy_value, latency_value),
392
+ )
393
+ except Exception as e:
394
+ move_queue_from_one_logger_to_another(initialized_obj.logger, self.logger)
395
+ raise e
396
+ estimation.add_messages(pop_all_messages(initialized_obj.logger))
397
+ estimation.model_name = self.model_name
398
+ return estimation
399
+
400
+ def get_area(self, query: ModelQuery) -> Estimation:
401
+ """Returns the area estimation for the given action."""
402
+ subclass = self.get_initialized_subclass(query)
403
+ return FloatEstimation(
404
+ value=subclass.area,
405
+ success=True,
406
+ model_name=self.model_name,
407
+ messages=pop_all_messages(subclass.logger),
408
+ )
409
+
410
+ def get_leak_power(self, query: ModelQuery) -> Estimation:
411
+ """Returns the leak power estimation for the given action."""
412
+ subclass = self.get_initialized_subclass(query)
413
+ return FloatEstimation(
414
+ value=subclass.leak_power,
415
+ success=True,
416
+ model_name=self.model_name,
417
+ messages=pop_all_messages(subclass.logger),
418
+ )
419
+
420
+ def get_name(self) -> str:
421
+ return self.model_name
422
+
423
+ def get_component_names(self) -> List[str]:
424
+ return (
425
+ [self.component_name]
426
+ if isinstance(self.component_name, str)
427
+ else self.component_name
428
+ )
429
+
430
+ @staticmethod
431
+ def print_action(action: CallableFunction) -> str:
432
+ return action.function_name
433
+
434
+
435
+ def check_for_valid_model_attrs(model: ComponentModel):
436
+ # Check for valid component_name. Must be a string or list of strings
437
+ component_name = model._component_name()
438
+ if not isinstance(component_name, str) and not (
439
+ isinstance(component_name, (list, tuple))
440
+ and all(isinstance(n, str) for n in component_name)
441
+ ):
442
+ raise AttributeError(
443
+ f"ComponentModel {model} component_name must be a string or list/tuple of strings"
444
+ )
445
+
446
+ # Check for valid priority. Must be a number between 0 and 100
447
+ if getattr(model, "priority", None) is None:
448
+ raise AttributeError(
449
+ f'ComponentModel for {component_name} must have a "priority" ' f"attribute."
450
+ )
451
+ priority = model.priority
452
+ if not isinstance(priority, Number):
453
+ raise AttributeError(
454
+ f"ComponentModel for {component_name} priority must be a "
455
+ f"number. It is currently a {type(priority)}"
456
+ )
457
+ if priority < 0 or priority > 1:
458
+ raise AttributeError(
459
+ f"ComponentModel for {component_name} priority must be "
460
+ f"between 0 and 1 inclusive."
461
+ )
hwcomponents/_util.py ADDED
@@ -0,0 +1,14 @@
1
+ import re
2
+
3
+
4
+ def parse_float(s: str, context: str = "") -> float:
5
+ """Parses a string into a float. Handles scientific notation."""
6
+ # Remove leading and trailing non-numeric characters
7
+ s = str(s)
8
+ numeric_re_sub = r"[^0-9eE\-\+\.]"
9
+ s_trimmed = re.sub(f"^{numeric_re_sub}+", "", s)
10
+ s_trimmed = re.sub(f"{numeric_re_sub}+$", "", s_trimmed)
11
+ try:
12
+ return float(s_trimmed)
13
+ except (ValueError, TypeError) as e:
14
+ raise ValueError(f'Could not parse "{s}" from "{context}" as a float.') from e
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '1.0.81'
32
+ __version_tuple__ = version_tuple = (1, 0, 81)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,23 @@
1
+ """Version scheme for setuptools-scm - creates post-release versions."""
2
+
3
+ from setuptools_scm.version import guess_next_version
4
+
5
+
6
+ def post_version(version):
7
+ """Create post-release versions instead of dev versions."""
8
+ if version.exact:
9
+ return version.format_with("{tag}").lstrip("v")
10
+
11
+ base = (
12
+ str(version.tag).lstrip("v")
13
+ if version.tag
14
+ else (guess_next_version(version) or "1.0")
15
+ )
16
+ distance = version.distance or 0
17
+
18
+ return f"{base}.{distance}" if distance > 0 else base
19
+
20
+
21
+ def no_local(version):
22
+ """No local version identifier."""
23
+ return ""