flopscope 0.2.0__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.
Files changed (115) hide show
  1. benchmarks/__init__.py +1 -0
  2. benchmarks/__main__.py +6 -0
  3. benchmarks/_baseline.py +171 -0
  4. benchmarks/_bitwise.py +231 -0
  5. benchmarks/_complex.py +176 -0
  6. benchmarks/_contractions.py +291 -0
  7. benchmarks/_fft.py +198 -0
  8. benchmarks/_impl_urls.py +139 -0
  9. benchmarks/_linalg.py +197 -0
  10. benchmarks/_linalg_delegates.py +407 -0
  11. benchmarks/_metadata.py +141 -0
  12. benchmarks/_misc.py +653 -0
  13. benchmarks/_perf.py +321 -0
  14. benchmarks/_perm_group_calibration.py +175 -0
  15. benchmarks/_pointwise.py +372 -0
  16. benchmarks/_polynomial.py +193 -0
  17. benchmarks/_random.py +209 -0
  18. benchmarks/_reductions.py +136 -0
  19. benchmarks/_sorting.py +289 -0
  20. benchmarks/_stats.py +137 -0
  21. benchmarks/_window.py +92 -0
  22. benchmarks/accumulation/__init__.py +0 -0
  23. benchmarks/accumulation/bench_cost_compute.py +138 -0
  24. benchmarks/dashboard.py +312 -0
  25. benchmarks/runner.py +636 -0
  26. flopscope/__init__.py +273 -0
  27. flopscope/_accumulation/__init__.py +13 -0
  28. flopscope/_accumulation/_bipartite.py +121 -0
  29. flopscope/_accumulation/_burnside.py +51 -0
  30. flopscope/_accumulation/_cache.py +146 -0
  31. flopscope/_accumulation/_components.py +153 -0
  32. flopscope/_accumulation/_cost.py +1414 -0
  33. flopscope/_accumulation/_cost_descriptions.py +63 -0
  34. flopscope/_accumulation/_detection.py +318 -0
  35. flopscope/_accumulation/_ladder.py +191 -0
  36. flopscope/_accumulation/_output_orbit.py +104 -0
  37. flopscope/_accumulation/_partition.py +290 -0
  38. flopscope/_accumulation/_path_info.py +211 -0
  39. flopscope/_accumulation/_public.py +169 -0
  40. flopscope/_accumulation/_reduction.py +310 -0
  41. flopscope/_accumulation/_regimes.py +303 -0
  42. flopscope/_accumulation/_shape.py +33 -0
  43. flopscope/_accumulation/_wreath.py +209 -0
  44. flopscope/_budget.py +1027 -0
  45. flopscope/_config.py +118 -0
  46. flopscope/_counting_ops.py +451 -0
  47. flopscope/_display.py +478 -0
  48. flopscope/_docstrings.py +59 -0
  49. flopscope/_dtypes.py +20 -0
  50. flopscope/_einsum.py +717 -0
  51. flopscope/_errstate.py +25 -0
  52. flopscope/_flops.py +282 -0
  53. flopscope/_free_ops.py +2654 -0
  54. flopscope/_ndarray.py +1126 -0
  55. flopscope/_opt_einsum/LICENSE +21 -0
  56. flopscope/_opt_einsum/NOTICE +59 -0
  57. flopscope/_opt_einsum/__init__.py +209 -0
  58. flopscope/_opt_einsum/_contract.py +1478 -0
  59. flopscope/_opt_einsum/_helpers.py +164 -0
  60. flopscope/_opt_einsum/_hsluv.py +273 -0
  61. flopscope/_opt_einsum/_path_random.py +462 -0
  62. flopscope/_opt_einsum/_paths.py +1653 -0
  63. flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
  64. flopscope/_opt_einsum/_symmetry.py +140 -0
  65. flopscope/_opt_einsum/_typing.py +37 -0
  66. flopscope/_perm_group.py +717 -0
  67. flopscope/_pointwise.py +2522 -0
  68. flopscope/_polynomial.py +278 -0
  69. flopscope/_registry.py +3216 -0
  70. flopscope/_sorting_ops.py +571 -0
  71. flopscope/_symmetric.py +812 -0
  72. flopscope/_symmetry_transport.py +510 -0
  73. flopscope/_symmetry_utils.py +669 -0
  74. flopscope/_type_info.py +12 -0
  75. flopscope/_unwrap.py +70 -0
  76. flopscope/_validation.py +83 -0
  77. flopscope/_version_check.py +46 -0
  78. flopscope/_weights.py +195 -0
  79. flopscope/_window.py +177 -0
  80. flopscope/accounting.py +565 -0
  81. flopscope/data/default_weights.json +462 -0
  82. flopscope/data/weights.csv +509 -0
  83. flopscope/errors.py +197 -0
  84. flopscope/numpy/__init__.py +878 -0
  85. flopscope/numpy/fft/__init__.py +55 -0
  86. flopscope/numpy/fft/_free.py +51 -0
  87. flopscope/numpy/fft/_transforms.py +695 -0
  88. flopscope/numpy/linalg/__init__.py +105 -0
  89. flopscope/numpy/linalg/_aliases.py +126 -0
  90. flopscope/numpy/linalg/_compound.py +161 -0
  91. flopscope/numpy/linalg/_decompositions.py +353 -0
  92. flopscope/numpy/linalg/_properties.py +533 -0
  93. flopscope/numpy/linalg/_solvers.py +444 -0
  94. flopscope/numpy/linalg/_svd.py +122 -0
  95. flopscope/numpy/random/__init__.py +684 -0
  96. flopscope/numpy/random/_cost_formulas.py +115 -0
  97. flopscope/numpy/random/_counted_classes.py +241 -0
  98. flopscope/numpy/testing/__init__.py +13 -0
  99. flopscope/numpy/typing/__init__.py +30 -0
  100. flopscope/py.typed +0 -0
  101. flopscope/stats/__init__.py +84 -0
  102. flopscope/stats/_base.py +77 -0
  103. flopscope/stats/_cauchy.py +146 -0
  104. flopscope/stats/_erf.py +190 -0
  105. flopscope/stats/_expon.py +146 -0
  106. flopscope/stats/_laplace.py +150 -0
  107. flopscope/stats/_logistic.py +148 -0
  108. flopscope/stats/_lognorm.py +160 -0
  109. flopscope/stats/_ndtri.py +133 -0
  110. flopscope/stats/_norm.py +149 -0
  111. flopscope/stats/_truncnorm.py +186 -0
  112. flopscope/stats/_uniform.py +141 -0
  113. flopscope-0.2.0.dist-info/METADATA +23 -0
  114. flopscope-0.2.0.dist-info/RECORD +115 -0
  115. flopscope-0.2.0.dist-info/WHEEL +4 -0
