Coverage for strongcoca/utilities.py: 94%

50 statements  

« 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 

3 

4 

5ValueAsName = object() 

6 

7 

8class ClassFormatter: 

9 """Utility class to simplify writing string representations of generic 

10 classes 

11 

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 

16 

17 Print the contents of the buffer using to_string() 

18 

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`. 

31 

32 Examples 

33 -------- 

34 

35 The following snippet illustrates how to write a string representation of 

36 dummy class DummyResponse: 

37 

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 """ 

61 

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 

73 

74 self.lines: List[str] = [] 

75 

76 def append_class_name(self) -> None: 

77 """Append the name of the class of `cls` to the print buffer. 

78 

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) 

83 

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. 

89 

90 The print format is `name` or `name (unit)` in the left column of width `pad`, 

91 and `value` in the right column. 

92 

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. 

103 

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) 

109 

110 if unit is not None: 

111 name = f'{name} ({unit})' 

112 

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) 

118 

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. 

124 

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. 

127 

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) 

138 

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 

145 

146 def to_string(self) -> str: 

147 """Dumps the print buffer as a string.""" 

148 return '\n'.join(self.lines) 

149 

150 

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. 

158 

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. 

176 

177 If linewidth is not one of these options it is automatically calculated 

178 from the indent, width and length of the suffix. 

179 

180 Returns 

181 ------- 

182 Formatted array. 

183 

184 Raises 

185 ------ 

186 ValueError 

187 If the lengths mismatch. 

188 

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] 

201 

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]] 

210 

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 """ 

230 

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}.') 

234 

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 

244 

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}') 

250 

251 with np.printoptions(**np_printoptions): 

252 np_lines = str(arr).split('\n') 

253 

254 # First line 

255 out_lines = [f'{prefix:<{indent}s}{np_lines[0]}'] 

256 

257 # Remaining lines 

258 out_lines += [f'{"":<{indent}}{line}' for line in np_lines[1:]] 

259 

260 # Append suffix to last line 

261 out_lines[-1] += suffix 

262 

263 return '\n'.join(out_lines)