beqforge 0.2.3__tar.gz → 0.2.5__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 (206) hide show
  1. beqforge-0.2.5/.gitattributes +1 -0
  2. {beqforge-0.2.3 → beqforge-0.2.5}/.github/workflows/build-designer-image.yml +6 -3
  3. {beqforge-0.2.3 → beqforge-0.2.5}/.github/workflows/build-executable.yml +20 -0
  4. {beqforge-0.2.3 → beqforge-0.2.5}/AGENTS.md +2 -1
  5. {beqforge-0.2.3 → beqforge-0.2.5}/PKG-INFO +48 -5
  6. {beqforge-0.2.3 → beqforge-0.2.5}/README.md +47 -4
  7. {beqforge-0.2.3 → beqforge-0.2.5}/TODO.md +1 -0
  8. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/__init__.py +1 -1
  9. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/AGENTS.md +1 -1
  10. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/diagnose.py +98 -8
  11. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/AGENTS.md +5 -1
  12. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/__init__.py +15 -0
  13. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/cache.py +182 -4
  14. beqforge-0.2.5/beqoptimiser/parameters.py +500 -0
  15. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser.md +50 -0
  16. beqforge-0.2.5/packaging/designer/beqforge-designer.cmd +33 -0
  17. {beqforge-0.2.3 → beqforge-0.2.5}/plans/done-design-and-pipeline.md +70 -0
  18. beqforge-0.2.5/plans/parameter-space-optimisation.md +126 -0
  19. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_designer_server.py +61 -5
  20. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_diagnose.py +49 -1
  21. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_designer_server_startup.py +30 -2
  22. beqforge-0.2.5/tests/test_optimiser_parameters.py +200 -0
  23. {beqforge-0.2.3 → beqforge-0.2.5}/tools/designer_server.py +98 -14
  24. beqforge-0.2.5/tools/experiments/parameter_benchmark.py +152 -0
  25. {beqforge-0.2.3 → beqforge-0.2.5}/tools/smoke_test_exe.py +21 -10
  26. {beqforge-0.2.3 → beqforge-0.2.5}/.dockerignore +0 -0
  27. {beqforge-0.2.3 → beqforge-0.2.5}/.github/dependabot.yml +0 -0
  28. {beqforge-0.2.3 → beqforge-0.2.5}/.github/workflows/build-device-check.yml +0 -0
  29. {beqforge-0.2.3 → beqforge-0.2.5}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  30. {beqforge-0.2.3 → beqforge-0.2.5}/.github/workflows/tests.yml +0 -0
  31. {beqforge-0.2.3 → beqforge-0.2.5}/.gitignore +0 -0
  32. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/.gitignore +0 -0
  33. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/beqforge.iml +0 -0
  34. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/editor.xml +0 -0
  35. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/jsonSchemas.xml +0 -0
  36. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/misc.xml +0 -0
  37. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/modules.xml +0 -0
  38. {beqforge-0.2.3 → beqforge-0.2.5}/.idea/vcs.xml +0 -0
  39. {beqforge-0.2.3 → beqforge-0.2.5}/CLAUDE.md +0 -0
  40. {beqforge-0.2.3 → beqforge-0.2.5}/LICENCE.md +0 -0
  41. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/AGENTS.md +0 -0
  42. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/biquad.py +0 -0
  43. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/provenance.py +0 -0
  44. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/publication.py +0 -0
  45. {beqforge-0.2.3 → beqforge-0.2.5}/beq_common/types.py +0 -0
  46. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/__init__.py +0 -0
  47. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/accept.py +0 -0
  48. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/beqd.py +0 -0
  49. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/biquad.py +0 -0
  50. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/cache.py +0 -0
  51. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/charts.py +0 -0
  52. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/cli.py +0 -0
  53. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/design.py +0 -0
  54. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/designer.py +0 -0
  55. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/explain.py +0 -0
  56. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/extraction.py +0 -0
  57. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/filters.py +0 -0
  58. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/harness.py +0 -0
  59. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/identify.py +0 -0
  60. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/material.py +0 -0
  61. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/pipeline.py +0 -0
  62. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/record.py +0 -0
  63. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/reference.py +0 -0
  64. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/rolloff.py +0 -0
  65. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge/verify.py +0 -0
  66. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge-device-check.spec +0 -0
  67. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge.spec +0 -0
  68. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/AGENTS.md +0 -0
  69. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/__init__.py +0 -0
  70. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/analyse.py +0 -0
  71. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/audio.py +0 -0
  72. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/catalogue.py +0 -0
  73. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/cli.py +0 -0
  74. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/coefficients.py +0 -0
  75. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/engines.py +0 -0
  76. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/evidence.py +0 -0
  77. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/manifest.py +0 -0
  78. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/measurement.py +0 -0
  79. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/profiles.py +0 -0
  80. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/qualification.py +0 -0
  81. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/report.py +0 -0
  82. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/transactions.py +0 -0
  83. {beqforge-0.2.3 → beqforge-0.2.5}/beqforge_device_check/verify.py +0 -0
  84. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/cli.py +0 -0
  85. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/core.py +0 -0
  86. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/data/seed-manifest.json +0 -0
  87. {beqforge-0.2.3 → beqforge-0.2.5}/beqoptimiser/data/seed.json.gz +0 -0
  88. {beqforge-0.2.3 → beqforge-0.2.5}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  89. {beqforge-0.2.3 → beqforge-0.2.5}/docs/device-check-examples/simulation.json +0 -0
  90. {beqforge-0.2.3 → beqforge-0.2.5}/docs/device-check.md +0 -0
  91. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/README.md +0 -0
  92. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  93. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  94. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/assets/example-1.png +0 -0
  95. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/assets/example-2.png +0 -0
  96. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/entry-results.json.gz +0 -0
  97. {beqforge-0.2.3 → beqforge-0.2.5}/docs/optimiser-report/statistics.json +0 -0
  98. {beqforge-0.2.3 → beqforge-0.2.5}/evidence_validation.json +0 -0
  99. {beqforge-0.2.3 → beqforge-0.2.5}/negative_corpus.json +0 -0
  100. {beqforge-0.2.3 → beqforge-0.2.5}/packaging/designer/Dockerfile +0 -0
  101. {beqforge-0.2.3 → beqforge-0.2.5}/packaging/designer/smoke.py +0 -0
  102. {beqforge-0.2.3 → beqforge-0.2.5}/packaging/device-check/NOTICE.txt +0 -0
  103. {beqforge-0.2.3 → beqforge-0.2.5}/plans/F2-catalogue-filter-optimisation.md +0 -0
  104. {beqforge-0.2.3 → beqforge-0.2.5}/plans/F2-device-precision-validation.md +0 -0
  105. {beqforge-0.2.3 → beqforge-0.2.5}/plans/F2-package-consolidation.md +0 -0
  106. {beqforge-0.2.3 → beqforge-0.2.5}/plans/R2a-server-stage-cache.md +0 -0
  107. {beqforge-0.2.3 → beqforge-0.2.5}/plans/done-baseline-evidence.md +0 -0
  108. {beqforge-0.2.3 → beqforge-0.2.5}/plans/research-design-decisions.md +0 -0
  109. {beqforge-0.2.3 → beqforge-0.2.5}/plans/test-portability-and-runtime.md +0 -0
  110. {beqforge-0.2.3 → beqforge-0.2.5}/plans/texture-yardstick-sub-band.md +0 -0
  111. {beqforge-0.2.3 → beqforge-0.2.5}/plans/texture-yardstick-sub-band.patch +0 -0
  112. {beqforge-0.2.3 → beqforge-0.2.5}/pyproject.toml +0 -0
  113. {beqforge-0.2.3 → beqforge-0.2.5}/tests/__init__.py +0 -0
  114. {beqforge-0.2.3 → beqforge-0.2.5}/tests/conftest.py +0 -0
  115. {beqforge-0.2.3 → beqforge-0.2.5}/tests/fixtures/optimiser_seed_entry.json +0 -0
  116. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_cli.py +0 -0
  117. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_accept.py +0 -0
  118. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_cache.py +0 -0
  119. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_cache_stores.py +0 -0
  120. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_channel_evidence.py +0 -0
  121. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_charts.py +0 -0
  122. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_corpus.py +0 -0
  123. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_design.py +0 -0
  124. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_designer.py +0 -0
  125. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_device_rate.py +0 -0
  126. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_evidence.py +0 -0
  127. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_evidence_excess.py +0 -0
  128. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_exclusions.py +0 -0
  129. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_explain.py +0 -0
  130. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_extraction.py +0 -0
  131. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_filters.py +0 -0
  132. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_frozen_cache.py +0 -0
  133. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_guard.py +0 -0
  134. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_harness.py +0 -0
  135. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_headroom.py +0 -0
  136. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_hum.py +0 -0
  137. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_identify.py +0 -0
  138. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_jacobian.py +0 -0
  139. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_parallel.py +0 -0
  140. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_pipeline.py +0 -0
  141. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_playback.py +0 -0
  142. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_playback_headroom.py +0 -0
  143. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_publication.py +0 -0
  144. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_record.py +0 -0
  145. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_record_fingerprint.py +0 -0
  146. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_reference.py +0 -0
  147. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_references.py +0 -0
  148. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_ripple_band.py +0 -0
  149. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_rolloff.py +0 -0
  150. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_strategy_config.py +0 -0
  151. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_timings.py +0 -0
  152. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_design_verify.py +0 -0
  153. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_designer_image.py +0 -0
  154. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_analyse.py +0 -0
  155. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_catalogue.py +0 -0
  156. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_catalogue_predict.py +0 -0
  157. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_cli.py +0 -0
  158. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_engines.py +0 -0
  159. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_evidence.py +0 -0
  160. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_examples.py +0 -0
  161. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_manifest.py +0 -0
  162. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_measurement.py +0 -0
  163. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_qualification.py +0 -0
  164. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_transactions.py +0 -0
  165. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_device_check_verify.py +0 -0
  166. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_extract_layout.py +0 -0
  167. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_optimiser.py +0 -0
  168. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_optimiser_cache.py +0 -0
  169. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_optimiser_report.py +0 -0
  170. {beqforge-0.2.3 → beqforge-0.2.5}/tests/test_package_boundaries.py +0 -0
  171. {beqforge-0.2.3 → beqforge-0.2.5}/tools/__init__.py +0 -0
  172. {beqforge-0.2.3 → beqforge-0.2.5}/tools/build_optimiser_seed.py +0 -0
  173. {beqforge-0.2.3 → beqforge-0.2.5}/tools/design_beq.py +0 -0
  174. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/a1_sweep.py +0 -0
  175. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/compare_records.py +0 -0
  176. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/compare_verdicts.py +0 -0
  177. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/inject_variants.py +0 -0
  178. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p14_compare.py +0 -0
  179. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p14_jacobian.py +0 -0
  180. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p14_run_pipeline.py +0 -0
  181. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p14_surrogate.py +0 -0
  182. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_compare_greedy.py +0 -0
  183. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_compare_seeded.py +0 -0
  184. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_compare_structure.py +0 -0
  185. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_greedy.py +0 -0
  186. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_seeded.py +0 -0
  187. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p18_structure.py +0 -0
  188. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/p20_smooth_drift.py +0 -0
  189. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/probe.py +0 -0
  190. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/refit_probe.py +0 -0
  191. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/request_timing.py +0 -0
  192. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/score_injected.py +0 -0
  193. {beqforge-0.2.3 → beqforge-0.2.5}/tools/experiments/t8_floor_rules.py +0 -0
  194. {beqforge-0.2.3 → beqforge-0.2.5}/tools/extract.py +0 -0
  195. {beqforge-0.2.3 → beqforge-0.2.5}/tools/ledger_template.html +0 -0
  196. {beqforge-0.2.3 → beqforge-0.2.5}/tools/negative_corpus.py +0 -0
  197. {beqforge-0.2.3 → beqforge-0.2.5}/tools/optimiser_catalogue_report.py +0 -0
  198. {beqforge-0.2.3 → beqforge-0.2.5}/tools/prepare_device_check_helper.py +0 -0
  199. {beqforge-0.2.3 → beqforge-0.2.5}/tools/render_ledger.py +0 -0
  200. {beqforge-0.2.3 → beqforge-0.2.5}/tools/replay.py +0 -0
  201. {beqforge-0.2.3 → beqforge-0.2.5}/tools/seed_optimiser_cache.py +0 -0
  202. {beqforge-0.2.3 → beqforge-0.2.5}/tools/smoke_test_device_check.py +0 -0
  203. {beqforge-0.2.3 → beqforge-0.2.5}/tools/smoke_test_install.py +0 -0
  204. {beqforge-0.2.3 → beqforge-0.2.5}/tools/summarise.py +0 -0
  205. {beqforge-0.2.3 → beqforge-0.2.5}/tools/validate_evidence.py +0 -0
  206. {beqforge-0.2.3 → beqforge-0.2.5}/uv.lock +0 -0
