Coverage for strongcoca/utilities.py: 94%
50 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-26 18:44 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-26 18:44 +0000
1from typing import Any, List, Optional
2import numpy as np
5ValueAsName = object()
8class ClassFormatter:
9 """Utility class to simplify writing string representations of generic
10 classes
12 After initialization add lines to be printed to the buffer
13 - :func:`append_class_name()` to buffer the name of the class
14 - :func:`append_attr()` to buffer an attribute and its value
15 - :func:`append_ndarray()` to buffer an attribute that is a numpy array
17 Print the contents of the buffer using to_string()
19 Parameters
20 ----------
21 cls
22 Instance of the class to be formatted.
23 width
24 Maximum line width.
25 pad
26 Pad the column with attribute names with spaces until it is this wide.
27 precision
28 Value passed to :attr:`np.printoptions`.
29 threshold
30 Value passed to :attr:`np.printoptions`.
32 Examples
33 --------
35 The following snippet illustrates how to write a string representation of
36 dummy class DummyResponse:
38 >>> import numpy as np
39 >>> from strongcoca.utilities import ClassFormatter
40 >>>
41 >>> class DummyResponse():
42 ... def __init__(self, name, E_field):
43 ... self.name = name
44 ... self.E_field = E_field
45 ...
46 ... def __str__(self):
47 ... fmt = ClassFormatter(self)
48 ... fmt.append_class_name()
49 ...
50 ... fmt.append_attr('name')
51 ... fmt.append_attr('E_field', self.E_field, unit='V/Å')
52 ...
53 ... return fmt.to_string()
54 >>>
55 >>> response = DummyResponse('Constant field', np.array([0.1, 0.0, 0.0]))
56 >>> print(response)
57 ---------------------- DummyResponse -----------------------
58 name : Constant field
59 E_field (V/Å) : [0.1 0. 0. ]
60 """
62 def __init__(self,
63 cls: object,
64 width: int = 60,
65 pad: int = 18,
66 precision: int = 5,
67 threshold: int = 10):
68 self.cls = cls
69 self.width = width
70 self.pad = pad
71 self.precision = precision
72 self.threshold = threshold
74 self.lines: List[str] = []
76 def append_class_name(self) -> None:
77 """Append the name of the class of `cls` to the print buffer.
79 The print format is such that the class name is centered in a bar of width `width`.
80 """
81 line = ' {} '.format(self.cls.__class__.__name__).center(self.width, '-')
82 self.lines.append(line)
84 def append_attr(self,
85 name: str,
86 value: Any = ValueAsName,
87 unit: Optional[str] = None) -> None:
88 """Append attribute name, value and optinally unit to print buffer.
90 The print format is `name` or `name (unit)` in the left column of width `pad`,
91 and `value` in the right column.
93 Parameters
94 ----------
95 name
96 Attribute name.
97 value
98 Value to print; if `None` the value of the attribute `name` of class `cls`
99 is used.
100 unit
101 Optional unit of attribute; if not `None` it is printed in paranthesis after
102 the attribute name.
104 This parameter is used solely for formatting, and does not perform unit conversions
105 of the value.
106 """
107 if value is ValueAsName:
108 value = getattr(self.cls, name)
110 if unit is not None:
111 name = f'{name} ({unit})'
113 if isinstance(value, np.ndarray):
114 self.append_ndarray(name, value)
115 return
116 line = f'{name:<{self.pad}}: {value}'
117 self.lines.append(line)
119 def append_ndarray(self,
120 name: str,
121 arr: np.ndarray,
122 suffix: str = '') -> None:
123 """Append attribute name and value to print buffer.
125 The print format is `name` in the left column of width `pad`, and `value`
126 in the right column. `value` must be a numpy array.
128 Parameters
129 ----------
130 name
131 Attribute name.
132 value
133 Value to print; if `None` the value of the attribute `name` of class `cls`
134 is used.
135 """
136 prefix = f'{name:<{self.pad}}: '
137 indent = len(prefix)
139 if arr is ValueAsName: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 arr = getattr(self.cls, name)
141 lines = format_ndarray(arr, width=self.width, prefix=prefix,
142 indent=indent, suffix=suffix,
143 precision=self.precision, threshold=self.threshold).split('\n')
144 self.lines += lines
146 def to_string(self) -> str:
147 """Dumps the print buffer as a string."""
148 return '\n'.join(self.lines)
151def format_ndarray(arr: np.ndarray,
152 width: int = 60,
153 indent: int = 0,
154 prefix: str = '',
155 suffix: str = '',
156 **np_printoptions: Any) -> str:
157 """Formats numpy array taking into account an indent, prefix and suffix.
159 Parameters
160 ----------
161 arr
162 Array to format.
163 width
164 Maximum width of any line.
165 indent
166 Pad the beginning of each line with this number of spaces.
167 prefix
168 Prepend this string at the begining of the first line; typically one would
169 write the array name followed by a colon.
170 The length of this string must be at most indent.
171 suffix
172 Append this string at the end of the last line; for example this could
173 be a unit.
174 **np_printoptions
175 Options passed to np.printoptions.
177 If linewidth is not one of these options it is automatically calculated
178 from the indent, width and length of the suffix.
180 Returns
181 -------
182 Formatted array.
184 Raises
185 ------
186 ValueError
187 If the lengths mismatch.
189 Examples
190 --------
191 >>> from numpy.random import seed, rand
192 >>> from strongcoca.utilities import format_ndarray
193 >>>
194 >>> seed(42)
195 >>> arr = rand(3)
196 >>> prefix = 'my_array: '
197 >>> s = format_ndarray(arr, prefix=prefix, indent=len(prefix),
198 ... precision=5, threshold=10)
199 >>> print(s)
200 my_array: [0.37454 0.95071 0.73199]
202 >>> arr = rand(3,3)
203 >>> prefix = 'matrix '
204 >>> s = format_ndarray(arr, width=50, prefix=prefix, indent=16,
205 ... precision=5, threshold=10)
206 >>> print(s)
207 matrix [[0.59866 0.15602 0.15599]
208 [0.05808 0.86618 0.60112]
209 [0.70807 0.02058 0.96991]]
211 >>> arr = rand(15,7)
212 >>> prefix = 'D : '
213 >>> s = format_ndarray(arr, width=50, prefix=prefix, indent=len(prefix), suffix=' eV',
214 ... precision=5, threshold=10)
215 >>> print(s)
216 D : [[0.83244 0.21234 0.18182 ... 0.30424
217 0.52476 0.43195]
218 [0.29123 0.61185 0.13949 ... 0.36636
219 0.45607 0.78518]
220 [0.19967 0.51423 0.59241 ... 0.60754
221 0.17052 0.06505]
222 ...
223 [0.52273 0.42754 0.02542 ... 0.03143
224 0.63641 0.31436]
225 [0.50857 0.90757 0.24929 ... 0.75555
226 0.2288 0.07698]
227 [0.28975 0.16122 0.9297 ... 0.6334
228 0.87146 0.80367]] eV
229 """
231 if len(prefix) > indent:
232 raise ValueError(f'The prefix cannot be longer than the indent. '
233 f'Prefix is {prefix} and indent {indent}.')
235 linewidth = np_printoptions.get('linewidth', None)
236 if linewidth is None:
237 linewidth = width - indent - len(suffix)
238 if linewidth <= 0: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true
239 raise ValueError(f'When computing the linewidth, the sum of the indent '
240 f'and suffix length must be less than width.\n'
241 f'Current values are {indent}+{len(suffix)}'
242 f'>={width}')
243 np_printoptions['linewidth'] = linewidth
245 if indent + linewidth + len(suffix) > width:
246 raise ValueError(f'The sum of the indent, linewidth, and suffix length '
247 f'must at most be width.\n'
248 f'Current values are {indent}+{linewidth}+{len(suffix)}'
249 f'>{width}')
251 with np.printoptions(**np_printoptions):
252 np_lines = str(arr).split('\n')
254 # First line
255 out_lines = [f'{prefix:<{indent}s}{np_lines[0]}']
257 # Remaining lines
258 out_lines += [f'{"":<{indent}}{line}' for line in np_lines[1:]]
260 # Append suffix to last line
261 out_lines[-1] += suffix
263 return '\n'.join(out_lines)