beqforge 0.3.0__tar.gz → 0.3.2__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 (236) hide show
  1. {beqforge-0.3.0 → beqforge-0.3.2}/.github/workflows/build-device-check.yml +1 -1
  2. {beqforge-0.3.0 → beqforge-0.3.2}/.github/workflows/build-executable.yml +2 -2
  3. {beqforge-0.3.0 → beqforge-0.3.2}/.github/workflows/tests.yml +3 -3
  4. {beqforge-0.3.0 → beqforge-0.3.2}/PKG-INFO +1 -1
  5. {beqforge-0.3.0 → beqforge-0.3.2}/TODO.md +5 -3
  6. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/__init__.py +1 -1
  7. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/accept.py +46 -9
  8. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/designer.py +4 -2
  9. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/e10.py +92 -14
  10. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/fit_kernel.py +20 -3
  11. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/impact.py +1 -1
  12. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/impact.toml +28 -0
  13. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/pipeline.py +27 -4
  14. {beqforge-0.3.0 → beqforge-0.3.2}/negative_corpus.json +327 -207
  15. {beqforge-0.3.0 → beqforge-0.3.2}/packaging/designer/Dockerfile +3 -0
  16. {beqforge-0.3.0 → beqforge-0.3.2}/packaging/designer/smoke.py +40 -9
  17. beqforge-0.3.2/plans/E10-validation-protocol.md +684 -0
  18. beqforge-0.3.2/plans/bass-ceiling.md +144 -0
  19. {beqforge-0.3.0 → beqforge-0.3.2}/plans/design-version.md +1 -0
  20. {beqforge-0.3.0 → beqforge-0.3.2}/plans/fit-kernel.md +10 -1
  21. {beqforge-0.3.0 → beqforge-0.3.2}/tests/fixtures/design_golden.json +24 -30
  22. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_accept.py +25 -3
  23. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_evidence.py +20 -1
  24. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_pipeline.py +12 -0
  25. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_playback.py +5 -1
  26. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_strategy_config.py +4 -1
  27. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_e10.py +79 -1
  28. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_fit_kernel.py +20 -0
  29. {beqforge-0.3.0 → beqforge-0.3.2}/tools/e10_cases.py +2 -2
  30. {beqforge-0.3.0 → beqforge-0.3.2}/uv.lock +20 -20
  31. beqforge-0.3.0/plans/E10-validation-protocol.md +0 -327
  32. {beqforge-0.3.0 → beqforge-0.3.2}/.dockerignore +0 -0
  33. {beqforge-0.3.0 → beqforge-0.3.2}/.gitattributes +0 -0
  34. {beqforge-0.3.0 → beqforge-0.3.2}/.github/dependabot.yml +0 -0
  35. {beqforge-0.3.0 → beqforge-0.3.2}/.github/workflows/build-designer-image.yml +0 -0
  36. {beqforge-0.3.0 → beqforge-0.3.2}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  37. {beqforge-0.3.0 → beqforge-0.3.2}/.gitignore +0 -0
  38. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/.gitignore +0 -0
  39. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/beqforge.iml +0 -0
  40. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/editor.xml +0 -0
  41. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/jsonSchemas.xml +0 -0
  42. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/misc.xml +0 -0
  43. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/modules.xml +0 -0
  44. {beqforge-0.3.0 → beqforge-0.3.2}/.idea/vcs.xml +0 -0
  45. {beqforge-0.3.0 → beqforge-0.3.2}/AGENTS.md +0 -0
  46. {beqforge-0.3.0 → beqforge-0.3.2}/CLAUDE.md +0 -0
  47. {beqforge-0.3.0 → beqforge-0.3.2}/LICENCE.md +0 -0
  48. {beqforge-0.3.0 → beqforge-0.3.2}/README.md +0 -0
  49. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/AGENTS.md +0 -0
  50. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/biquad.py +0 -0
  51. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/provenance.py +0 -0
  52. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/publication.py +0 -0
  53. {beqforge-0.3.0 → beqforge-0.3.2}/beq_common/types.py +0 -0
  54. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/AGENTS.md +0 -0
  55. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/__init__.py +0 -0
  56. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/adjustments.py +0 -0
  57. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/beqd.py +0 -0
  58. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/biquad.py +0 -0
  59. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/cache.py +0 -0
  60. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/charts.py +0 -0
  61. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/cli.py +0 -0
  62. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/dc_offset.py +0 -0
  63. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/design.py +0 -0
  64. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/diagnose.py +0 -0
  65. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/explain.py +0 -0
  66. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/extraction.py +0 -0
  67. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/filters.py +0 -0
  68. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/harness.py +0 -0
  69. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/identify.py +0 -0
  70. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/material.py +0 -0
  71. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/record.py +0 -0
  72. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/reference.py +0 -0
  73. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/rolloff.py +0 -0
  74. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge/verify.py +0 -0
  75. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge-device-check.spec +0 -0
  76. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge.spec +0 -0
  77. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/AGENTS.md +0 -0
  78. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/__init__.py +0 -0
  79. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/analyse.py +0 -0
  80. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/audio.py +0 -0
  81. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/catalogue.py +0 -0
  82. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/cli.py +0 -0
  83. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/coefficients.py +0 -0
  84. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/engines.py +0 -0
  85. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/evidence.py +0 -0
  86. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/manifest.py +0 -0
  87. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/measurement.py +0 -0
  88. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/profiles.py +0 -0
  89. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/qualification.py +0 -0
  90. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/report.py +0 -0
  91. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/transactions.py +0 -0
  92. {beqforge-0.3.0 → beqforge-0.3.2}/beqforge_device_check/verify.py +0 -0
  93. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/AGENTS.md +0 -0
  94. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/__init__.py +0 -0
  95. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/cache.py +0 -0
  96. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/cli.py +0 -0
  97. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/core.py +0 -0
  98. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/data/seed-manifest.json +0 -0
  99. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/data/seed.json.gz +0 -0
  100. {beqforge-0.3.0 → beqforge-0.3.2}/beqoptimiser/parameters.py +0 -0
  101. {beqforge-0.3.0 → beqforge-0.3.2}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  102. {beqforge-0.3.0 → beqforge-0.3.2}/docs/device-check-examples/simulation.json +0 -0
  103. {beqforge-0.3.0 → beqforge-0.3.2}/docs/device-check.md +0 -0
  104. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/README.md +0 -0
  105. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  106. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  107. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/assets/example-1.png +0 -0
  108. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/assets/example-2.png +0 -0
  109. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/entry-results.json.gz +0 -0
  110. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser-report/statistics.json +0 -0
  111. {beqforge-0.3.0 → beqforge-0.3.2}/docs/optimiser.md +0 -0
  112. {beqforge-0.3.0 → beqforge-0.3.2}/evidence_validation.json +0 -0
  113. {beqforge-0.3.0 → beqforge-0.3.2}/packaging/designer/beqforge-designer.cmd +0 -0
  114. {beqforge-0.3.0 → beqforge-0.3.2}/packaging/device-check/NOTICE.txt +0 -0
  115. {beqforge-0.3.0 → beqforge-0.3.2}/plans/F2-catalogue-filter-optimisation.md +0 -0
  116. {beqforge-0.3.0 → beqforge-0.3.2}/plans/F2-device-precision-validation.md +0 -0
  117. {beqforge-0.3.0 → beqforge-0.3.2}/plans/F2-package-consolidation.md +0 -0
  118. {beqforge-0.3.0 → beqforge-0.3.2}/plans/R2a-server-stage-cache.md +0 -0
  119. {beqforge-0.3.0 → beqforge-0.3.2}/plans/contract-1.5-adjustments.md +0 -0
  120. {beqforge-0.3.0 → beqforge-0.3.2}/plans/default-record-and-log-locations.md +0 -0
  121. {beqforge-0.3.0 → beqforge-0.3.2}/plans/done-baseline-evidence.md +0 -0
  122. {beqforge-0.3.0 → beqforge-0.3.2}/plans/done-design-and-pipeline.md +0 -0
  123. {beqforge-0.3.0 → beqforge-0.3.2}/plans/e10-titles-v1.json +0 -0
  124. {beqforge-0.3.0 → beqforge-0.3.2}/plans/fit-weighting.md +0 -0
  125. {beqforge-0.3.0 → beqforge-0.3.2}/plans/library-review-improvements.md +0 -0
  126. {beqforge-0.3.0 → beqforge-0.3.2}/plans/library-v5-review.md +0 -0
  127. {beqforge-0.3.0 → beqforge-0.3.2}/plans/parameter-space-optimisation.md +0 -0
  128. {beqforge-0.3.0 → beqforge-0.3.2}/plans/playback-contract.md +0 -0
  129. {beqforge-0.3.0 → beqforge-0.3.2}/plans/redesign-scope.md +0 -0
  130. {beqforge-0.3.0 → beqforge-0.3.2}/plans/research-design-decisions.md +0 -0
  131. {beqforge-0.3.0 → beqforge-0.3.2}/plans/test-portability-and-runtime.md +0 -0
  132. {beqforge-0.3.0 → beqforge-0.3.2}/plans/texture-yardstick-sub-band.md +0 -0
  133. {beqforge-0.3.0 → beqforge-0.3.2}/plans/texture-yardstick-sub-band.patch +0 -0
  134. {beqforge-0.3.0 → beqforge-0.3.2}/pyproject.toml +0 -0
  135. {beqforge-0.3.0 → beqforge-0.3.2}/tests/__init__.py +0 -0
  136. {beqforge-0.3.0 → beqforge-0.3.2}/tests/conftest.py +0 -0
  137. {beqforge-0.3.0 → beqforge-0.3.2}/tests/fixtures/optimiser_seed_entry.json +0 -0
  138. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_cli.py +0 -0
  139. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_adjustments.py +0 -0
  140. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_cache.py +0 -0
  141. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_cache_stores.py +0 -0
  142. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_channel_evidence.py +0 -0
  143. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_charts.py +0 -0
  144. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_corpus.py +0 -0
  145. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_dc_offset.py +0 -0
  146. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_design.py +0 -0
  147. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_designer.py +0 -0
  148. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_designer_server.py +0 -0
  149. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_device_rate.py +0 -0
  150. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_diagnose.py +0 -0
  151. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_evidence_excess.py +0 -0
  152. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_exclusions.py +0 -0
  153. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_explain.py +0 -0
  154. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_extraction.py +0 -0
  155. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_filters.py +0 -0
  156. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_frozen_cache.py +0 -0
  157. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_golden.py +0 -0
  158. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_guard.py +0 -0
  159. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_harness.py +0 -0
  160. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_headroom.py +0 -0
  161. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_hum.py +0 -0
  162. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_identify.py +0 -0
  163. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_impact.py +0 -0
  164. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_jacobian.py +0 -0
  165. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_parallel.py +0 -0
  166. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_playback_headroom.py +0 -0
  167. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_publication.py +0 -0
  168. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_record.py +0 -0
  169. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_record_fingerprint.py +0 -0
  170. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_reference.py +0 -0
  171. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_references.py +0 -0
  172. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_ripple_band.py +0 -0
  173. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_rolloff.py +0 -0
  174. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_target_fit.py +0 -0
  175. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_timings.py +0 -0
  176. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_design_verify.py +0 -0
  177. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_designer_image.py +0 -0
  178. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_designer_server_startup.py +0 -0
  179. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_analyse.py +0 -0
  180. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_catalogue.py +0 -0
  181. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_catalogue_predict.py +0 -0
  182. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_cli.py +0 -0
  183. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_engines.py +0 -0
  184. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_evidence.py +0 -0
  185. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_examples.py +0 -0
  186. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_manifest.py +0 -0
  187. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_measurement.py +0 -0
  188. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_qualification.py +0 -0
  189. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_transactions.py +0 -0
  190. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_device_check_verify.py +0 -0
  191. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_extract_layout.py +0 -0
  192. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_optimiser.py +0 -0
  193. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_optimiser_cache.py +0 -0
  194. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_optimiser_parameters.py +0 -0
  195. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_optimiser_report.py +0 -0
  196. {beqforge-0.3.0 → beqforge-0.3.2}/tests/test_package_boundaries.py +0 -0
  197. {beqforge-0.3.0 → beqforge-0.3.2}/tools/__init__.py +0 -0
  198. {beqforge-0.3.0 → beqforge-0.3.2}/tools/build_optimiser_seed.py +0 -0
  199. {beqforge-0.3.0 → beqforge-0.3.2}/tools/design_beq.py +0 -0
  200. {beqforge-0.3.0 → beqforge-0.3.2}/tools/designer_server.py +0 -0
  201. {beqforge-0.3.0 → beqforge-0.3.2}/tools/e10_run.py +0 -0
  202. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/a1_sweep.py +0 -0
  203. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/compare_records.py +0 -0
  204. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/compare_verdicts.py +0 -0
  205. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/inject_variants.py +0 -0
  206. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p14_compare.py +0 -0
  207. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p14_jacobian.py +0 -0
  208. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p14_run_pipeline.py +0 -0
  209. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p14_surrogate.py +0 -0
  210. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_compare_greedy.py +0 -0
  211. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_compare_seeded.py +0 -0
  212. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_compare_structure.py +0 -0
  213. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_greedy.py +0 -0
  214. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_seeded.py +0 -0
  215. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p18_structure.py +0 -0
  216. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/p20_smooth_drift.py +0 -0
  217. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/parameter_benchmark.py +0 -0
  218. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/probe.py +0 -0
  219. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/refit_probe.py +0 -0
  220. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/request_timing.py +0 -0
  221. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/score_injected.py +0 -0
  222. {beqforge-0.3.0 → beqforge-0.3.2}/tools/experiments/t8_floor_rules.py +0 -0
  223. {beqforge-0.3.0 → beqforge-0.3.2}/tools/extract.py +0 -0
  224. {beqforge-0.3.0 → beqforge-0.3.2}/tools/impact.py +0 -0
  225. {beqforge-0.3.0 → beqforge-0.3.2}/tools/ledger_template.html +0 -0
  226. {beqforge-0.3.0 → beqforge-0.3.2}/tools/negative_corpus.py +0 -0
  227. {beqforge-0.3.0 → beqforge-0.3.2}/tools/optimiser_catalogue_report.py +0 -0
  228. {beqforge-0.3.0 → beqforge-0.3.2}/tools/prepare_device_check_helper.py +0 -0
  229. {beqforge-0.3.0 → beqforge-0.3.2}/tools/render_ledger.py +0 -0
  230. {beqforge-0.3.0 → beqforge-0.3.2}/tools/replay.py +0 -0
  231. {beqforge-0.3.0 → beqforge-0.3.2}/tools/seed_optimiser_cache.py +0 -0
  232. {beqforge-0.3.0 → beqforge-0.3.2}/tools/smoke_test_device_check.py +0 -0
  233. {beqforge-0.3.0 → beqforge-0.3.2}/tools/smoke_test_exe.py +0 -0
  234. {beqforge-0.3.0 → beqforge-0.3.2}/tools/smoke_test_install.py +0 -0
  235. {beqforge-0.3.0 → beqforge-0.3.2}/tools/summarise.py +0 -0
  236. {beqforge-0.3.0 → beqforge-0.3.2}/tools/validate_evidence.py +0 -0