@@ -0,0 +1 @@
1
+ *.cmd text eol=crlf
@@ -1,15 +1,16 @@
1
1
  name: Designer image
2
2
 
3
3
  # The designer as a container, beside a beqdesigner pipeline service (README, "Running the
4
- # designer in a container"). Every change to what goes into it is built and smoke-tested;
5
- # a vX.Y.Z tag also publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer.
4
+ # designer in a container"). Every change to what goes into it is built and smoke-tested.
5
+ # A release publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer:
6
+ # build-executable.yml calls this for a vX.Y.Z tag once the release tests have passed, as it
7
+ # does for the executables and PyPI, so an image never ships ahead of its tests.
6
8
  # A manual run with `publish` publishes the image of an existing release from a later commit,
7
9
  # only if the designer's runtime code is identical to that release's tag (as when the image
8
10
  # recipe arrived after the release).
9
11
  on:
10
12
  push:
11
13
  branches: [main]
12
- tags: ['v*']
13
14
  paths:
14
15
  - 'packaging/designer/**'
15
16
  - '.dockerignore'
@@ -30,6 +31,8 @@ on:
30
31
  description: Publish the image as beq_common.__version__ (its vX.Y.Z tag must exist, with identical designer code)
31
32
  type: boolean
32
33
  default: false
