counted-float 1.0.2__tar.gz → 1.0.4__tar.gz

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 (94) hide show
  1. {counted_float-1.0.2 → counted_float-1.0.4}/.gitignore +4 -1
  2. counted_float-1.0.2/README.md → counted_float-1.0.4/PKG-INFO +54 -9
  3. counted_float-1.0.2/PKG-INFO → counted_float-1.0.4/README.md +33 -30
  4. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/_counted_float.py +93 -8
  5. {counted_float-1.0.2 → counted_float-1.0.4}/pyproject.toml +20 -7
  6. {counted_float-1.0.2 → counted_float-1.0.4}/LICENSE +0 -0
  7. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/__init__.py +0 -0
  8. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/__init__.py +0 -0
  9. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/_cli.py +0 -0
  10. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/__init__.py +0 -0
  11. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/counted_float/__init__.py +0 -0
  12. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/counted_float/_counted_float_benchmark.py +0 -0
  13. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/flops/__init__.py +0 -0
  14. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/flops/_array_generator.py +0 -0
  15. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/flops/_flops_benchmark_suite.py +0 -0
  16. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/flops/_flops_micro_benchmark.py +0 -0
  17. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/micro/__init__.py +0 -0
  18. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/benchmarking/micro/_micro_benchmark.py +0 -0
  19. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/compatibility/__init__.py +0 -0
  20. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/compatibility/_numba.py +0 -0
  21. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/__init__.py +0 -0
  22. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/_builtin_data.py +0 -0
  23. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/_context_managers.py +0 -0
  24. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/_global_counter.py +0 -0
  25. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/config/__init__.py +0 -0
  26. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/config/_config.py +0 -0
  27. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/counting/config/_defaults.py +0 -0
  28. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/__init__.py +0 -0
  29. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_base.py +0 -0
  30. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flop_counts.py +0 -0
  31. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flop_type.py +0 -0
  32. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flop_weights.py +0 -0
  33. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flops_benchmark_meta_data.py +0 -0
  34. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flops_benchmark_result.py +0 -0
  35. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_flops_benchmark_type.py +0 -0
  36. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_instruction_latencies.py +0 -0
  37. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/models/_micro_benchmark_result.py +0 -0
  38. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/__init__.py +0 -0
  39. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_cpu_freq.py +0 -0
  40. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_formatting.py +0 -0
  41. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_geo_mean.py +0 -0
  42. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_latency.py +0 -0
  43. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_missing_data.py +0 -0
  44. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_rounding.py +0 -0
  45. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/_core/utils/_timer.py +0 -0
  46. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/benchmarking/__init__.py +0 -0
  47. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/config/__init__.py +0 -0
  48. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/benchmarks/apple_m1_github_actions.json +0 -0
  49. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/benchmarks/apple_m3_max_mbp16.json +0 -0
  50. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/benchmarks/apple_m3_mba15.json +0 -0
  51. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/benchmarks/aws_graviton_2_neoverse_n1_ec2_m6g_xlarge.json +0 -0
  52. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/benchmarks/aws_graviton_3_neoverse_v1_ec2_m7g_xlarge.json +0 -0
  53. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/specs/arm_cortex_a76.json +0 -0
  54. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/specs/arm_cortex_x1.json +0 -0
  55. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/specs/arm_neoverse_n1.json +0 -0
  56. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v8_x/specs/arm_neoverse_v1.json +0 -0
  57. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/benchmarks/aws_graviton_4_neoverse_v2_ec2_m8g_xlarge.json +0 -0
  58. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/benchmarks/azure_cobalt_100_neoverse_n2_github_actions.json +0 -0
  59. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/specs/arm_cortex_x2.json +0 -0
  60. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/specs/arm_cortex_x3.json +0 -0
  61. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/specs/arm_neoverse_n2.json +0 -0
  62. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_0/specs/arm_neoverse_v2.json +0 -0
  63. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_2/benchmarks/apple_m4_pro_mbp16.json +0 -0
  64. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_2/specs/arm_cortex_x4.json +0 -0
  65. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_2/specs/arm_cortex_x925.json +0 -0
  66. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/arm/v9_2/specs/arm_neoverse_v3.json +0 -0
  67. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2017_zen1/benchmarks/amd_ryzen_1700x.json +0 -0
  68. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2017_zen1/other/analysis_uops_info_zen1+.json +0 -0
  69. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2020_zen3/benchmark/amd_epyc_7763_linux_github.json +0 -0
  70. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2020_zen3/benchmark/amd_epyc_7763_windows_github.json +0 -0
  71. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2020_zen3/other/analysis_agner_fog_r7_5800x.json +0 -0
  72. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2020_zen3/other/analysis_uops_info_zen3.json +0 -0
  73. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2022_zen4/benchmark/amd_epyc_9r14_ec2_m7a_xlarge.json +0 -0
  74. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2022_zen4/other/analysis_agner_fog_r9_7900x.json +0 -0
  75. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2022_zen4/other/analysis_uops_info_zen4.json +0 -0
  76. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2022_zen4/other/specs_amd.json +0 -0
  77. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2024_zen5/benchmark/amd_epyc_9r45_ec2_m8a_xlarge.json +0 -0
  78. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2024_zen5/other/analysis_agner_fog_r7_9800x3d.json +0 -0
  79. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/amd/2024_zen5/other/specs_amd.json +0 -0
  80. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2017_coffee_lake_gen_8/benchmarks/intel_i7_8550U_windows.json +0 -0
  81. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2017_coffee_lake_gen_8/benchmarks/intel_i7_8700B_macos_github_actions.json +0 -0
  82. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2017_coffee_lake_gen_8/other/analysis_agner_fog_coffee_lake.json +0 -0
  83. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2017_coffee_lake_gen_8/other/analysis_uops_info_coffee_lake.json +0 -0
  84. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2019_sunny_cove_gen_10/benchmarks/intel_xeon_8375c_ice_lake_ec2_m6i_xlarge.json +0 -0
  85. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2019_sunny_cove_gen_10/other/analysis_agner_fog_ice_lake.json +0 -0
  86. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2019_sunny_cove_gen_10/other/analysis_uops_info_ice_lake.json +0 -0
  87. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2019_sunny_cove_gen_10/other/analysis_uops_info_tiger_lake.json +0 -0
  88. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2021_golden_cove_gen_12/benchmarks/intel_xeon_8488c_sapphire_rapids_ec2_m7i_xlarge.json +0 -0
  89. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2021_golden_cove_gen_12/other/analysis_uops_info_alder_lake_p.json +0 -0
  90. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2021_golden_cove_gen_12/other/specs_intel.json +0 -0
  91. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2022_raptor_cove_gen_13_14/benchmarks/intel_xeon_8559c_emerald_rapids_ec2_i7i_xlarge.json +0 -0
  92. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2022_raptor_cove_gen_13_14/other/specs_intel.json +0 -0
  93. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2023_redwood_cove_ultra_1/benchmarks/intel_xeon_6975p_granite_rapids_ec2_m8i_xlarge.json +0 -0
  94. {counted_float-1.0.2 → counted_float-1.0.4}/counted_float/data/x86/intel/2023_redwood_cove_ultra_1/other/specs_intel.json +0 -0