@@ -61,7 +61,7 @@ jobs:
61
61
  executable: beqforge-device-check
62
62
  steps:
63
63
  - uses: actions/checkout@v7
64
- - uses: astral-sh/setup-uv@v10.3.0
64
+ - uses: astral-sh/setup-uv@v10.3.2
65
65
  with:
66
66
  enable-cache: true
67
67
  - name: Install Linux audio and USB libraries
@@ -112,7 +112,7 @@ jobs:
112
112
  contents: read
113
113
  steps:
114
114
  - uses: actions/checkout@v7
115
- - uses: astral-sh/setup-uv@v10.3.0
115
+ - uses: astral-sh/setup-uv@v10.3.2
116
116
  with:
117
117
  enable-cache: true
118
118
  - name: Build wheel and source distribution
@@ -201,7 +201,7 @@ jobs:
201
201
  - uses: actions/checkout@v7
202
202
 
203
203
  - name: Install uv
204
- uses: astral-sh/setup-uv@v10.3.0
204
+ uses: astral-sh/setup-uv@v10.3.2
205
205
  with:
206
206
  enable-cache: true
207
207
 
@@ -67,7 +67,7 @@ jobs:
67
67
  run: choco install ffmpeg -y
68
68
 
69
69
  - name: Install uv
70
- uses: astral-sh/setup-uv@v10.3.0
70
+ uses: astral-sh/setup-uv@v10.3.2
71
71
  with:
72
72
  enable-cache: true
73
73
 
@@ -100,7 +100,7 @@ jobs:
100
100
  - uses: actions/checkout@v7
101
101
 
102
102
  - name: Install uv
103
- uses: astral-sh/setup-uv@v10.3.0
103
+ uses: astral-sh/setup-uv@v10.3.2
104
104
  with:
105
105
  enable-cache: true
106
106
 
@@ -131,7 +131,7 @@ jobs:
131
131
  profile: [base, designer, optimiser, device-check, all]
132
132
  steps:
133
133
  - uses: actions/checkout@v7
134
- - uses: astral-sh/setup-uv@v10.3.0
134
+ - uses: astral-sh/setup-uv@v10.3.2
135
135
  with:
136
136
  enable-cache: true
137
137
  - name: Build the single distribution
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Audio-derived BEQ design, published filter optimisation and DSP validation
5
5
  License-File: LICENCE.md
6
6
  Requires-Python: <3.15,>=3.13
@@ -28,7 +28,7 @@ ID alone (AGENTS.md says where each lives), and the stale contract versions are
28
28
  | --- | --- | --- | --- |