34
+ # the release: build-executable.yml, from a tag, after its test gate
35
+ workflow_call:
33
36
 
34
37
  permissions:
35
38
  contents: read
@@ -226,6 +226,26 @@ jobs:
226
226
  env:
227
227
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
228
228
 
229
+ - name: Attach the Windows designer launcher
230
+ if: runner.os == 'Windows' && startsWith(github.ref, 'refs/tags/')
231
+ uses: softprops/action-gh-release@v3
232
+ with:
233
+ files: packaging/designer/beqforge-designer.cmd
234
+ env:
235
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
236
+
237
+ image:
238
+ name: Publish designer image
239
+ needs: [validated_tests, package]
240
+ # see create_release for why the needed results are checked explicitly
241
+ if: >-
242
+ !cancelled() && startsWith(github.ref, 'refs/tags/') &&
243
+ needs.validated_tests.result == 'success' && needs.package.result == 'success'
244
+ uses: ./.github/workflows/build-designer-image.yml
245
+ permissions:
246
+ contents: read
247
+ packages: write
248
+
229
249
  publish:
230
250
  name: Publish Python package to PyPI
231
251
  needs: [validated_tests, package, build, create_release]
@@ -60,7 +60,8 @@ or fixed ports), with coverage (`--cov`, configured in `pyproject.toml`, `multip
60
60
  children included) uploaded to Codecov by each test job using the `CODECOV_TOKEN` secret;
61
61
  release runs inherit it. Feature branch pushes test on Linux only and a newer push cancels the branch's
62
62
  previous run; `main`, tags and manual runs test Linux, macOS and Windows and are never cancelled. `.github/workflows/build-executable.yml` builds release executables and distributions;
63
- tag pushes publish to PyPI after validation and successful executable builds. Tags must match
63
+ tag pushes publish to PyPI after validation and successful executable builds, and the designer
64
+ image to GHCR after validation. Tags must match
64
65
  the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
65
66
  PyPI uses the `pypi` environment and Trusted Publishing; setup details are in the README.
66
67
  Device-check preview executable packaging has its own workflow, not a separate Python release;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -187,6 +187,13 @@ naming the array, rather than a design decline. Inline requests remain supported
187
187
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
188
188
  measures cold and warm requests.
189
189
 
190
+ **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
191
+ its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
192
+ caller can send each server as many designs as it takes rather than be configured with that
193
+ number. Concurrent designs share the machine's logical processors, SMT threads included, each
194
+ at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
195
+ The share changes how long a fit takes, never its answer.
196
+
190
197
  Every `serve-designer` configuration option can also be set through an environment variable.
191
198
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
192
199
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -207,6 +214,7 @@ their existing CLI configuration.
207
214
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
208
215
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
209
216
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
217
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
210
218
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
211
219
 
212
220
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -252,8 +260,11 @@ pure `design(request) -> response` adapter, independently testable without a soc
252
260
 
253
261
  `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
254
262
  pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
255
- published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
263
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
256
264
  each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
265
+ The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
266
+ as it does for the executables and PyPI; pushes to `main` and pull requests build and
267
+ smoke-test the image without publishing it.
257
268
  A manual run of that workflow with `publish` set publishes the current version's image from a
258
269
  later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
259
270
  image was published this way, the recipe having arrived after the release).