flopscope/_display.py ADDED
@@ -0,0 +1,478 @@
1
+ """Budget display rendering with Rich (optional) and plain-text fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from flopscope._budget import _snapshot_records, budget_summary_dict
6
+
7
+
8
+ def _format_flops(n: int) -> str:
9
+ """Format a FLOP count with thousands separators."""
10
+ return f"{n:,}"
11
+
12
+
13
+ def _pct(used: int | float, total: int | float) -> str:
14
+ """Return a percentage string."""
15
+ if total == 0:
16
+ return "0.0%"
17
+ return f"{100 * used / total:.1f}%"
18
+
19
+
20
+ def _usage_color(used: int, total: int) -> str:
21
+ """Return a Rich color name based on usage percentage."""
22
+ if total == 0:
23
+ return "white"
24
+ ratio = used / total
25
+ if ratio < 0.5:
26
+ return "green"
27
+ if ratio < 0.8:
28
+ return "yellow"
29
+ return "red"
30
+
31
+
32
+ def _namespace_label(ns: str | None) -> str:
33
+ return ns if ns is not None else "(unlabeled)"
34
+
35
+
36
+ def _call_label(calls: int) -> str:
37
+ return f"{calls} call{'s' if calls != 1 else ''}"
38
+
39
+
40
+ def _sorted_namespace_rows(
41
+ by_namespace: dict[str | None, dict],
42
+ ) -> list[tuple[str | None, dict]]:
43
+ return sorted(
44
+ by_namespace.items(),
45
+ key=lambda item: (-item[1]["flops_used"], _namespace_label(item[0])),
46
+ )
47
+
48
+
49
+ def _format_budget_summary_text(
50
+ data: dict,
51
+ *,
52
+ by_namespace: bool = False,
53
+ header: str = "flopscope FLOP Budget Summary",
54
+ ) -> str:
55
+ """Render structured budget data as plain text."""
56
+ if data["flops_used"] == 0 and not data.get("operations"):
57
+ return "No budget data recorded yet."
58
+
59
+ lines = [
60
+ header,
61
+ "=" * len(header),
62
+ f" Total budget: {_format_flops(data['flop_budget']):>20}",
63
+ f" Used: {_format_flops(data['flops_used']):>20} ({_pct(data['flops_used'], data['flop_budget'])})",
64
+ f" Remaining: {_format_flops(data['flops_remaining']):>20} ({_pct(data['flops_remaining'], data['flop_budget'])})",
65
+ ]
66
+
67
+ if by_namespace and data.get("by_namespace"):
68
+ lines += ["", " By namespace:"]
69
+ for namespace, bucket in _sorted_namespace_rows(data["by_namespace"]):
70
+ lines.append(
71
+ f" {_namespace_label(namespace):<24} {_format_flops(bucket['flops_used']):>12} ({_pct(bucket['flops_used'], data['flops_used']):>6}) [{_call_label(bucket['calls'])}] Backend {bucket['flopscope_backend_time_s']:.3f}s Overhead {bucket['flopscope_overhead_time_s']:.3f}s"
72
+ )
73
+
74
+ ops = data.get("operations", {})
75
+ if ops:
76
+ lines += ["", " By operation:"]
77
+ for op_name, op_info in sorted(
78
+ ops.items(), key=lambda item: -item[1]["flop_cost"]
79
+ ):
80
+ lines.append(
81
+ f" {op_name:<20} {_format_flops(op_info['flop_cost']):>12} ({_pct(op_info['flop_cost'], data['flops_used']):>6}) [{_call_label(op_info['calls'])}]"
82
+ )
83
+
84
+ wall_time = data.get("wall_time_s")
85
+ backend_time = data.get("flopscope_backend_time_s", 0.0)
86
+ overhead_time = data.get("flopscope_overhead_time_s", 0.0)
87
+ residual_wall_time = data.get("residual_wall_time_s")
88
+ if wall_time is not None and residual_wall_time is not None:
89
+ lines += [
90
+ "",
91
+ f" Total Wall Time: {wall_time:.3f}s",
92
+ f" Flopscope Backend: {backend_time:.3f}s ({_pct(backend_time, wall_time)})",
93
+ f" Flopscope Overhead: {overhead_time:.3f}s ({_pct(overhead_time, wall_time)})",
94
+ f" Residual Wall Time: {residual_wall_time:.3f}s ({_pct(residual_wall_time, wall_time)})",
95
+ ]
96
+
97
+ op_backend_times = {
98
+ op_name: op_info["flopscope_backend_time_s"]
99
+ for op_name, op_info in ops.items()
100
+ if op_info.get("flopscope_backend_time_s", 0.0) > 0
101
+ }
102
+ if backend_time > 0 and op_backend_times:
103
+ lines += ["", " By operation (time):"]
104
+ for op_name, op_backend_time in sorted(
105
+ op_backend_times.items(), key=lambda item: -item[1]
106
+ ):
107
+ lines.append(
108
+ f" {op_name:<20} {op_backend_time:.3f}s ({_pct(op_backend_time, backend_time):>6}) [{_call_label(ops[op_name]['calls'])}]"
109
+ )
110
+
111
+ return "\n".join(lines)
112
+
113
+
114
+ def _plain_text_summary(by_namespace: bool = False) -> str:
115
+ """Render a plain-text budget summary."""
116
+ return _format_budget_summary_text(
117
+ budget_summary_dict(by_namespace=by_namespace),
118
+ by_namespace=by_namespace,
119
+ )
120
+
121
+
122
+ _GLOBAL_DEFAULT_BUDGET = int(1e15)
123
+
124
+
125
+ def _is_global_default_ns(ns, ns_data: dict) -> bool:
126
+ """Check if a namespace record is the implicit global default."""
127
+ return ns is None and ns_data.get("flop_budget", 0) >= _GLOBAL_DEFAULT_BUDGET
128
+
129
+
130
+ def _display_totals(data: dict | None = None) -> dict:
131
+ """Compute user-facing totals, hiding the implicit global default budget."""
132
+ if data is None:
133
+ data = budget_summary_dict()
134
+
135
+ explicit_budget = 0
136
+ explicit_used = 0
137
+ for record in _snapshot_records():
138
+ if record.namespace is None and record.flop_budget >= _GLOBAL_DEFAULT_BUDGET:
139
+ explicit_used += record.flops_used
140
+ else:
141
+ explicit_budget += record.flop_budget
142
+ explicit_used += record.flops_used
143
+
144
+ has_explicit_budget = explicit_budget > 0
145
+ if has_explicit_budget:
146
+ return {
147
+ "has_explicit_budget": True,
148
+ "budget": explicit_budget,
149
+ "used": explicit_used,
150
+ "remaining": explicit_budget - explicit_used,
151
+ "color": _usage_color(explicit_used, explicit_budget),
152
+ }
153
+
154
+ return {
155
+ "has_explicit_budget": False,
156
+ "budget": data["flop_budget"],
157
+ "used": data["flops_used"],
158
+ "remaining": data["flops_remaining"],
159
+ "color": "green",
160
+ }
161
+
162
+
163
+ def _rich_namespace_table(label: str, ns_data: dict, color: str):
164
+ """Build a Rich Table for a single namespace bucket."""
165
+ from rich.table import Table
166
+ from rich.text import Text
167
+
168
+ table = Table(
169
+ show_header=True,
170
+ header_style="bold",
171
+ title_style="bold",
172
+ expand=True,
173
+ padding=(0, 1),
174
+ )
175
+ table.add_column("Operation", style="dim")
176
+ table.add_column("FLOPs", justify="right")
177
+ table.add_column("%", justify="right")
178
+ table.add_column("Backend", justify="right", style="dim")
179
+ table.add_column("Overhead", justify="right", style="dim")
180
+ table.add_column("Calls", justify="right", style="dim")
181
+
182
+ ops = ns_data.get("operations", {})
183
+ total = ns_data.get("flop_budget", ns_data.get("flops_used", 0))
184
+ for op_name, op_info in sorted(ops.items(), key=lambda item: -item[1]["flop_cost"]):
185
+ backend = op_info.get("flopscope_backend_time_s", 0.0)
186
+ overhead = op_info.get("flopscope_overhead_time_s", 0.0)
187
+ table.add_row(
188
+ op_name,
189
+ _format_flops(op_info["flop_cost"]),
190
+ _pct(op_info["flop_cost"], total),
191
+ f"{backend:.3f}s" if backend > 0 else "",
192
+ f"{overhead:.3f}s" if overhead > 0 else "",
193
+ _call_label(op_info["calls"]),
194
+ )
195
+
196
+ table.add_section()
197
+ table.add_row(
198
+ Text("Used", style="bold"),
199
+ Text(_format_flops(ns_data.get("flops_used", 0)), style=f"bold {color}"),
200
+ Text(_pct(ns_data.get("flops_used", 0), total), style=color),
201
+ "",
202
+ "",
203
+ "",
204
+ )
205
+ if "flop_budget" in ns_data:
206
+ remaining = ns_data["flop_budget"] - ns_data.get("flops_used", 0)
207
+ table.add_row(
208
+ Text("Remaining", style="bold"),
209
+ Text(_format_flops(remaining), style="bold"),
210
+ "",
211
+ "",
212
+ "",
213
+ "",
214
+ )
215
+
216
+ return table
217
+
218
+
219
+ def _rich_totals_table(data: dict):
220
+ from rich.table import Table
221
+ from rich.text import Text
222
+
223
+ table = Table(show_header=False, expand=True, padding=(0, 1), box=None)
224
+ table.add_column("label", style="bold")
225
+ table.add_column("value", justify="right")
226
+
227
+ totals = _display_totals(data)
228
+ if totals["has_explicit_budget"]:
229
+ table.add_row("Budget", _format_flops(totals["budget"]))
230
+ table.add_row(
231
+ "Used",
232
+ Text(
233
+ (
234
+ f"{_format_flops(totals['used'])} ({_pct(totals['used'], totals['budget'])})"
235
+ if totals["has_explicit_budget"]
236
+ else _format_flops(totals["used"])
237
+ ),
238
+ style=totals["color"],
239
+ ),
240
+ )
241
+ if totals["has_explicit_budget"]:
242
+ table.add_row(
243
+ "Remaining",
244
+ f"{_format_flops(totals['remaining'])} ({_pct(totals['remaining'], totals['budget'])})",
245
+ )
246
+
247
+ wall_time = data.get("wall_time_s")
248
+ backend_time = data.get("flopscope_backend_time_s", 0.0)
249
+ overhead_time = data.get("flopscope_overhead_time_s", 0.0)
250
+ residual_wall_time = data.get("residual_wall_time_s")
251
+ if wall_time is not None and residual_wall_time is not None:
252
+ table.add_section()
253
+ table.add_row("Total Wall Time", f"{wall_time:.3f}s")
254
+ table.add_row(
255
+ "Flopscope Backend",
256
+ Text(
257
+ f"{backend_time:.3f}s ({_pct(backend_time, wall_time)})",
258
+ style="dim",
259
+ ),
260
+ )
261
+ table.add_row(
262
+ "Flopscope Overhead",
263
+ Text(
264
+ f"{overhead_time:.3f}s ({_pct(overhead_time, wall_time)})",
265
+ style="dim",
266
+ ),
267
+ )
268
+ table.add_row(
269
+ "Residual Wall Time",
270
+ Text(
271
+ f"{residual_wall_time:.3f}s ({_pct(residual_wall_time, wall_time)})",
272
+ style="dim",
273
+ ),
274
+ )
275
+
276
+ return table
277
+
278
+
279
+ def _rich_attribution_table(by_ns: dict[str | None, dict], total_flops_used: int):
280
+ from rich.table import Table
281
+
282
+ table = Table(
283
+ title="By namespace",
284
+ show_header=True,
285
+ header_style="bold",
286
+ expand=True,
287
+ padding=(0, 1),
288
+ )
289
+ table.add_column("Namespace")
290
+ table.add_column("FLOPs", justify="right")
291
+ table.add_column("%", justify="right")
292
+ table.add_column("Calls", justify="right")
293
+ table.add_column("Backend", justify="right")
294
+ table.add_column("Overhead", justify="right")
295
+
296
+ for namespace, bucket in _sorted_namespace_rows(by_ns):
297
+ table.add_row(
298
+ _namespace_label(namespace),
299
+ _format_flops(bucket["flops_used"]),
300
+ _pct(bucket["flops_used"], total_flops_used),
301
+ str(bucket["calls"]),
302
+ f"{bucket['flopscope_backend_time_s']:.3f}s",
303
+ f"{bucket['flopscope_overhead_time_s']:.3f}s",
304
+ )
305
+
306
+ return table
307
+
308
+
309
+ def _rich_operations_table(ops: dict[str, dict], total_flops_used: int):
310
+ from rich.table import Table
311
+
312
+ table = Table(
313
+ title="By operation",
314
+ show_header=True,
315
+ header_style="bold",
316
+ expand=True,
317
+ padding=(0, 1),
318
+ )
319
+ table.add_column("Operation", style="dim")
320
+ table.add_column("FLOPs", justify="right")
321
+ table.add_column("%", justify="right")
322
+ table.add_column("Backend", justify="right", style="dim")
323
+ table.add_column("Overhead", justify="right", style="dim")
324
+ table.add_column("Calls", justify="right", style="dim")
325
+
326
+ for op_name, op_info in sorted(ops.items(), key=lambda item: -item[1]["flop_cost"]):
327
+ backend = op_info.get("flopscope_backend_time_s", 0.0)
328
+ overhead = op_info.get("flopscope_overhead_time_s", 0.0)
329
+ table.add_row(
330
+ op_name,
331
+ _format_flops(op_info["flop_cost"]),
332
+ _pct(op_info["flop_cost"], total_flops_used),
333
+ f"{backend:.3f}s" if backend > 0 else "",
334
+ f"{overhead:.3f}s" if overhead > 0 else "",
335
+ _call_label(op_info["calls"]),
336
+ )
337
+
338
+ return table
339
+
340
+
341
+ def _rich_summary(by_namespace: bool = False):
342
+ """Render a Rich-formatted budget summary."""
343
+ from rich.console import Group
344
+ from rich.panel import Panel
345
+
346
+ data = budget_summary_dict(by_namespace=by_namespace)
347
+ if data["flops_used"] == 0 and not data.get("operations"):
348
+ return Panel("No budget data recorded yet.", title="flopscope Budget")
349
+
350
+ renderables = [_rich_totals_table(data)]
351
+ if by_namespace and data.get("by_namespace"):
352
+ renderables.append(
353
+ _rich_attribution_table(data["by_namespace"], data["flops_used"])
354
+ )
355
+ if data.get("operations"):
356
+ renderables.append(
357
+ _rich_operations_table(data["operations"], data["flops_used"])
358
+ )
359
+
360
+ return Panel(
361
+ Group(*renderables),
362
+ title="[bold cyan]flopscope FLOP Budget Summary[/bold cyan]",
363
+ border_style="cyan",
364
+ )
365
+
366
+
367
+ def render_budget_summary(by_namespace: bool = False):
368
+ """Return a Rich renderable if Rich is installed, otherwise plain text."""
369
+ try:
370
+ import rich # noqa: F401
371
+
372
+ return _rich_summary(by_namespace=by_namespace)
373
+ except ImportError:
374
+ return _plain_text_summary(by_namespace=by_namespace)
375
+
376
+
377
+ class _PlainTextLive:
378
+ """Fallback live display that prints summary on exit."""
379
+
380
+ def __init__(self, by_namespace: bool = False):
381
+ self._by_namespace = by_namespace
382
+
383
+ def __enter__(self):
384
+ return self
385
+
386
+ def __exit__(self, *args):
387
+ print(_plain_text_summary(by_namespace=self._by_namespace))
388
+ return None
389
+
390
+
391
+ def budget_live(by_namespace: bool = False):
392
+ """Return a live-updating budget summary context manager.
393
+
394
+ Parameters
395
+ ----------
396
+ by_namespace : bool, optional
397
+ If ``True``, include the namespace breakdown in the live display.
398
+ Default ``False``.
399
+
400
+ Returns
401
+ -------
402
+ object
403
+ A context manager that refreshes the session-wide budget summary while
404
+ the ``with`` block is active. When Rich is installed this is a
405
+ live-rendered display; otherwise it falls back to a plain-text summary
406
+ printed when the block exits.
407
+
408
+ Examples
409
+ --------
410
+ >>> import flopscope as flops
411
+ >>> import flopscope.numpy as fnp
412
+ >>> with flops.budget_live():
413
+ ... with flops.BudgetContext(flop_budget=100):
414
+ ... _ = fnp.add(fnp.array([1.0]), fnp.array([2.0]))
415
+ """
416
+ try:
417
+ from rich.live import Live
418
+
419
+ class _RichBudgetLive:
420
+ def __init__(self, by_namespace: bool):
421
+ self._by_namespace = by_namespace
422
+ self._live = None
423
+
424
+ def __enter__(self):
425
+ self._live = Live(
426
+ _rich_summary(by_namespace=self._by_namespace),
427
+ refresh_per_second=2,
428
+ )
429
+ self._live.__enter__()
430
+ return self
431
+
432
+ def __exit__(self, *args):
433
+ if self._live is not None:
434
+ self._live.update(_rich_summary(by_namespace=self._by_namespace))
435
+ self._live.__exit__(*args)
436
+ return None
437
+
438
+ return _RichBudgetLive(by_namespace)
439
+ except ImportError:
440
+ return _PlainTextLive(by_namespace=by_namespace)
441
+
442
+
443
+ def budget_summary(by_namespace: bool = False):
444
+ """Render the session-wide budget summary.
445
+
446
+ Parameters
447
+ ----------
448
+ by_namespace : bool, optional
449
+ If ``True``, include the namespace breakdown in the rendered summary.
450
+ Default ``False``.
451
+
452
+ Returns
453
+ -------
454
+ object or None
455
+ In notebook-style environments, returns the Rich renderable or plain
456
+ text summary object. In a standard terminal, prints the summary and
457
+ returns ``None``.
458
+
459
+ Examples
460
+ --------
461
+ >>> import flopscope as flops
462
+ >>> import flopscope.numpy as fnp
463
+ >>> with flops.BudgetContext(flop_budget=100):
464
+ ... _ = fnp.add(fnp.array([1.0]), fnp.array([2.0]))
465
+ >>> flops.budget_summary()
466
+ """
467
+ result = render_budget_summary(by_namespace=by_namespace)
468
+ try:
469
+ _ = get_ipython # type: ignore[name-defined] # noqa: F821
470
+ return result
471
+ except NameError:
472
+ if isinstance(result, str):
473
+ print(result)
474
+ else:
475
+ from rich.console import Console
476
+
477
+ Console().print(result)
478
+ return None
@@ -0,0 +1,59 @@
1
+ """Docstring inheritance helper for flopscope wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def attach_docstring(wrapper, np_func, category: str, cost_description: str) -> None:
7
+ """Attach NumPy's docstring to a flopscope wrapper with a FLOP Cost section.
8
+
9
+ Inserts a dedicated "FLOP Cost" section after the summary line(s),
10
+ before the Parameters section. This keeps cost info prominent and
11
+ separate from NumPy's own Notes.
12
+ """
13
+ np_doc = getattr(np_func, "__doc__", None) or ""
14
+
15
+ cost_section = f"FLOP Cost\n---------\n{cost_description}\n"
16
+
17
+ if not np_doc:
18
+ wrapper.__doc__ = (
19
+ f"Counted wrapper for ``numpy.{np_func.__name__}``.\n\n{cost_section}"
20
+ )
21
+ return
22
+
23
+ # Find the first standard section header (Parameters, Returns, etc.)
24
+ # and insert the cost section before it.
25
+ lines = np_doc.split("\n")
26
+ section_headers = {
27
+ "Parameters",
28
+ "Returns",
29
+ "Raises",
30
+ "See Also",
31
+ "Notes",
32
+ "References",
33
+ "Examples",
34
+ "Yields",
35
+ "Warns",
36
+ "Other Parameters",
37
+ "Attributes",
38
+ "Methods",
39
+ }
40
+
41
+ insert_idx = None
42
+ for i, line in enumerate(lines):
43
+ stripped = line.strip()
44
+ if (
45
+ stripped in section_headers
46
+ and i + 1 < len(lines)
47
+ and lines[i + 1].strip().startswith("---")
48
+ ):
49
+ insert_idx = i
50
+ break
51
+
52
+ if insert_idx is not None:
53
+ # Insert cost section before the first standard section
54
+ before = "\n".join(lines[:insert_idx]).rstrip()
55
+ after = "\n".join(lines[insert_idx:])
56
+ wrapper.__doc__ = f"{before}\n\n{cost_section}\n{after}"
57
+ else:
58
+ # No standard sections found — append cost section
59
+ wrapper.__doc__ = f"{np_doc.rstrip()}\n\n{cost_section}"
flopscope/_dtypes.py ADDED
@@ -0,0 +1,20 @@
1
+ """NumPy dtype types re-exported as free (0-FLOP) attributes.
2
+
3
+ These are plain aliases — they perform no FLOP tracking because they
4
+ are type objects, not operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as _np
10
+
11
+ # Abstract dtype hierarchy
12
+ floating = _np.floating
13
+ integer = _np.integer
14
+ number = _np.number
15
+ dtype = _np.dtype
16
+
17
+ # Missing unsigned integer dtypes (uint8 is already exported from __init__.py)
18
+ uint16 = _np.uint16
19
+ uint32 = _np.uint32
20
+ uint64 = _np.uint64