29
29
  | 1 | Playback contract | released in 0.2.10 (design version 6) | The sub feed is now built from the request's `bass_management` (one LR4 for "Before"/"After", none for "Off", WCS or numeric headroom gains); `clip_before`/`clip_after` are named in the clipping line, not simulated. Default path unchanged (probe tol 0). On the 11 titles no verdict or winner moves; the clipping figure does, by up to 1.8 dB (Black Bag's gain reduction 1.40 to 3.16 dB). Caller propagation was already done in beqdesigner. See [the outcome](plans/playback-contract.md#outcome). |
30
30
  | 2 | Texture yardstick band | open (E10 only) | Implemented: the mix's ripple is measured about a cubic over the unchanged band, so the LFE knee is trend. 9 (2009), Beauty and the Beast, Crazy, Stupid, Love and Despicable Me 3 now accept; Carrie stays declined. Probe and protocol unchanged; corpus 0/45 gate pass. Neither width-normalised form separated `broadband/5`. The degree was chosen on corpus and library data, so it needs the fresh E10 validation before it counts as settled; see [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
31
- | 3 | E10 | instrument v3; first development run done | The [protocol](plans/E10-validation-protocol.md) is predeclared and its instrument built: synthetic families and scorer (`beqforge/e10.py`, `tools/e10_run.py`), the real-texture half (`tools/e10_cases.py`), the title partition frozen. The [first development run](plans/E10-validation-protocol.md#first-development-run-22-october-2026) corrected the instrument to v2: the deficit is read against each case's own source, overshoot needs both readings, and device failure means an unrealisable cascade. Under v2 the negatives hold; a pumping floor is lifted 27-33 dB (row 33). v3 [settles the tilted goal's pivot](plans/E10-validation-protocol.md#the-tilted-goals-pivot-v3-22-october-2026): a gentle rolloff tilts the content's recovery, not the track, so the goal pivots where the case's deficit ends, as the designer's does; gentle overshoots 7/36 → 1/36. Development follow-ups from the v3 rescore: `finite_stopband/2` still overshoots at the gentle goal (pivot 32.6 Hz; is it the designer, or E10's deficit reading against the judged top?); `cancellation/1,2` now need a correction at the gentle goal and abstain, and `natural_droop/2` turns from an overshoot into a 0.84 dB miss (see [v3](plans/E10-validation-protocol.md#the-tilted-goals-pivot-v3-22-october-2026)). Before any held-out run: decide whether the cancellation recipe's corner moves clear of the reference (its 60 Hz corner leaves a +2-3 dB bump at 40-56 Hz that reaches the 80-160 Hz reference; about 40 Hz would clear it). Then freeze the first change it gates (row 33 is the candidate). |
31
+ | 3 | E10 | instrument v4; ready for its first gated change | The [protocol](plans/E10-validation-protocol.md) is predeclared and its instrument built: synthetic families and scorer (`beqforge/e10.py`, `tools/e10_run.py`), the real-texture half (`tools/e10_cases.py`), the title partition frozen. The [first development run](plans/E10-validation-protocol.md#first-development-run-22-october-2026) corrected the instrument to v2, and v3 [settled the tilted goal's pivot](plans/E10-validation-protocol.md#the-tilted-goals-pivot-v3-22-october-2026). [v4](plans/E10-validation-protocol.md#version-8-baseline-and-v4-28-october-2026) reran the development seeds at design version 8 and settled the rest: the cancellation recipe cancels in phase (its causal low-pass added +3-3.8 dB at 40-71 Hz); `finite_stopband/2`'s gentle overshoot is the designer's (0.25 of its 0.65 dB is pivot scatter); overshoot and missed recovery use a 1.5 dB accuracy bound at every goal, the goal tolerance being the pipeline's act threshold (tight scored 11 overshoots of ordinary fit ripple); and the decision rule gates on no safety count getting worse, with 5% a reported target. Under v4 at version 8, flat: noise lift 4/15, overshoot 5/36, missed 21/24, median shortfall 0.21 dB. v5 replaced the pumping floor (row 33). Row 33's candidate, bounding lift by the evidence, was [measured in three forms and not adopted](plans/E10-validation-protocol.md#bounding-lift-by-the-evidence-three-experiments-none-adopted-29-october-2026). Next: choose another first change for it to gate, then the held-out run. |
32
32
  | 4 | E1 | open | Add record-only scene-held-out verification: evidence independent of the material that generated the target. A gate needs a separate demonstrated benefit. |
33
33
  | 5 | Steep-filter scoring | open | Implement coloured/codec-like floors and recipe-specific truth bands under E10; inspect shape failures and published-ceiling excess. Existing titles can supply programme texture. |
34
34
  | 6 | Steep-filter default | blocked | Recovery improves substantially with `--content-edge`, but changing the default needs E10, the scoring above, listening judgements and more independent real steep titles. Safe House already works with the default; keep the option off meanwhile. |
@@ -47,7 +47,7 @@ ID alone (AGENTS.md says where each lives), and the stale contract versions are
47
47
  | 20 | Protective corner (former TODO 9) | parked | The corner is already derived from boost cap and implied order, floored by `lowest_frequency_hz`. No case requires an independent override or different fallback alignment; reopen with one. |
48
48
  | 21 | Device configuration on the wire | parked | The current contract deliberately omits device preference; server flags already configure realisation. Reopen if one server must serve different device formats/rates per request; then agree an upstream extension. Hardware validity remains F2. |
49
49
  | 22 | Catalogue comparison (former TODO 8) | parked | A disagreement detector has no demonstrated accuracy benefit. Reopen with an independent discrepancy to investigate; authored filters remain comparisons, never calibration targets. |
50
- | 23 | Performance | fit kernel released in 0.3.0 (design version 8) | [The measurement and outcome](plans/fit-kernel.md#outcome). The fit's search now scores each DE generation at once in the linear domain (`beqforge/fit_kernel.py`; numba, numpy fallback, bit-identical). Real runs on the 11 titles: 703 to 283 s wall, 1905 to 373 CPU-s; the `fit` stage 10x, so a title is 28 s and 37 CPU-s. A re-roll: refit probe no worse on balance, no baseline title's winner or verdict changed in real runs, protocol and corpus unchanged where gated. **Left:** the fit re-rolls near the top of the band as freely as at 10 Hz (row 37: weighting the search works, held back behind row 36); measure a library sample for the impact basis and replace it in 0.3.1 (0.3.0's is mostly injected variants); CI's Windows and macOS executables with numba. Still parked: reuse of repeated numerical identities, the ~9 s a title outside the pipeline (measure on the server path), an own DE loop. GPU offload rejected. Missed-fit search belongs to F3/F4. |
50
+ | 23 | Performance | fit kernel released in 0.3.0 (design version 8); 0.3.1 fixes the designer image run as another user | [The measurement and outcome](plans/fit-kernel.md#outcome). The fit's search now scores each DE generation at once in the linear domain (`beqforge/fit_kernel.py`; numba, numpy fallback, bit-identical). Real runs on the 11 titles: 703 to 283 s wall, 1905 to 373 CPU-s; the `fit` stage 10x, so a title is 28 s and 37 CPU-s. A re-roll: refit probe no worse on balance, no baseline title's winner or verdict changed in real runs, protocol and corpus unchanged where gated. **Left:** the fit re-rolls near the top of the band as freely as at 10 Hz (row 37: weighting the search works, held back behind row 36); measure a library sample for the impact basis and replace it in a later 0.3 release (0.3.0's is mostly injected variants); CI's Windows and macOS executables with numba. Still parked: reuse of repeated numerical identities, the ~9 s a title outside the pipeline (measure on the server path), an own DE loop. GPU offload rejected. Missed-fit search belongs to F3/F4. |
51
51
  | 24 | Chart peak bias | parked | Deliberate catalogue-compatible per-bin maximum; it affects presentation only. Reopen if a reader needs an alternative percentile view; keep it distinct from the 95th-percentile evidence envelope. |
52
52
  | 26 | Texture yardstick retune (library review) | blocked on E10 | The cubic (row 2) keeps the 200 Hz band only because the sub-band cap admitted one synthetic negative, `broadband/5`: chance unevenness in noise bursts, at a 2.6 dB boost. With 45 gated cases a single case moves the gate's upper bound from 0.079 to 0.118. Carrie is still declined (0.85x about a cubic, 1.33x with the cap). The reopening trigger (the library review) is met and the first measurement is done ([research item 4](plans/library-review-improvements.md#4-the-texture-retune-row-26)): the trend degree does nothing; ending the band at 80 Hz clears five of the seven review declines but lets five more gated negatives past this rule and costs three filtered cases their passband. Its full corpus run lets in exactly `broadband/5` (a false acceptance at a 2.5 dB boost; gate 0/45 to 1/45, upper bound 0.118, FAIL) and moves nothing else, `filtered` included ([the run](plans/library-review-improvements.md#4-the-texture-retune-row-26)). So the open question is the boost-weighted gate, and E10 (row 3) before adopting any retune. Retune against its declines: the band edge, the trend degree, and whether the gate weights a false acceptance by its boost. The library holds no known negative, so it can expose wrong declines but not measure false acceptances. Any retune still needs the corpus or a fresh E10 set for that, ideally with negatives built from real programme texture (row 5). See [the outcome](plans/texture-yardstick-sub-band.md#outcome-17-october-2026). |
53
53
  | 28 | Fit sensitivity to arithmetic | released in 0.2.10 (design version 6) | Identification now converges to 1e-12 and the fit gets its target and ceiling quantised to 0.01 dB. The refit probe gives the same filters natively and as an emulated older CPU (0 of 11 titles moved; 28 Years Later's parametric moved before). Protocol false acceptances unchanged; corpus gate pass, `filtered` 7/9 (was 6/9). The 11 titles' corrected curves stay within 1.3 dB, though three swap between near-tied winners ([outcome](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6)). CI's verdict on the golden: `counterfactual` matches on Linux, Windows and macOS and is back in it; `parametric` matches on the x86 runners that disagreed in 0.2.6, but macOS arm64 publishes the same filters with figures 0.06 dB apart, so it stays out ([verdict](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6)). Left: find where arm64's parametric fit parts from x86's (needs an arm64 machine: compare the identified target, the quantised fit input and the fitted cascade unrounded) and either close the gap or accept it and say so in the golden's docstring; until then an Apple-silicon caller can see parametric figures a few hundredths of a dB from an x86 one. From design version 8 (row 23's kernel) `counterfactual` on the golden's rolloff title also publishes one step away on arm64 (36.25 Hz against 36.24, figures within tolerance), so the golden skips that case on arm64 too. Ship before the version 5 redesign (row 31) so the library is redesigned once. |
@@ -55,11 +55,13 @@ ID alone (AGENTS.md says where each lives), and the stale contract versions are
55
55
  | 30 | Contract 1.5: design adjustments | released in 0.2.9; one gap parked | beqdesigner can ask for a different design per title: ignore a band, a floor, a boost cap, a tilted goal, a note. `beqforge/adjustments.py` answers each; default answers unchanged (golden; probe `--tol 0`, all 11 titles). See [the outcome](plans/contract-1.5-adjustments.md#outcome). Only gap: a note's text is not written into the run record (the record holds the adjusted params, so the design replays). Reopen if a review needs the note from the record. |
56
56
  | 31 | Review the version 5 library redesign | open: handoff received 23 October 2026; group 1 released in 0.2.10 (design version 6) | Outcome and evidence: [the v5 review](plans/library-v5-review.md). beqdesigner's v5 rerun (job `ef4265fa`, 1,083 titles, about 1,000 designed and 69 declined; 18 titles v4 designed now decline, about half wrongly) is triaged into four groups in `/media/beq/triage/handoff-v5.md` (queue ids, v5 records and earlier runs per title). 1 (built): O2 refused a plateau starting just under 10 Hz on the knee of a real rolloff (A Quiet Place: Day One, Elysium, Pan); it now keeps such a plateau when it reaches 20 Hz and the shortfall beneath exceeds the passband's ripple, and all three design. Probe, protocol and corpus unchanged. 2: a whole candidate rejected for missing one check by a fraction: cliff by under 3 dB/oct (Heartburn, Back to the Future, Guardians of the Galaxy, The Commitments), tilt (Guardians, The Exorcist, Alien Resurrection, Philadelphia), level (Ray), F9 residual just below the content edge (Drive, Amy). The library has no negatives, so a tolerance it suggests still goes through the protocol and E10, never the catalogue. 3: every candidate corrected only down to above the content edge (Return to Seoul, Spirited Away, The Jungle Book, Bugonia). 4: known, rows 5/6 (steep filter; `judge_from_content_edge` is a server flag, not on the wire), F5 (Eastern Promises, Fame, Home Alone) and row 26. Earlier scope of this row: 0.2.9 reports design version 5, so beqdesigner's out-of-date filter flags every title designed before it. Review the redesign's declines and changed designs from their run records, as the first review was (row 29): confirm the fixes' titles moved as measured, rerun the open cases (row 29's tilt titles, the near-texture watch cases Black Bag, Bugonia and Incredible Hulk), and collect real-title evidence for rows 26 and 29's research. It can show wrong declines but not false acceptances (the library has no known negatives), so a retune it suggests still goes through E10. Do row 28 first if both machines will design. Records are kept by default from 0.2.10 (row 27, done). |
57
57
  | 32 | Fit optimum selection | part built (design version 7, not released); strategy choice still unstable | Found by row 28: reproducible is not stable. Changing the fit's input by at most 0.005 dB changed 24 winners on 54 titles and variants; Black Bag LR4 35's recovery fell from a 13.1 dB to a 3.4 dB peak. **Built (25 October 2026):** the fitter returns every fit within its 0.5 dB residual bar of its answer, and the pipeline keeps the first the judge passes, fewest sections first; an alternative must lift by more than the goal tolerance to replace the answer. Perturbing the fit input by 0.004 dB now flips 14 winners where HEAD flipped 19, 33 verdicts against 36, and no title swaps between designed and declined (HEAD 1). Black Bag LR4 35 goes to a 0.4 dB shortfall and holds; 13 of row 35's 14 new declines design again and none of its 17 gains is lost. Protocol unchanged, corpus gate passes. **Left:** most remaining flips are which strategy wins (flatten against a counterfactual on the steep variants, up to 38 dB apart below the content edge). The choice among strategies is untouched, and that is the next research step. See [the outcome](plans/library-review-improvements.md#row-32-choosing-among-near-equal-fits-25-october-2026-design_version-7) and [the finding](plans/library-review-improvements.md#built-a-reproducible-fit-input-22-october-2026-design_version-6). |
58
- | 33 | Noise lift on a pumping floor | open | E10's first development run: `nonstationary_floor` (a coloured floor 33-53 dB down that rises with the programme, as a compressor's pumping would) is lifted 27-33 dB below the programme's corner on all three seeds at every goal, where the same floors held stationary (`coloured_floor`) abstain. A floor that rises and falls with the programme tracks like programme, so the evidence model reads it as programme. A safety failure under the content principle, and the first change E10 could gate: find what distinguishes it (its level relative to the programme's scenes, its spectrum's continuity across the corner), change on development seeds only, judge on the held-out half. See [the run](plans/E10-validation-protocol.md#first-development-run-22-october-2026). |
58
+ | 33 | Noise lift beyond the evidence (found on a pumping floor) | open: the evidence bound was measured and not adopted; next, validate the floor detection | E10's first runs lifted `nonstationary_floor` 27-33 dB, but that recipe swung about 37 dB inside every event: event-locked content, matching real stopband leakage on contrast and coherence, so not a failure under the content principle ([v5](plans/E10-validation-protocol.md#the-pumping-floor-v5-28-october-2026) reports it as `event_locked_floor` and counts realistic 2-6 dB `tracking_floor` and `breathing_floor`). On seeds 1-12 under [v6](plans/E10-validation-protocol.md#the-skirt-and-the-cases-it-leaves-v6-28-october-2026) (noise lift counted beyond half an octave under the truth band), 4 of 72 realistic-floor rows lift noise. The two large ones are the judge passing the candidate that fits worst: on `breathing_floor/2` four three-section fits within 1.1-2.1 dB of target fail the cliff check, and a single shelf 6.4 dB off target and 5.5 dB over the evidence ceiling passes and is selected; on `breathing_floor/5` (gentle) a single section 9.0 dB over the ceiling at an unsupported bin is selected over a passing three-section fit 1.9 dB over. Nothing refuses a design for exceeding its evidence: the over-lift bar is measured from the goal, and `evidence_excess` is reported only. **Bounding lift by the evidence, not adopted (29 October 2026):** the fitter bound at the licence plus 1.5 dB (A), a judge check on any excess over 1.5 dB (B), and that check limited to excesses at least half an octave wide (C) were each built on branch `experiment-evidence-bound`, uncommitted. C cleared E10's counted noise lifts, but applied to the v8 library's stored `evidence_excess` it refuses the accepted design for 96 of 1,033 titles, and 14 of those would decline. Inspection showed that most of these are a smooth fit against a ragged licence on content that tracks the programme (Any Given Sunday, To Die For). The large lifts it catches below a diagnosed floor (Wait Until Dark, White Heat) sit inside a licence that allows 20-25 dB there, and C catches them only by an edge excess. **Lift below the diagnosed floor:** 247 of the 304 designed titles with measured bins under `noise_floor_hz` lift more than 3 dB there, and 151 more than 10 dB. Almost all are flatten, holding its lift flat, among them Moana (37 dB under 8.4 Hz) and White Heat (29 dB under 40 Hz). These are not counts of noise lift. `noise_floor_hz` is the mix tracking boundary, the first band whose envelope fails to track the passband, and failing that is not proof of noise. The contrast licence often disagrees with it (Wait Until Dark: 20 dB licensed below it). **On E10's truth** ([evidence](plans/E10-validation-protocol.md#is-the-tracking-boundary-a-noise-detector-29-october-2026)), the boundary sits 0.24-0.42 octave (median) below the true noise boundary on every floor family, up to 1.12 octave, and never above it: it calls the noise just above it content. It also misses half of `finite_stopband`'s exposed cases, and calls LFE-only content noise at 15-27 Hz on 3 of 12 `partial_channels`. **On the library**, an independent test calibrated on E10 (does the band's level scale with programme loudness, and is it steady) finds content under the boundary on 151 of 300 floored titles, among them Moana, Creed III and Juror #2, the largest lifts below it. Only 10 look like steady noise. The other 139, mostly older, neither scale nor stay steady, which these statistics cannot resolve. So the boundary is not fit to gate a rule, and its content-as-noise error also holds restorations flat that the content supports. Measured within 240 s stretches, only 27 of those 139 become steady noise; 85 stay bursty within minutes without scaling with the plateau, which is either content independent of the plateau or transient noise (splices, handling), and E10 has no transient noise to calibrate against. **Next:** listen to Wait Until Dark, Marnie and GoodFellas (bursty, not scaling) against Mary Poppins and The Exorcist (steady noise). If bursty and non-scaling proves to be noise, add a transient-noise family to E10. Then improve the floor detection itself, before deciding how a restoration should behave below a floor shown to be noise: hold flat, cap at the local target as the counterfactuals do, or return to 0 dB. [Evidence](plans/E10-validation-protocol.md#bounding-lift-by-the-evidence-three-experiments-none-adopted-29-october-2026). Related: rows 32, 36, E4/E9. |
59
59
  | 34 | Redesign scope | built and released in 0.2.12 (contract 1.6's `POST /impact`); beqdesigner's step 2 live check passed, step 4 verification next | The pipeline is a catalogue of 37 named features (`beqforge/impact.py`), each owning its code and reading from a run record how a design relied on it. Each answer-changing design version declares in `beqforge/impact.toml` which features it changed and how much that moved; `tools/impact.py check` refuses changed code no feature owns and undeclared answer-reaching changes (regression step 5). `POST /impact` scores up to 500 records (15 s over the share), and records from before 0.2.11 are identified by the release that wrote them, so the current library can be scored. Version 7's fit-choice degree was measured on the library on one machine: 18 of 34 post-fit declines moved, 7 of 15 designs with a failing candidate, 2 of 15 with all passing, 0 of 8 analysis declines. Against the library: 34 likely, 1,015 possible, 236 unaffected, 70 unknown (pre-design-version builds). It doesn't meet the plan's bar for this change (likely holds 18 of the sample's 27 movers): a change that re-rolls the fit can't be narrowed by outcome class. The ranking still holds: the top 30% hold about two thirds of the expected movers. Records now carry each verdict's margins and each fit `_choose_fit` had, so a threshold change is scored exactly (`will_move` or `unaffected`), and `unknown` (`no_figure`) only for older records or a losing candidate that would start passing; this changes no answer (rejudge probe 11 of 11 at `--tol 0`, refit probe 0 of 54 moved). Responses carry each change's `basis` and `unknown_reason` (agreed with beqdesigner). `impact.MOVED`'s cascade line is now 20/f dB, capped at 2 dB at 10 Hz and below, not a flat 1 dB. Version 7's designed classes were recounted on beqdesigner's 105-title sample: 16 of 35 with a failing candidate, 22 of 60 all passing. **Left:** beqdesigner's step 2 live check and step 4 verification sample (105 titles), and the check's from-the-results direction. See [the outcome](plans/redesign-scope.md#outcome). |
60
60
  | 35 | Review the version 6 library redesign | open: handoff received 24 October 2026; the 500 and record overwriting fixed in 0.2.11; the 14 new declines traced to row 28, 13 of them designing again on row 32 (version 7, unreleased) | beqdesigner's v6 rerun (job `e6a70f7e`, 1,287 titles): 1,016 designed and 270 declined. A Quiet Place: Day One, Elysium and Pan now design (row 31's O2 fix). 17 titles declined on v5 now design, and 14 designed on v5 now decline. Handoff: `/media/beq/triage/handoff-v6.md`; ledger `/media/beq/triage/ledger.jsonl`, stage `E`. **Fixed:** A Quiet Place Part II got a 500 every time, because `TargetFit.within_tolerance` came back as 1.0000000000000002. Zero-weight bins that miss the tolerance change how numpy pairs up a sum, so the in-tolerance sum could round above the full one. It is now met / (met + missed), which can't exceed 1; this code predates row 28. The figure is reporting only, so no design version: the title's filters are unchanged (low shelf 18.25 Hz +24.6 dB, peak 28.38 Hz −3.5 dB), and its response is now valid. Probe 11 of 11 unchanged at `--tol 0`; suite passes. **The 14 new declines (analysed 24 October 2026):** all 14 were rerun on one machine, at v0.2.9, at `f6dca94` (row 28 alone) and at HEAD, from requests identical to the library's (same digest). All 14 design on 0.2.9 and decline at `f6dca94`; HEAD agrees with `f6dca94`. So row 28 alone is the cause, and processor variance accounts for none of them. In every case the target is identical (0.000 dB apart) and the fit lands on a different cascade: row 28's 0.01 dB quantisation of the fit's input re-rolls the fitter's choice among near-equal optima (row 32). Except for TINA, the new fits are within 1 dB of the old by the fitter's own measure, worst-case error (Kung Fu Panda 4 0.4-0.9 dB worse, You've Got Mail 0.9 dB better), yet a judge check that depends on the error's shape flips: cliff (Platoon, The Manchurian Candidate, You've Got Mail, How to Marry a Millionaire), tilt (On Golden Pond, Christine, Twilight's counterfactual), ripple (Arrival, Kung Fu Panda 4), turnover (Magnolia, Zoolander), extent (Twilight's flatten) or slot (Rocky, Souleymane's Story). In TINA's case the fit itself is worse: 0.2.9's three-section fits drifted 2.1-3.0 dB on publication rounding, against a 3.0 limit, with their lowest sections about 4 quantisation steps from the device's DC limit. The new fits most likely drifted just past it, and the exposure rule fell back to one shelf fitting 4-6 dB off. No threshold should move on this evidence: the declines are as arbitrary as the 0.2.9 acceptances were, and the fix is row 32's, choosing among near-equal fits by what the judge checks rather than by which optimum the arithmetic lands on. **Slot check:** Rocky's and Souleymane's Story's new fits carry sections under 1 dB that the pruner keeps, because dropping them costs 1.02 and 1.12 dB of worst-case error against its 1 dB allowance. Their 0.2.9 two-section fits reached the same error without them. `designer-interface.md` §4 allows a rejection only for a reason internal to the evidence, which a redundant section is not, so the stated reason is wrong whatever the outcome. The outcome itself is doubtful too: both designs are 0.2-2.6 dB sections at 71-353 Hz, mostly above an 80 Hz crossover, not low-end restoration. The library's flip lists are not ground truth for row 34 by themselves (see [the proposal](plans/redesign-scope.md#validation-before-relying-on-it)): its v5 records were overwritten by v6's, because a record was named by its request's audio digest. **Fixed (0.2.11):** a record is now named by digests of the audio and of the rest of the request (coverage, bass management, adjustments, settings) and by the design version, and holds `design_version` and `design_settings` itself. A response's `run_record` (1.6, agreed in principle with beqdesigner) names it relative to the shared root. No answer changes. |
61
61
  | 36 | Is a design worth publishing at all? | open | The judge checks that a design does what its target asks, never whether the target is worth asking for. Only the target stage refuses a correction within the goal tolerance (1.5 dB), so anything that reaches the judge by another route can pass as a trivial design: row 32's search over near-equal fits accepted a single 0.98 dB shelf on the band-exclusion test until `pipeline._lifts_beyond_goal` gated alternatives. Rocky's and Souleymane's Story's v6 designs (row 35: 0.2-2.6 dB sections at 71-353 Hz, mostly above the crossover) are the real-title form of it. Decide whether the judge should refuse a design whose low-end lift stays within the goal tolerance, and with what reason under `designer-interface.md` §4. A decision change: protocol, corpus and the library's small designs. |
62
62
  | 37 | A frequency-dependent fit tolerance | variant C works; held back behind row 36, not in 0.3.0 | The fit scores the worst absolute dB error the same at every frequency, so near-equal fits are interchangeable to it wherever their error sits. Version 8's re-roll (row 23) moved 28 Years Later by 1.13 dB at 33 Hz and Obsession by 1.09 dB at 29 Hz. `impact.MOVED` now uses 20/f dB, capped at 2 dB at 10 Hz. Weighting the fit's bars as well as its search failed: the fitter either spent sections on fractions of a dB above 50 Hz (A), or the near-equal window, 2 dB wide at 10 Hz, let single shelves up to 3.6 dB off win on section count (B). **C**, weighting only the search (f/40 Hz, 0.25 to 1), works: over the 31 winners, median worst error at 30-40 Hz falls from 1.11 to 0.63 dB and at 5-10 Hz rises from 1.05 to 1.65. No real winner changes, protocol and corpus gate are unchanged, and corpus recovery goes from 1.37 to 0.85 dB RMS. It is held back because the band-exclusion test now designs a 1 dB nothing where version 8 declined, which is row 36's hole. **Next:** C together with a row 36 rule (refuse the fitter's answer, not only its alternatives, when its lift stays within the goal tolerance), validated as one decision change. Code on branch `experiment-fit-weighting-search`. See [the evidence](plans/fit-weighting.md). |
63
+ | 38 | Review the version 8 library redesign | open: handoff received 28 October 2026; nothing new to build, evidence for rows 32 and 36 | beqdesigner redesigned 1,051 titles on 0.3.1 (design version 8). Handoff: `/media/beq/triage/handoff-v8.md`, per-title results in `/media/beq/triage/dv8-redesign-moved.json`. Every decline now has a ledger row against the build that made it (`ledger.jsonl` stage `F` for the 44 at v7 and v8; the 208 still at v6 are stage `E`). The handoff reports no new class of finding: all seven items of beqdesigner's library-run review (its L4) are already in row 29 and, where still open, rows 31 and 35. **Row 32:** one new designed-to-declined flip, 22 Jump Street (`-1428523`), designed on v5-v7. On v8 flatten fails only the cliff check (40.7 dB/oct at 12.9 Hz against the input's 35.5), the same shape as Platoon's v6 flip; records `designer-37ed4387a860f027-7806f696-v7.run.json.gz` and `-v8`. 19 titles declined on v6 now design, including 12 of row 35's 14 flips. **Row 36:** four titles reviewed as genuine declines now design, one design each: The Adventures of Priscilla, Queen of the Desert; The Book Thief; The Manchurian Candidate; and Bugonia (declined before on coverage below 8.1 Hz). Check whether each is worth publishing. The other 43 of the 44 declines are unchanged in reason and failing checks from the stage E review. |
64
+ | 39 | A bass ceiling | released in 0.3.2 (design version 9) | Bass EQ restores the low end a mix's filtering removed, and that ends at or below about 60 Hz. The designer has no such limit: the judged band's top follows the deficit's first settled end, at least an octave above the tracking floor, so on a mix whose floor sits high it reaches the midbass and `flatten` fills everything under it. Wait Until Dark was judged to 192 Hz, flat; its cascade lifted 110 Hz by 7.4 dB and 250 Hz by 2 dB, and passed, because the over-lift bar's slack (half the input's 31.5 dB roughness) allowed 15.7 dB. Measured by where each cascade's lift last reaches 3 dB: 12.4% of the version 8 library's accepted designs pass 60 Hz, 7.2% pass 80 Hz and 45 pass 100 Hz, against 0.7%, 0.1% and 2 of the 15,586 catalogue entries. This checks the premise; it is not a calibration. Most of the catalogue's exceptions above 80 Hz are one author's flatten-like designs that review judged wrong. The library's age mix explains about two thirds of the gap: lifting past 60 Hz is 49-63% of pre-1980 designs against 2.5-5% from 2000. Reweighted to the catalogue's age mix it is still 4.5% against 0.7% at 60 Hz, and 1.7% against 0.1% at 80 Hz. Candidate: `AcceptParams.bass_ceiling_hz` (60). It caps the deficit's end in the judged top, but not the one-octave span, which a slope needs. Every priced target is tapered to nothing by the ceiling. Above the ceiling, the over-lift bar bounds the cascade's own lift to the level tolerance, with no roughness slack, and the fitter is given that bound. The mix-texture blocker keeps its uncapped band. Measured: probe moves no verdict or winner; protocol moves no false acceptance; corpus 0/45 gated, natural droop 5/9 to 4/9, filtered 7/9 to 6/9 (the loss had 15.6 dB rms recovery). On 80 paired library titles, 57 of 60 lifters still accept and none lifts 3 dB past 58.7 Hz; 3 decline (The Manchurian Candidate, Solaris, The Red Shoes, all flatten filling a plateau above 60 Hz); 20 of 20 controls unchanged. The lift below 60 Hz is untouched, so Wait Until Dark's rumble is still row 33's. Of 22 sampled library declines, 3 now design, all fit re-rolls below the ceiling (row 32). Adopted: `impact.toml` declares it, the golden answers, corpus and baseline records are refreshed. Left: row 33, for the lift that remains below 60 Hz. Evidence and outcome: [the experiment](plans/bass-ceiling.md). |
63
65
 
64
66
  ## Review evidence
65
67
 
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -147,6 +147,23 @@ class AcceptParams:
147
147
  constrained by this clause, so how wide their bands are says nothing about where it should
148
148
  sit; a value chosen that way rejected a good filter whose band was perfectly measurable."""
149
149
 
150
+ bass_ceiling_hz: float | None = 60.0
151
+ """Where bass EQ stops: no target asks for lift above it and no cascade may add more than
152
+ `level_tolerance_db` beyond it. `None` turns it off (TODO 39).
153
+
154
+ A BEQ restores the low end a mix's filtering removed, and that low end ends at or below
155
+ about 60 Hz. The judged band's top used to follow the deficit's first settled end, at
156
+ least `min_judge_octaves` above the tracking floor, so on a mix whose floor sits high it
157
+ reached the midbass, and `flatten` then filled everything under it: Wait Until Dark's band
158
+ ran to 192 Hz and its cascade lifted 110 Hz by 7.4 dB. 12.4% of the version 8 library's
159
+ accepted designs lifted 3 dB or more above 60 Hz, against 0.7% of the published catalogue
160
+ (a check of the premise, not a calibration).
161
+
162
+ The ceiling caps the deficit's end, not the band's span: a floor high enough that less
163
+ than `min_judge_octaves` lies under the ceiling still gets its octave to read a slope over
164
+ (above the ceiling, where nothing is asked for), because a slope read over less is
165
+ arbitrary."""
166
+
150
167
  turnover_min_octaves: float = 0.5
151
168
  """How far inside the band the peak must sit before a turnover is measured.
152
169
 
@@ -414,19 +431,32 @@ def overlift_bar_db(
414
431
  band only, never on a filter, so the fitter scores against the same bar before anything is
415
432
  fitted (`overlift_allowance` in `pipeline`).
416
433
  """
417
- guard = (correction.freqs >= DESIGN_GRID[0]) & (
434
+ in_band = (correction.freqs >= DESIGN_GRID[0]) & (
418
435
  correction.freqs <= correction.band_hz[1]
419
436
  )
420
437
  slack = spectral_roughness(correction, params.roughness_degree) / 2.0
421
438
  bar = (
422
439
  np.maximum(
423
- correction.requested_db(params.target_tilt_db_per_octave)[guard],
424
- correction.before_db[guard],
440
+ correction.requested_db(params.target_tilt_db_per_octave),
441
+ correction.before_db,
425
442
  )
426
443
  + params.level_tolerance_db
427
444
  + slack
428
445
  )
429
- return guard, bar
446
+ guard = in_band
447
+ if params.bass_ceiling_hz is not None:
448
+ # Above the bass ceiling nothing is asked for, so the cascade's own lift is what is
449
+ # bounded: no more than the level tolerance over the material, whatever its roughness
450
+ # (TODO 39).
451
+ above = (correction.freqs > params.bass_ceiling_hz) & (
452
+ correction.freqs <= DESIGN_GRID[-1]
453
+ )
454
+ ceiling_bar = correction.before_db + params.level_tolerance_db
455
+ bar = np.where(
456
+ above, np.where(in_band, np.minimum(bar, ceiling_bar), ceiling_bar), bar
457
+ )
458
+ guard = in_band | above
459
+ return guard, bar[guard]
430
460
 
431
461
 
432
462
  def spectral_roughness(correction: Correction, degree: int = 3) -> float:
@@ -633,11 +663,18 @@ def assess(
633
663
  figures["overlift"] = float(overshoot[worst])
634
664
  if _at(float(overshoot[worst]), quantum) > 0.0:
635
665
  at_hz = float(correction.freqs[guard][worst])
636
- failures.append(
637
- f"lifts {at_hz:.1f} Hz to {correction.after_db[guard][worst]:+.1f} dB, "
638
- f"{float(overshoot[worst]):.1f} dB above anything the material or the requested "
639
- "shape supports there — boosting what is not content"
640
- )
666
+ if params.bass_ceiling_hz is not None and at_hz > params.bass_ceiling_hz:
667
+ failures.append(
668
+ f"lifts {at_hz:.1f} Hz by "
669
+ f"{float(overshoot[worst]) + params.level_tolerance_db:.1f} dB, above the "
670
+ f"{params.bass_ceiling_hz:g} Hz bass ceiling where bass EQ stops"
671
+ )
672
+ else:
673
+ failures.append(
674
+ f"lifts {at_hz:.1f} Hz to {correction.after_db[guard][worst]:+.1f} dB, "
675
+ f"{float(overshoot[worst]):.1f} dB above anything the material or the "
676
+ "requested shape supports there — boosting what is not content"
677
+ )
641
678
 
642
679
  roughness = spectral_roughness(correction, params.roughness_degree)
643
680
  wobble = corrected_wobble(correction, params.roughness_degree)
@@ -77,7 +77,7 @@ logger = logging.getLogger(__name__)
77
77
  CONTRACT_VERSION = "1.6"
78
78
  """The version this module implements. Responses echo the request's own version instead."""
79
79
 
80
- DESIGN_VERSION = 8
80
+ DESIGN_VERSION = 9
81
81
  """Which answer this code gives (1.4): bumped by any change that can alter `candidates`,
82
82
  `rejected`, a decline or a 1.3 fit figure for an unchanged request and unchanged settings.
83
83
 
@@ -103,7 +103,9 @@ tolerance in [0, 1] (a last-digit difference, not an answer change) and only ren
103
103
  the first the judge passes, fewest sections first, instead of whichever the arithmetic landed on
104
104
  (TODO 32). 0.3.0 is 8: the fit's search scores a whole generation at once, in the linear
105
105
  domain (TODO 23), which re-rolls the fit among near-equal optima as any change to its
106
- arithmetic does. `plans/design-version.md` has each one's build."""
106
+ arithmetic does. 0.3.2 is 9: it stops bass EQ at a 60 Hz ceiling: no target asks for lift
107
+ above it, the judged band's top is capped there, and a cascade may add no more than the level
108
+ tolerance beyond it (TODO 39). `plans/design-version.md` has each one's build."""
107
109
 
108
110
  Coverage = Literal["complete_programme", "excerpt"]
109
111
  ChannelScope = Literal["all_channels", "lfe_only", "mixed"]
@@ -23,9 +23,13 @@ from scipy.stats import beta
23
23
  from beqforge import Alignment, BiquadSpec, HighPass
24
24
  from beqforge.harness import SyntheticProfile, apply_high_pass, synthesise
25
25
 
26
- PROTOCOL_VERSION = "e10-v3"
26
+ PROTOCOL_VERSION = "e10-v6"
27
27
  """v1 read the goal deficit from the observed spectrum alone; v2 pivoted a tilted goal at the
28
- reference's bottom, not where the deficit begins; see `truth`."""
28
+ reference's bottom, not where the deficit begins (see `truth`); v3 scored a correction's
29
+ accuracy against the goal tolerance (see `ACCURACY_DB`) and cancelled through a causal low-pass
30
+ (see `e10_case`); v4's `nonstationary_floor` swung about 37 dB with every event, which is
31
+ event-locked content, not a pumping floor (see `REPORTED`); v5 counted a restoration's skirt
32
+ under the truth band as noise lift (see `TRANSITION_OCTAVES`)."""
29
33
 
30
34
  GOALS: dict[str, tuple[float, float]] = {
31
35
  "flat": (0.0, 1.5),
@@ -34,6 +38,15 @@ GOALS: dict[str, tuple[float, float]] = {
34
38
  }
35
39
  """Goal name: (`target_tilt_db_per_octave`, `goal_tolerance_db`), predeclared."""
36
40
 
41
+ ACCURACY_DB = 1.5
42
+ """How far a correction may land from the goal, at every goal: overshoot and missed recovery.
43
+
44
+ The goal tolerance is the pipeline's act threshold (`AcceptParams.goal_tolerance_db`: how far
45
+ from the goal a low end may sit and still need nothing), not a bound on a correction's
46
+ accuracy, so it decides only whether a case needs a correction. v3 scored overshoot and missed
47
+ recovery against it too, so the tight goal, whose designs are the flat goal's filter for
48
+ filter, counted the fit's ordinary 0.3-1 dB ripple as overshoot (11 of 36 on seeds 1-3)."""
49
+
37
50
  DEVELOPMENT_SEEDS = range(1, 1000)
38
51
  HELD_OUT_SEEDS = range(100001, 101000)
39
52
  HELD_OUT_PER_FAMILY = 36
@@ -45,7 +58,7 @@ FAMILIES = (
45
58
  "sparse_dialogue",
46
59
  "stationary_noise",
47
60
  "coloured_floor",
48
- "nonstationary_floor",
61
+ "tracking_floor",
49
62
  "correlated_noise",
50
63
  "finite_stopband",
51
64
  "partial_channels",
@@ -53,7 +66,25 @@ FAMILIES = (
53
66
  "natural_droop",
54
67
  "continuous_bass",
55
68
  "filtered",
69
+ "breathing_floor",
70
+ "event_locked_floor",
56
71
  )
72
+ """Appended, never reordered: a family's index is part of its seed's draws."""
73
+
74
+ REPORTED = ("event_locked_floor",)
75
+ """Families reported beside the counts but kept out of every pooled tally.
76
+
77
+ `event_locked_floor` is v4's `nonstationary_floor`: noise scaled by `0.25 + level / mean`, so it
78
+ swings about 37 dB, rising only inside programme events. Below the corner it shows the contrast
79
+ real stopband leakage does (32-36 dB against `finite_stopband`'s 30-32) and the same partial
80
+ coherence (about zero), so under the content principle ("content correlated with the rest of
81
+ the soundtrack is not noise") it is event-locked content, and no rule could refuse it without
82
+ refusing that leakage too. Like the frozen protocol's `natural_bass_light`, it is reported, not
83
+ counted. `tracking_floor` and `breathing_floor` are the pumping floors a mix can carry."""
84
+
85
+ PUMPING_DB = (2.0, 6.0)
86
+ """How far a pumping floor moves with the programme, drawn per case: a compressor's or a codec's
87
+ few dB, not an event's dynamic range."""
57
88
 
58
89
  FS = 1000
59
90
  DURATION_S = 240.0
@@ -68,6 +99,16 @@ FLOOR_HZ = 5.0
68
99
  """The lowest frequency scored: the pipeline's verification floor."""
69
100
 
70
101
  NOISE_LIFT_DB = 3.0
102
+
103
+ TRANSITION_OCTAVES = 0.5
104
+ """How far under the truth band's bottom noise lift starts to count.
105
+
106
+ A realisable filter that restores the bottom of the truth band cannot stop dead at its edge:
107
+ v5 counted that skirt as noise lift on cases whose design followed its evidence-priced target
108
+ within about 1 dB (`tracking_floor/1`, `breathing_floor/3`; plans/E10-validation-protocol.md,
109
+ "The pumping floor"). Half an octave is the allowance the real-texture half's `noise_replaced`
110
+ already makes under its corner. A case with no truth band has no restoration to skirt, so every
111
+ noise-only bin counts."""
71
112
  MIN_SPAN_OCTAVES = 1.0 / 3.0
72
113
  SMOOTHING_OCTAVES = 1.0 / 6.0
73
114
 
@@ -145,6 +186,25 @@ def _shaped_noise(
145
186
  return noise / (np.std(noise) or 1.0)
146
187
 
147
188
 
189
+ def _pumping_db(samples: np.ndarray, depth_db: float, family: str) -> np.ndarray:
190
+ """A floor's gain over time as the programme's 1 s level moves, at most `depth_db`.
191
+
192
+ Tracking: the floor rises with the programme above its median level, as a codec's noise
193
+ follows the signal. Breathing: a compressor's gain reduction pulls the floor down under loud
194
+ passages and releases it over about a second."""
195
+ level = np.convolve(np.abs(samples), np.ones(FS) / FS)[: samples.size]
196
+ above = np.clip(
197
+ 20.0 * np.log10(np.maximum(level, 1e-30) / (np.median(level) or 1.0)),
198
+ 0.0,
199
+ depth_db,
200
+ )
201
+ if family == "tracking_floor":
202
+ return above
203
+ release = math.exp(-1.0 / FS)
204
+ held = signal.lfilter([1.0 - release], [1.0, -release], above)
205
+ return -np.maximum(held, above)
206
+
207
+
148
208
  def e10_case(family: str, seed: int, duration_s: float = DURATION_S) -> E10Case:
149
209
  """One reproducible case: its family, and every parameter drawn from its seed."""
150
210
  if family not in FAMILIES:
@@ -191,20 +251,29 @@ def e10_case(family: str, seed: int, duration_s: float = DURATION_S) -> E10Case:
191
251
  colour = str(rng.choice(("pink", "brown", "codec")))
192
252
  noise = {name: _shaped_noise(rng, n, colour) * 0.01 for name in ("L", "LFE")}
193
253
  change = f"{colour} noise only"
194
- elif family in ("coloured_floor", "nonstationary_floor"):
254
+ elif family in (
255
+ "coloured_floor",
256
+ "tracking_floor",
257
+ "breathing_floor",
258
+ "event_locked_floor",
259
+ ):
195
260
  hp = _high_pass(rng, (8, 12, 16))
196
261
  observed = {name: apply_high_pass(s, hp, FS) for name, s in recorded.items()}
197
262
  colour = str(rng.choice(("pink", "brown", "codec")))
198
263
  down = float(rng.uniform(30.0, 60.0))
264
+ depth = float(rng.uniform(*PUMPING_DB))
199
265
  noise = {}
200
266
  for name, samples in recorded.items():
201
267
  floor = _shaped_noise(rng, n, colour) * below(samples, down)
202
- if family == "nonstationary_floor":
203
- # pumping: the floor rises with the programme, as a compressor's would
268
+ if family == "event_locked_floor":
204
269
  level = np.convolve(np.abs(samples), np.ones(FS) / FS, mode="same")
205
270
  floor *= 0.25 + level / (np.mean(level) or 1.0)
271
+ elif family != "coloured_floor":
272
+ floor *= 10.0 ** (_pumping_db(samples, depth, family) / 20.0)
206
273
  noise[name] = floor
207
274
  change = f"{hp}; {colour} floor {down:.0f} dB down"
275
+ if family in ("tracking_floor", "breathing_floor"):
276
+ change += f", {family.split('_')[0]} {depth:.1f} dB"
208
277
  elif family == "correlated_noise":
209
278
  hp = _high_pass(rng, (8, 12))
210
279
  recorded = {name: apply_high_pass(s, hp, FS) for name, s in recorded.items()}
@@ -230,7 +299,11 @@ def e10_case(family: str, seed: int, duration_s: float = DURATION_S) -> E10Case:
230
299
  change = f"{hp} on {touched} only"
231
300
  elif family == "cancellation":
232
301
  depth = float(rng.uniform(0.3, 0.8))
233
- low = signal.sosfilt(
302
+ # zero phase, so the low band cancels in phase and the mix's change is 1 - d|H|^2, a
303
+ # plain deficit. v3's causal low-pass turned through -180 degrees near its corner, so
304
+ # the inverted copy added there: +3 to +3.8 dB at 40-71 Hz, which the designer then
305
+ # levelled the band below up to
306
+ low = signal.sosfiltfilt(
234
307
  signal.butter(4, 60.0, fs=FS, output="sos"), recorded["LFE"]
235
308
  )
236
309
  from beqforge.material import LFE_GAIN, MAIN_GAIN
@@ -494,9 +567,15 @@ def score_selection(
494
567
  if filters is None
495
568
  else published_response_db(filters, params.realisation, known.freqs)
496
569
  )
497
- lifted = response[known.noise_only]
498
- row["noise_exposed"] = bool(known.noise_only.any())
570
+ counted = known.noise_only
571
+ if known.truth_band.any():
572
+ edge = float(known.freqs[known.truth_band].min())
573
+ counted = counted & (known.freqs <= edge * 2.0**-TRANSITION_OCTAVES)
574
+ lifted = response[counted]
575
+ skirt = response[known.noise_only]
576
+ row["noise_exposed"] = bool(counted.any())
499
577
  row["noise_lift_db"] = round(float(lifted.max()), 2) if lifted.size else 0.0
578
+ row["noise_lift_skirt_db"] = round(float(skirt.max()), 2) if skirt.size else 0.0
500
579
  row["noise_lift"] = bool(label is not None and row["noise_lift_db"] > NOISE_LIFT_DB)
501
580
  row["unnecessary"] = bool(label is not None and not known.needs_correction)
502
581
  corrected = known.programme_db + response
@@ -507,13 +586,12 @@ def score_selection(
507
586
  # there. Either alone mistakes the source's chance shape for an overshoot: filling a
508
587
  # chance dip in the programme rises over the first, restoring a chance peak the recipe
509
588
  # cut rises over the second
510
- tolerance = known.tolerance_db
511
- left = corrected > np.maximum(known.goal_db, known.programme_db) + tolerance
589
+ left = corrected > np.maximum(known.goal_db, known.programme_db) + ACCURACY_DB
512
590
  seen = known.observed_db + response
513
- there = seen > np.maximum(known.goal_db, known.observed_db) + tolerance
591
+ there = seen > np.maximum(known.goal_db, known.observed_db) + ACCURACY_DB
514
592
  over = band & left & there
515
593
  gap = np.maximum(known.goal_db - corrected, 0.0)[band]
516
- short = band & (corrected < known.goal_db - known.tolerance_db)
594
+ short = band & (corrected < known.goal_db - ACCURACY_DB)
517
595
  row["overshoot"] = bool(label is not None and over.any())
518
596
  row["shortfall_median_db"] = round(float(np.median(gap)), 2)
519
597
  row["shortfall_worst_db"] = round(float(gap.max()), 2)
@@ -602,7 +680,7 @@ def summarise(rows: list[dict]) -> dict:
602
680
  for goal in sorted({r["goal"] for r in rows}):
603
681
  at_goal = [r for r in rows if r["goal"] == goal]
604
682
  summary[goal] = {
605
- "pooled": tally(at_goal),
683
+ "pooled": tally([r for r in at_goal if r["family"] not in REPORTED]),
606
684
  "families": {
607
685
  family: tally([r for r in at_goal if r["family"] == family])
608
686
  for family in sorted({r["family"] for r in at_goal})
@@ -221,12 +221,29 @@ def _power_numpy(coefficients, c1, s1, c2, s2):
221
221
  return num / den
222
222
 
223
223
 
224
+ def _compiled(function):
225
+ """`numba.njit`, with its on-disk cache when there is somewhere to write one.
226
+
227
+ numba caches beside the source, or failing that in the user's cache directory, and refuses
228
+ to compile at all when it can write to neither: an install the running user can't write to,
229
+ with no home of its own, such as the designer image run as another user. The cache only saves
230
+ the compile (~0.7 s a process), so without one the kernel is compiled afresh, to the same
231
+ answer.
232
+ """
233
+ try:
234
+ return numba.njit(cache=True)(function)
235
+ except RuntimeError as e:
236
+ if "cannot cache" not in str(e):
237
+ raise
238
+ return numba.njit(function)
239
+
240
+
224
241
  if numba is None:
225
242
  _fit_and_drift = _fit_and_drift_numpy
226
243
  _worst_ratio = _worst_ratio_numpy
227
244
  else:
228
245
 
229
- @numba.njit(cache=True)
246
+ @_compiled
230
247
  def _power_into(
231
248
  coefficients, m, c1, s1, c2, s2, out
232
249
  ): # pragma: no cover - compiled
@@ -249,7 +266,7 @@ else:
249
266
  for i in range(bins):
250
267
  out[i] = num[i] / den[i]
251
268
 
252
- @numba.njit(cache=True)
269
+ @_compiled
253
270
  def _fit_and_drift(
254
271
  response, realised, c1, s1, c2, s2, target_inverse
255
272
  ): # pragma: no cover - compiled
@@ -277,7 +294,7 @@ else:
277
294
  drift[m] = worst
278
295
  return error, drift
279
296
 
280
- @numba.njit(cache=True)
297
+ @_compiled
281
298
  def _worst_ratio(
282
299
  numerator, denominator, c1, s1, c2, s2, weight, symmetric
283
300
  ): # pragma: no cover - compiled
@@ -485,7 +485,7 @@ FEATURES: tuple[Feature, ...] = (
485
485
  _judged(
486
486
  "judge.overlift",
487
487
  "lifting above anything the material or the target asks",
488
- ("accept:overlift_bar_db",),
488
+ ("accept:overlift_bar_db", "accept:AcceptParams.bass_ceiling_hz"),
489
489
  "overlift",
490
490
  "lifts ",
491
491
  ),
@@ -116,3 +116,31 @@ note = "the target and ceiling reach the fit quantised as before"
116
116
  feature = "fit.choice"
117
117
  kind = "unchanged"
118
118
  note = "`_choose_fit` and the near-equal window are untouched; what they choose among is re-rolled"
119
+
120
+ [[change]]
121
+ id = "bass-ceiling"
122
+ design_version = 9
123
+ summary = "bass EQ stops at a 60 Hz ceiling: every priced target tapers to nothing by it, the judged band's top is capped there (its one-octave span is not), and above it a cascade may add no more than the level tolerance, a bound the fitter is given too (TODO 39)"
124
+ evidence = "plans/bass-ceiling.md#outcome-31-october-2026"
125
+
126
+ [[change.touches]]
127
+ feature = "targets.shared"
128
+ kind = "behaviour"
129
+ # Measured on the library, moved by `impact.MOVED`, together with judge.overlift (one change,
130
+ # not separable): v8 library titles designed at version 8 and 9 on one machine, from the
131
+ # library's audio with default settings. The designed classes are 60 titles lifting 3 dB above
132
+ # 60 Hz at version 8 (random, plus those named in review) and 20 random others; the declined
133
+ # classes a random sample of each. The 3 declined_after_fit titles that now design are fit
134
+ # re-rolls inside bands that end at 36-43 Hz, where the ceiling does not bind (row 32).
135
+ basis = { no_fit = [0, 10], declined_after_fit = [3, 12], designed_with_failures = [11, 15], designed_all_passed = [54, 65] }
136
+
137
+ [[change.touches]]
138
+ feature = "judge.overlift"
139
+ kind = "behaviour"
140
+ # The same measurement as targets.shared.
141
+ basis = { no_fit = [0, 10], declined_after_fit = [3, 12], designed_with_failures = [11, 15], designed_all_passed = [54, 65] }
142
+
143
+ [[change.touches]]
144
+ feature = "judge.assess"
145
+ kind = "unchanged"
146
+ note = "the over-lift failure names the bass ceiling when its worst bin lies above it; wording only"
@@ -819,18 +819,25 @@ def _flat_deficit(material: Material, params: "PipelineParams"):
819
819
 
820
820
 
821
821
  def _judged_top_hz(
822
- anchor_hz: float, params: "PipelineParams", noise_floor_hz: float = math.nan
822
+ anchor_hz: float,
823
+ params: "PipelineParams",
824
+ noise_floor_hz: float = math.nan,
825
+ *,
826
+ ceiling: bool = True,
823
827
  ) -> float:
824
828
  """Upper edge of the judged band — and the pivot of the goal below the knee.
825
829
 
826
- The deficit's first settled end (where the correction stops), but at least
830
+ The deficit's first settled end (where the correction stops), no higher than the bass
831
+ ceiling (`AcceptParams.bass_ceiling_hz`) unless `ceiling` is false, but at least
827
832
  `min_judge_octaves` above the band's floor, so there is always enough band to read a
828
- slope over. No fixed frequency.
833
+ slope over.
829
834
  """
830
835
  low = params.verify_floor_hz
831
836
  if not math.isnan(noise_floor_hz):
832
837
  low = max(low, noise_floor_hz)
833
838
  widest = low * 2.0**params.accept.min_judge_octaves
839
+ if ceiling and params.accept.bass_ceiling_hz is not None:
840
+ anchor_hz = min(anchor_hz, params.accept.bass_ceiling_hz)
834
841
  return min(max(anchor_hz, widest), float(DESIGN_GRID[-1]))
835
842
 
836
843
 
@@ -1033,8 +1040,9 @@ def passband_ripple_db(
1033
1040
  if measured is None:
1034
1041
  return None
1035
1042
  freqs, response, _, _, _, anchor = measured
1043
+ # the passband above the deficit's own end, wherever the bass ceiling stops the correction
1036
1044
  low, high = (
1037
- _judged_top_hz(anchor, params, noise_floor_hz),
1045
+ _judged_top_hz(anchor, params, noise_floor_hz, ceiling=False),
1038
1046
  params.diagnose.band_hz[1],
1039
1047
  )
1040
1048
  bands = (*params.exclude_bands_hz, *mains_hum_notches(freqs, response, low, high))
@@ -1131,6 +1139,21 @@ def priced_by_evidence(
1131
1139
  — the ambition E3 found selection rewarding.
1132
1140
  """
1133
1141
  target, notes = _held_below_floor(target, diagnosis.noise_floor_hz)
1142
+ ceiling_hz = params.accept.bass_ceiling_hz
1143
+ if ceiling_hz is not None:
1144
+ # tapered to nothing at the bass ceiling, over the same ratio a deficit closes over
1145
+ capped = target * _taper(
1146
+ DESIGN_GRID,
1147
+ ceiling_hz / params.flatten_taper_ratio,
1148
+ params.flatten_taper_ratio,
1149
+ )
1150
+ cut = int(np.argmax(target - capped))
1151
+ if target[cut] - capped[cut] > 0.5:
1152
+ notes.append(
1153
+ f"bass ceiling binds: the target asks for {target[cut]:.1f} dB at "
1154
+ f"{DESIGN_GRID[cut]:.1f} Hz, tapered to nothing by {ceiling_hz:g} Hz"
1155
+ )
1156
+ target = capped
1134
1157
  if deficit_db is not None:
1135
1158
  excess = target - deficit_db
1136
1159
  worst = int(np.argmax(excess))