@@ -270,9 +281,9 @@ The image's defaults, all overridable with the environment variables above:
270
281
 
271
282
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
272
283
  the shared root and the cache: the server checks both with a write at startup. Its
273
- `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
274
- worker process, one at a time, so a caller sending several at once queues them; size the
275
- caller's timeout for that. `docker stop` stops a design in progress.
284
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
285
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
286
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
276
287
 
277
288
  Build and check it locally from the repository root:
278
289
 
@@ -284,6 +295,38 @@ python3 packaging/designer/smoke.py beqforge-designer
284
295
  The smoke script checks `/health` and one design inline and one by reference;
285
296
  `tests/test_designer_image.py` runs the same check against an in-process server.
286
297
 
298
+ ### Running the designer on Windows
299
+
300
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
301
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
302
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
303
+ exe and double-click it, or run the exe directly:
304
+
305
+ ```bat
306
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
307
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
308
+ ```
309
+
310
+ The launcher sets the image's defaults through the environment variables above, each only if
311
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
312
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
313
+
314
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
315
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
316
+ networks.
317
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
318
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
319
+ work directory. Without it, beqdesigner must send audio inline.
320
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
321
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
322
+ Check `http://<address>:8420/health` from there first.
323
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
324
+ design more than one title at a time; `/health` reports it as `parallel`.
325
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
326
+ press Ctrl+C, to stop.
327
+
328
+ ### Building and releasing
329
+
287
330
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
288
331
  including `serve-designer`) is built for Linux/macOS/Windows by