@@ -33,4 +33,7 @@ Thumbs.db
33
33
  .vscode
34
34
 
35
35
  # other local files
36
- .local
36
+ .local
37
+ # Only working_docs/README.md is committed; everything else here stays out of git.
38
+ working_docs/*
39
+ !working_docs/README.md
@@ -1,9 +1,28 @@
1
- <!--START_SECTION:images-->
1
+ Metadata-Version: 2.4
2
+ Name: counted-float
3
+ Version: 1.0.4
4
+ Summary: Count floating-point operations in Python code & benchmark relative flop costs.
5
+ Project-URL: Source, https://github.com/bertpl/counted-float
6
+ Project-URL: ChangeLog, https://github.com/bertpl/counted-float/blob/main/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/bertpl/counted-float/issues
8
+ Project-URL: Roadmap, https://github.com/bertpl/counted-float/milestones
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: numpy>=1.20
12
+ Requires-Dist: psutil>=5.0
13
+ Requires-Dist: py-cpuinfo>=9.0.0
14
+ Requires-Dist: pydantic>=2.0.0
15
+ Requires-Dist: rich>=13.0.0
16
+ Provides-Extra: cli
17
+ Requires-Dist: click>=8.0.0; extra == 'cli'
18
+ Provides-Extra: numba
19
+ Requires-Dist: numba>=0.50; extra == 'numba'
20
+ Description-Content-Type: text/markdown
21
+
2
22
  ![shields.io-python-versions](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
3
- ![genbadge-test-count](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/badge-test-count.svg)
4
- ![genbadge-test-coverage](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/badge-coverage.svg)
5
- ![counted_float logo](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/splash.webp)
6
- <!--END_SECTION:images-->
23
+ ![genbadge-test-count](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/badge-test-count.svg)
24
+ ![genbadge-test-coverage](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/badge-coverage.svg)
25
+ ![counted_float logo](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/splash.webp)
7
26
 
8
27
  # counted-float
9
28
 
@@ -71,7 +90,33 @@ s = math.sqrt(cf1) # s = CountedFloat(0.9)
71
90
  is_float = isinstance(s, float) # True
72
91
  ```
73
92
 
74
- ## 2.2. FLOP counting context managers
93
+ ### The counting model: what gets counted and why
94
+
95
+ The counting model is a contract with two sides:
96
+
97
+ - **Your side:** wrap every *runtime input* of the algorithm you want to measure in `CountedFloat`
98
+ at its boundary. Contagion does the rest — everything derived from those inputs stays counted
99
+ automatically.
100
+ - **The library's side:** count every FLOP that a compiled (C/Rust/...) port of your algorithm
101
+ would execute *on data derived from those inputs*.
102
+
103
+ From this contract follows a clean rule for everything else: **constants are free.** Any plain
104
+ float encountered mid-computation is, by the contract, not an input — so it must be a constant of
105
+ the algorithm (a literal, a coefficient, a tolerance), and operations purely among constants are
106
+ work a compiled port would fold at compile time or precompute. This is why e.g. `math.sqrt(3)`
107
+ counts nothing: the port ships `sqrt(3)` as a precomputed constant.
108
+
109
+ The library detects constants through two mechanisms, applying the same rule:
110
+
111
+ - **Unwrapped values** (plain floats): constants by the wrapping contract, as above.
112
+ - **`int` operands**: evidence of a *hardcoded* constant — ints don't fall out of floating-point
113
+ computations, so an int operand almost certainly appears literally in your source. This enables
114
+ counting the strength reductions a compiled port would apply: `x**2` counts MUL (i.e. `x*x`),
115
+ `2**x` counts EXP2, `math.log(x, 10)` counts LOG10 — while `x**2.0`, with a float that *could*
116
+ be a runtime value, conservatively counts a generic POW.
117
+
118
+ The flip side: an unwrapped runtime input is invisible to the counter — that is a wrapping error
119
+ at your algorithm's boundary, not something the library can detect. When in doubt, wrap.
75
120
 
76
121
  Once we use the `CountedFloat` class, we can use the available context managers to count the number of
77
122
  flops performed by `CountedFloat` objects.
@@ -617,21 +662,21 @@ This appendix provides detailed information about how each floating-point operat
617
662
  - Relevant CPU instructions
618
663
  - **ARM:** (software)
619
664
  - **x86:** (software)
620
- - **Counted Python operations:** `math.log(x)` for `CountedFloat`
665
+ - **Counted Python operations:** `math.log(x)` for `CountedFloat`; `math.log(x, base)` for `CountedFloat` decomposes per the constant-detection heuristic (int base 2/10 -> LOG2/LOG10; other int base -> LOG+MUL; float base -> LOG per counted operand + DIV)
621
666
  - **Not counted:** `numpy.log`, log on non-CountedFloat
622
667
 
623
668
  ### FlopType.LOG2 (`log2(x)`)
624
669
  - Relevant CPU instructions
625
670
  - **ARM:** (software)
626
671
  - **x86:** (software)
627
- - **Counted Python operations:** `math.log2(x)` for `CountedFloat`
672
+ - **Counted Python operations:** `math.log2(x)` for `CountedFloat`; `math.log(x, 2)` (int base) for `CountedFloat`
628
673
  - **Not counted:** `numpy.log2`, log2 on non-CountedFloat
629
674
 
630
675
  ### FlopType.LOG10 (`log10(x)`)
631
676
  - Relevant CPU instructions
632
677
  - **ARM:** (software)
633
678
  - **x86:** (software)
634
- - **Counted Python operations:** `math.log10(x)` for `CountedFloat`
679
+ - **Counted Python operations:** `math.log10(x)` for `CountedFloat`; `math.log(x, 10)` (int base) for `CountedFloat`
635
680
  - **Not counted:** `numpy.log10`, log10 on non-CountedFloat
636
681
 
637
682
  ### FlopType.POW (`x^y`)
@@ -1,30 +1,7 @@
1
- Metadata-Version: 2.4
2
- Name: counted-float
3
- Version: 1.0.2
4
- Summary: Count floating-point operations in Python code & benchmark relative flop costs.
5
- Project-URL: Source, https://github.com/bertpl/counted-float
6
- Project-URL: ChangeLog, https://github.com/bertpl/counted-float/blob/develop/CHANGELOG.md
7
- Project-URL: Issues, https://github.com/bertpl/counted-float/issues
8
- Project-URL: Roadmap, https://github.com/bertpl/counted-float/milestones
9
- License-File: LICENSE
10
- Requires-Python: >=3.11
11
- Requires-Dist: numpy>=1.20
12
- Requires-Dist: psutil>=5.0
13
- Requires-Dist: py-cpuinfo>=9.0.0
14
- Requires-Dist: pydantic>=2.0.0
15
- Requires-Dist: rich>=13.0.0
16
- Provides-Extra: cli
17
- Requires-Dist: click>=8.0.0; extra == 'cli'
18
- Provides-Extra: numba
19
- Requires-Dist: numba>=0.50; extra == 'numba'
20
- Description-Content-Type: text/markdown
21
-
22
- <!--START_SECTION:images-->
23
1
  ![shields.io-python-versions](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)
24
- ![genbadge-test-count](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/badge-test-count.svg)
25
- ![genbadge-test-coverage](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/badge-coverage.svg)
26
- ![counted_float logo](https://bertpl.github.io/counted-float/version_artifacts/v1.0.2/splash.webp)
27
- <!--END_SECTION:images-->
2
+ ![genbadge-test-count](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/badge-test-count.svg)
3
+ ![genbadge-test-coverage](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/badge-coverage.svg)
4
+ ![counted_float logo](https://bertpl.github.io/counted-float/version_artifacts/release/v1.0.4/splash.webp)
28
5
 
29
6
  # counted-float
30
7
 
@@ -92,7 +69,33 @@ s = math.sqrt(cf1) # s = CountedFloat(0.9)
92
69
  is_float = isinstance(s, float) # True
93
70
  ```
94
71
 
95
- ## 2.2. FLOP counting context managers
72
+ ### The counting model: what gets counted and why
73
+
74
+ The counting model is a contract with two sides:
75
+
76
+ - **Your side:** wrap every *runtime input* of the algorithm you want to measure in `CountedFloat`
77
+ at its boundary. Contagion does the rest — everything derived from those inputs stays counted
78
+ automatically.
79
+ - **The library's side:** count every FLOP that a compiled (C/Rust/...) port of your algorithm
80
+ would execute *on data derived from those inputs*.
81
+
82
+ From this contract follows a clean rule for everything else: **constants are free.** Any plain
83
+ float encountered mid-computation is, by the contract, not an input — so it must be a constant of
84
+ the algorithm (a literal, a coefficient, a tolerance), and operations purely among constants are
85
+ work a compiled port would fold at compile time or precompute. This is why e.g. `math.sqrt(3)`
86
+ counts nothing: the port ships `sqrt(3)` as a precomputed constant.
87
+
88
+ The library detects constants through two mechanisms, applying the same rule:
89
+
90
+ - **Unwrapped values** (plain floats): constants by the wrapping contract, as above.
91
+ - **`int` operands**: evidence of a *hardcoded* constant — ints don't fall out of floating-point
92
+ computations, so an int operand almost certainly appears literally in your source. This enables
93
+ counting the strength reductions a compiled port would apply: `x**2` counts MUL (i.e. `x*x`),
94
+ `2**x` counts EXP2, `math.log(x, 10)` counts LOG10 — while `x**2.0`, with a float that *could*
95
+ be a runtime value, conservatively counts a generic POW.
96
+
97
+ The flip side: an unwrapped runtime input is invisible to the counter — that is a wrapping error
98
+ at your algorithm's boundary, not something the library can detect. When in doubt, wrap.
96
99
 
97
100
  Once we use the `CountedFloat` class, we can use the available context managers to count the number of
98
101
  flops performed by `CountedFloat` objects.
@@ -638,21 +641,21 @@ This appendix provides detailed information about how each floating-point operat
638
641
  - Relevant CPU instructions
639
642
  - **ARM:** (software)
640
643
  - **x86:** (software)
641
- - **Counted Python operations:** `math.log(x)` for `CountedFloat`
644
+ - **Counted Python operations:** `math.log(x)` for `CountedFloat`; `math.log(x, base)` for `CountedFloat` decomposes per the constant-detection heuristic (int base 2/10 -> LOG2/LOG10; other int base -> LOG+MUL; float base -> LOG per counted operand + DIV)
642
645
  - **Not counted:** `numpy.log`, log on non-CountedFloat
643
646
 
644
647
  ### FlopType.LOG2 (`log2(x)`)
645
648
  - Relevant CPU instructions
646
649
  - **ARM:** (software)
647
650
  - **x86:** (software)
648
- - **Counted Python operations:** `math.log2(x)` for `CountedFloat`
651
+ - **Counted Python operations:** `math.log2(x)` for `CountedFloat`; `math.log(x, 2)` (int base) for `CountedFloat`
649
652
  - **Not counted:** `numpy.log2`, log2 on non-CountedFloat
650
653
 
651
654
  ### FlopType.LOG10 (`log10(x)`)
652
655
  - Relevant CPU instructions
653
656
  - **ARM:** (software)
654
657
  - **x86:** (software)
655
- - **Counted Python operations:** `math.log10(x)` for `CountedFloat`
658
+ - **Counted Python operations:** `math.log10(x)` for `CountedFloat`; `math.log(x, 10)` (int base) for `CountedFloat`
656
659
  - **Not counted:** `numpy.log10`, log10 on non-CountedFloat
657
660
 
658
661
  ### FlopType.POW (`x^y`)
@@ -182,7 +182,15 @@ class CountedFloat(float):
182
182
  return CountedFloat(super().__rtruediv__(other))
183
183
 
184
184
  def __pow__(self, other) -> CountedFloat:
185
- """x**other"""
185
+ """
186
+ x**other
187
+
188
+ Counting heuristic: an `int` operand is taken as evidence of a hardcoded constant in the
189
+ source (ints don't fall out of floating-point computations), so `x**2` counts as MUL —
190
+ the strength reduction (x*x) a compiled port would apply. A float operand may just as well
191
+ be a runtime variable that happens to hold that value, where a port would compile a
192
+ generic pow, so `x**2.0` counts as POW.
193
+ """
186
194
  if isinstance(other, int) and other == 2:
187
195
  GLOBAL_COUNTER.incr_mul() # x^2 = x*x
188
196
  else:
@@ -192,7 +200,14 @@ class CountedFloat(float):
192
200
  return CountedFloat(super().__pow__(other))
193
201
 
194
202
  def __rpow__(self, other) -> CountedFloat:
195
- """other**x"""
203
+ """
204
+ other**x
205
+
206
+ Same constant-detection heuristic as __pow__, applied to the base: an `int` base 2 or 10
207
+ is taken as a hardcoded constant, counting as EXP2 / EXP10 (the strength reduction a
208
+ compiled port would apply); a float base may be a runtime variable, so it counts as
209
+ generic POW.
210
+ """
196
211
  if isinstance(other, int) and other == 2:
197
212
  GLOBAL_COUNTER.incr_exp2()
198
213
  elif isinstance(other, int) and other == 10:
@@ -214,10 +229,15 @@ original_math_log2 = math.log2
214
229
  original_math_log10 = math.log10
215
230
  original_math_exp = math.exp
216
231
  original_math_exp2 = math.exp2
232
+ original_math_pow = math.pow
217
233
  original_math_sin = math.sin
218
234
  original_math_cos = math.cos
219
235
  original_math_tan = math.tan
220
236
 
237
+ # sentinel for math_log's optional base argument; the stdlib signature is math.log(x[, base]),
238
+ # where omitting base is not the same as passing any real value (and None is rejected)
239
+ _NO_BASE = object()
240
+
221
241
 
222
242
  def math_sqrt(x: float) -> float | CountedFloat:
223
243
  if isinstance(x, CountedFloat):
@@ -235,12 +255,50 @@ def math_cbrt(x: float) -> float | CountedFloat:
235
255
  return original_math_cbrt(x)
236
256
 
237
257
 
238
- def math_log(x: float) -> float | CountedFloat:
239
- if isinstance(x, CountedFloat):
240
- GLOBAL_COUNTER.incr_log()
241
- return CountedFloat(original_math_log(x))
258
+ def math_log(x: float, base=_NO_BASE) -> float | CountedFloat:
259
+ """
260
+ Patched math.log: stdlib contract (optional base), with flop classification for the base
261
+ following the same constant-detection heuristic as CountedFloat.__pow__ / __rpow__
262
+ (int operand = hardcoded constant in the source):
263
+ - base omitted -> LOG
264
+ - base int 2 / 10 -> LOG2 / LOG10 (a compiled port calls log2/log10 directly)
265
+ - base other int -> LOG + MUL (a port computes log(x) * C, with C = 1/log(base) folded
266
+ at compile time)
267
+ - base float -> a port computes log(x)/log(base): LOG per CountedFloat operand + DIV
268
+ As everywhere in the counting model, only operations touching CountedFloat values are counted:
269
+ any runtime input to the counted algorithm should itself be a CountedFloat; whatever remains a
270
+ plain float is by definition not part of the core algorithm and/or precomputable.
271
+ """
272
+ if base is _NO_BASE:
273
+ if isinstance(x, CountedFloat):
274
+ GLOBAL_COUNTER.incr_log()
275
+ return CountedFloat(original_math_log(x))
276
+ else:
277
+ return original_math_log(x)
242
278
  else:
243
- return original_math_log(x)
279
+ # computed first: raises per stdlib contract before anything is counted
280
+ result = original_math_log(x, base)
281
+ if isinstance(base, int) and base == 2:
282
+ if isinstance(x, CountedFloat):
283
+ GLOBAL_COUNTER.incr_log2()
284
+ elif isinstance(base, int) and base == 10:
285
+ if isinstance(x, CountedFloat):
286
+ GLOBAL_COUNTER.incr_log10()
287
+ elif isinstance(base, int):
288
+ if isinstance(x, CountedFloat):
289
+ GLOBAL_COUNTER.incr_log()
290
+ GLOBAL_COUNTER.incr_mul()
291
+ else:
292
+ if isinstance(x, CountedFloat):
293
+ GLOBAL_COUNTER.incr_log()
294
+ if isinstance(base, CountedFloat):
295
+ GLOBAL_COUNTER.incr_log()
296
+ if isinstance(x, CountedFloat) or isinstance(base, CountedFloat):
297
+ GLOBAL_COUNTER.incr_div()
298
+ if isinstance(x, CountedFloat) or isinstance(base, CountedFloat):
299
+ return CountedFloat(result)
300
+ else:
301
+ return result
244
302
 
245
303
 
246
304
  def math_log2(x: float) -> float | CountedFloat:
@@ -276,7 +334,34 @@ def math_exp2(x: float) -> float | CountedFloat:
276
334
 
277
335
 
278
336
  def math_pow(x: float, y: float) -> float | CountedFloat:
279
- return x**y
337
+ """
338
+ Patched math.pow: stdlib contract (always-float result, ValueError on domain errors), with
339
+ flop classification identical to the x**y form — including the constant-detection heuristic
340
+ documented on CountedFloat.__pow__ / __rpow__ (int operand = hardcoded constant).
341
+ """
342
+ if isinstance(x, CountedFloat) or isinstance(y, CountedFloat):
343
+ # computed first: math.pow raises ValueError on domain errors (e.g. negative base with
344
+ # fractional exponent) and then nothing should be counted
345
+ result = original_math_pow(x, y)
346
+ if isinstance(x, CountedFloat):
347
+ if isinstance(y, int) and y == 2:
348
+ GLOBAL_COUNTER.incr_mul() # x^2 = x*x
349
+ else:
350
+ if isinstance(y, int):
351
+ GLOBAL_COUNTER.incr_i2f()
352
+ GLOBAL_COUNTER.incr_pow()
353
+ else:
354
+ if isinstance(x, int) and x == 2:
355
+ GLOBAL_COUNTER.incr_exp2()
356
+ elif isinstance(x, int) and x == 10:
357
+ GLOBAL_COUNTER.incr_exp10()
358
+ else:
359
+ if isinstance(x, int):
360
+ GLOBAL_COUNTER.incr_i2f()
361
+ GLOBAL_COUNTER.incr_pow()
362
+ return CountedFloat(result)
363
+ else:
364
+ return original_math_pow(x, y)
280
365
 
281
366
 
282
367
  def math_sin(x: float) -> float | CountedFloat:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "counted-float"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "Count floating-point operations in Python code & benchmark relative flop costs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -17,7 +17,7 @@ counted_float = "counted_float._core._cli:cli"
17
17
 
18
18
  [project.urls]
19
19
  Source = "https://github.com/bertpl/counted-float"
20
- ChangeLog = "https://github.com/bertpl/counted-float/blob/develop/CHANGELOG.md"
20
+ ChangeLog = "https://github.com/bertpl/counted-float/blob/main/CHANGELOG.md"
21
21
  Issues = "https://github.com/bertpl/counted-float/issues"
22
22
  Roadmap = "https://github.com/bertpl/counted-float/milestones"
23
23
 
@@ -29,6 +29,14 @@ cli = [
29
29
  "click>=8.0.0",
30
30
  ]
31
31
 
32
+ [dependency-groups]
33
+ dev = [
34
+ "genbadge[all]>=1.1.2",
35
+ "pytest>=8.0",
36
+ "pytest-cov>=6.0",
37
+ "pytest-xdist>=3.5.0",
38
+ ]
39
+
32
40
  [build-system]
33
41
  requires = ["hatchling"]
34
42
  build-backend = "hatchling.build"
@@ -48,9 +56,14 @@ target-version = "py313"
48
56
  [tool.ruff.lint]
49
57
  select = ["I"]
50
58
 
51
- [dependency-groups]
52
- dev = [
53
- "genbadge[all]>=1.1.2",
54
- "pytest>=8.0",
55
- "pytest-cov>=6.0",
59
+ [tool.coverage.run]
60
+ source = ["counted_float"]
61
+
62
+ [tool.coverage.report]
63
+ exclude_also = [
64
+ "@abstractmethod",
65
+ "@abc.abstractmethod",
56
66
  ]
67
+
68
+ [tool.pytest.ini_options]
69
+ addopts = "-n auto --dist worksteal"
File without changes