289
332
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -161,6 +161,13 @@ naming the array, rather than a design decline. Inline requests remain supported
161
161
  logs read, parse, decode, design and response timings; `tools/experiments/request_timing.py`
162
162
  measures cold and warm requests.
163
163
 
164
+ **Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
165
+ its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
166
+ caller can send each server as many designs as it takes rather than be configured with that
167
+ number. Concurrent designs share the machine's logical processors, SMT threads included, each
168
+ at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
169
+ The share changes how long a fit takes, never its answer.
170
+
164
171
  Every `serve-designer` configuration option can also be set through an environment variable.
165
172
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
166
173
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -181,6 +188,7 @@ their existing CLI configuration.
181
188
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
182
189
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
183
190
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
191
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
184
192
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
185
193
 
186
194
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -226,8 +234,11 @@ pure `design(request) -> response` adapter, independently testable without a soc
226
234
 
227
235
  `ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
228
236
  pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
229
- published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` on
237
+ published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
230
238
  each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
239
+ The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
240
+ as it does for the executables and PyPI; pushes to `main` and pull requests build and
241
+ smoke-test the image without publishing it.
231
242
  A manual run of that workflow with `publish` set publishes the current version's image from a
232
243
  later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
233
244
  image was published this way, the recipe having arrived after the release).
@@ -244,9 +255,9 @@ The image's defaults, all overridable with the environment variables above:
244
255
 
245
256
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
246
257
  the shared root and the cache: the server checks both with a write at startup. Its
247
- `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in a separate
248
- worker process, one at a time, so a caller sending several at once queues them; size the
249
- caller's timeout for that. `docker stop` stops a design in progress.
258
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
259
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
260
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
250
261
 
251
262
  Build and check it locally from the repository root:
252
263
 
@@ -258,6 +269,38 @@ python3 packaging/designer/smoke.py beqforge-designer
258
269
  The smoke script checks `/health` and one design inline and one by reference;
259
270
  `tests/test_designer_image.py` runs the same check against an in-process server.
260
271
 
272
+ ### Running the designer on Windows
273
+
274
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
275
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
276
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
277
+ exe and double-click it, or run the exe directly:
278
+
279
+ ```bat
280
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
281
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
282
+ ```
283
+
284
+ The launcher sets the image's defaults through the environment variables above, each only if
285
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
286
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
287
+
288
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
289
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
290
+ networks.
291
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
292
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
293
+ work directory. Without it, beqdesigner must send audio inline.
294
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
295
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
296
+ Check `http://<address>:8420/health` from there first.
297
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
298
+ design more than one title at a time; `/health` reports it as `parallel`.
299
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
300
+ press Ctrl+C, to stop.
301
+
302
+ ### Building and releasing
303
+
261
304
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
262
305
  including `serve-designer`) is built for Linux/macOS/Windows by
263
306
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -43,6 +43,7 @@ are retained so records and code comments remain traceable. Completed items are
43
43
  | 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. |
44
44
  | 25 | Documentation citations | open | Remove or repoint orphan references and stale descriptions of shipped behaviour. Low filter impact, but prevents future changes being based on obsolete assumptions. |
45
45
  | 26 | Texture yardstick retune (library review) | parked | 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). Reopen when the beqdesigner library review (~1,290 titles) is complete. Then 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). |
46
+ | 27 | Parameter-space optimisation for several profiles | implemented; release pending | `beqoptimiser.optimise_parameters` built and benchmarked (18 October 2026): within margin on 21/25 and 10/25 of the worst 48/96 kHz float32 entries against coefficient search's 20/25 and 16/25; see [the outcome](plans/parameter-space-optimisation.md#outcome-18-october-2026). Done once released in a version beqdesigner can pin. Originally: beqdesigner's main window will let a filter designer optimise a filter for chosen device profiles before publishing, in freq/Q/gain terms (a BEQ is published and loaded as parameters, so no catalogue schema change), minimising the worst error across all chosen profiles at once. The search belongs in `beqoptimiser`, beside `optimise()`; beqdesigner builds the interface. See [the request](plans/parameter-space-optimisation.md). |
46
47
 
47
48
  ## Review evidence
48
49
 
@@ -1,6 +1,6 @@
1
1
  """Shared filter primitives; no workflow package dependencies."""
2
2
 
3
- __version__ = "0.2.3"
3
+ __version__ = "0.2.5"
4
4
 
5
5
  from .types import BiquadSpec as BiquadSpec
6
6
  from .types import BiquadType as BiquadType
@@ -43,7 +43,7 @@ still holds: no other API, no other service, and the server is a thin transport
43
43
  | `beqforge/explain.py` | The plain-language account a designer response leads with — `found` (reference plateau, which channels carry it and which are silent, down to where there is real bass content, judged band, goal), `correction` (the cascade, then frequency by frequency what was wanted, what the content supports and before → after), `clipping` (the sub-feed peak and how far to turn the sub down, on the request's or the assumed bass management) and `alternatives` (why each other candidate lost). Reads the `Report`; measures and decides nothing. |
44
44
  | `beqforge/designer.py` | The `design(request) -> response` adapter between beqdesigner's `designer-interface.md` v1.1 and `beqforge.pipeline.run` — request/response dataclasses (1.1: every candidate the judge failed goes back in `rejected` with its failures, review only, beside the answer or the decline), wire-format (de)serialisation and the field-by-field mapping onto `Report`/`Candidate`/`Verdict`. Also `validate_response`, an independent re-implementation of beqdesigner's own §3-§5 rules (that repo isn't importable — PyQt6), called by the server before a response ever goes on the wire. Pure and independently testable; no socket. |
45
45
  | `tools/design_beq.py` | **The entry point.** One command, a filter and its reasoning. `--charts DIR` for the pictures; writes a `.run.json.gz` record beside the material unless `--no-record`. |
46
- | `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in one long-lived worker process started with "spawn" (`_DesignWorker`): the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock, so the process that forks must stay single-threaded — the worker's log records come back over a `SimpleQueue`, not a `Queue`, for that reason. One worker keeps designs serial (the contract is one synchronous POST per `design()` call); a worker that dies answers 500 and is replaced. SIGTERM shuts down like Ctrl-C, stopping the worker's process group, fit pool included. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond, queued); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version` and `shared_root`. |
46
+ | `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in a long-lived worker process started with "spawn" (`_DesignWorker`): the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock, so the process that forks must stay single-threaded — the worker's log records come back over a `SimpleQueue`, not a `Queue`, for that reason. A worker designs one title at a time; `--workers N` (1-8) runs N of them, each request taking the next idle one, and `/health` reports N as `parallel` so a caller can size its sends from the designer. With N > 1 each worker's fit pool is the logical processors less one, divided by N and capped at `filters.FIT_WORKERS` (`fit_share`), unless `BEQ_FIT_WORKERS` is set: concurrent designs are what can use SMT threads, and pool size changes timing, never an answer. A worker that dies answers 500 and is replaced; the others carry on. SIGTERM shuts down like Ctrl-C, stopping the worker's process group, fit pool included. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond, queued); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version`, `shared_root` and `parallel`. |
47
47
  | `tools/replay.py` | Redraw charts and export to beqdesigner from a record — no rerun, no extraction. Refuses on a stale record unless `--force`. |
48
48
  | `tools/extract.py` | ffmpeg → 1 kHz per-channel `.npz`. Requires an explicit supported channel layout; preserves layout provenance. Needs no beqdesigner. |
49
49
  | `tools/summarise.py` | Sanity-check an extraction before using it. |
@@ -8,6 +8,7 @@ unknown source spectrum.
8
8
 
9
9
  import logging
10
10
  import math
11
+ from concurrent.futures import ProcessPoolExecutor
11
12
  from dataclasses import dataclass, field, replace
12
13
  from typing import TYPE_CHECKING
13
14
 
@@ -633,7 +634,7 @@ def diagnose(
633
634
  # and reported the band top as where tracking stopped (A Few Good Men: 150.5-200 Hz found by
634
635
  # one, none by the other).
635
636
  _, mix_plateau = plateau_reference(mix_spectrum, spectrum_freqs, params)
636
- from beqforge.extraction import ExtractionParams, extract
637
+ from beqforge.extraction import ExtractionParams
637
638
 
638
639
  extraction_params = extraction_params or ExtractionParams()
639
640
  extraction_params = replace(
@@ -645,12 +646,18 @@ def diagnose(
645
646
  ),
646
647
  )
647
648
 
649
+ plateaus = {
650
+ name: plateau_reference(
651
+ spectra[name][band], freqs, params, one_sided_fallback=False
652
+ )
653
+ for name in material.channels
654
+ }
655
+ evidence = _channel_evidence(material, params, extraction_params, plateaus, freqs)
656
+
648
657
  channels: dict[str, ChannelDiagnosis] = {}
649
658
  for name in material.channels:
650
659
  response = spectra[name][band]
651
- level, plateau = plateau_reference(
652
- response, freqs, params, one_sided_fallback=False
653
- )
660
+ level, plateau = plateaus[name]
654
661
  response = np.where(
655
662
  unexcluded(freqs, params.exclude_bands_hz), response, np.nan
656
663
  )
@@ -666,10 +673,19 @@ def diagnose(
666
673
  passband_share = (
667
674
  float(share[in_share_band].mean()) if in_share_band.any() else 0.0
668
675
  )
669
- _, channel_spread, _, _, tracking = _temporal_evidence(
670
- material.channels[name], material.fs, params, plateau, freqs
671
- )
672
- envelope = extract(material.channels[name], material.fs, extraction_params)
676
+ if name in evidence:
677
+ (_, channel_spread, _, _, tracking), envelope, records = evidence[name]
678
+ for record in records:
679
+ logging.getLogger(record.name).handle(record)
680
+ else:
681
+ (_, channel_spread, _, _, tracking), envelope = _one_channel(
682
+ material.channels[name],
683
+ material.fs,
684
+ params,
685
+ plateau,
686
+ freqs,
687
+ extraction_params,
688
+ )
673
689
  channels[name] = ChannelDiagnosis(
674
690
  name=name,
675
691
  response_db=response,
@@ -707,6 +723,80 @@ def diagnose(
707
723
  )
708
724
 
709
725
 
726
+ def _one_channel(samples, fs, params, plateau, freqs, extraction_params):
727
+ """A channel's temporal evidence and envelopes: most of a cold analysis's time, and
728
+ independent of every other channel's."""
729
+ from beqforge.extraction import extract
730
+
731
+ return (
732
+ _temporal_evidence(samples, fs, params, plateau, freqs),
733
+ extract(samples, fs, extraction_params),
734
+ )
735
+
736
+
737
+ class _Collect(logging.Handler):
738
+ def __init__(self):
739
+ super().__init__()
740
+ self.records: list[logging.LogRecord] = []
741
+
742
+ def emit(self, record: logging.LogRecord) -> None:
743
+ # made picklable: the message rendered, any traceback as text
744
+ record.msg, record.args = record.getMessage(), None
745
+ if record.exc_info:
746
+ record.exc_text = logging.Formatter().formatException(record.exc_info)
747
+ record.exc_info = None
748
+ self.records.append(record)
749
+
750
+
751
+ def _one_channel_logged(level, *args):
752
+ """`_one_channel` in a pool process, its log records returned rather than emitted, so the
753
+ parent logs them in channel order on every platform: a spawned process has none of the
754
+ parent's handlers, and a forked one would print them out of order."""
755
+ root = logging.getLogger()
756
+ handlers, previous = root.handlers[:], root.level
757
+ collect = _Collect()
758
+ root.handlers[:] = [collect]
759
+ root.setLevel(level)
760
+ try:
761
+ return (*_one_channel(*args), collect.records)
762
+ finally:
763
+ root.handlers[:] = handlers
764
+ root.setLevel(previous)
765
+
766
+
767
+ def _channel_evidence(material, params, extraction_params, plateaus, freqs):
768
+ """`_one_channel` for every channel, with its log records, computed in parallel: a
769
+ process per channel up to the fitter's pool size, when the fitter may run in parallel
770
+ (`filters.PARALLEL_FITS`, `filters.FIT_WORKERS`). Empty otherwise, and `diagnose` works
771
+ through the channels in turn as it always did. Each channel's work is seeded and
772
+ self-contained, so the answer is the serial one; only the time differs."""
773
+ from beqforge import filters
774
+
775
+ names = list(material.channels)
776
+ tasks = [
777
+ (
778
+ material.channels[n],
779
+ material.fs,
780
+ params,
781
+ plateaus[n][1],
782
+ freqs,
783
+ extraction_params,
784
+ )
785
+ for n in names
786
+ ]
787
+ workers = min(len(names), filters.FIT_WORKERS) if filters.PARALLEL_FITS else 1
788
+ if workers < 2:
789
+ return {}
790
+ level = logging.getLogger().getEffectiveLevel()
791
+ with ProcessPoolExecutor(max_workers=workers) as pool:
792
+ results = list(
793
+ pool.map(
794
+ _one_channel_logged, [level] * len(tasks), *zip(*tasks, strict=True)
795
+ )
796
+ )
797
+ return dict(zip(names, results, strict=True))
798
+
799
+
710
800
  def _temporal_evidence(
711
801
  subject: np.ndarray,
712
802
  fs: float,
@@ -41,7 +41,8 @@ strings with additive feedback; internal SOS uses subtractive feedback.
41
41
  | File | Responsibility |
42
42
  | --- | --- |
43
43
  | `core.py` | Precision models, deterministic bounded search and independent validation |
44
- | `cache.py` | Public cached optimisation, immutable numerical results and seed lookup |
44
+ | `parameters.py` | Parameter (freq/Q/gain) search for several (rate, precision) profiles at once, on the publication grid |
45
+ | `cache.py` | Public cached optimisation (coefficients and parameters), immutable numerical results and seed lookup |
45
46
  | `cli.py` | Catalogue preparation, current-entry metadata, variant export and CLI |
46
47
  | `__init__.py` | Public library exports, including the cached `optimise` wrapper |
47
48
  | `data/seed.json.gz`, `data/seed-manifest.json` | Bundled numerical results and provenance |
@@ -53,6 +54,9 @@ dependency versions and execution architecture.
53
54
  Custom precision implementations without a complete explicit `cache_identity()` bypass caching.
54
55
  Corrupt entries are misses; unwritable storage must not prevent calculation or seed reuse.
55
56
  Use atomic writes and retain checksums, request identity and publication-policy validation.
57
+ Parameter results are keyed by that identity plus `parameters.py`'s own hash and
58
+ `PARAMETER_VERSION`, kept out of `implementation_identity()` so they never invalidate the seed;
59
+ put parameter-search code in `parameters.py`, never `core.py`, for the same reason.
56
60
 
57
61
  Bundled seeds are reusable only in their recorded numerical environment. Changes to numerical
58
62
  code or settings invalidate them; do not relabel old results as compatible. Bump
@@ -8,6 +8,9 @@ from .cache import (
8
8
  from .cache import (
9
9
  optimise as optimise,
10
10
  )
11
+ from .cache import (
12
+ optimise_parameters as optimise_parameters,
13
+ )
11
14
  from .core import (
12
15
  FixedPoint as FixedPoint,
13
16
  )
@@ -32,3 +35,15 @@ from .core import (
32
35
  from .core import (
33
36
  stable as stable,
34
37
  )
38
+ from .parameters import (
39
+ Fixed as Fixed,
40
+ )
41
+ from .parameters import (
42
+ ParameterResult as ParameterResult,
43
+ )
44
+ from .parameters import (
45
+ ParameterSettings as ParameterSettings,
46
+ )
47
+ from .parameters import (
48
+ ProfileError as ProfileError,
49
+ )