beqforge 0.2.3__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. beqforge-0.2.4/.gitattributes +1 -0
  2. {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-executable.yml +8 -0
  3. {beqforge-0.2.3 → beqforge-0.2.4}/PKG-INFO +43 -4
  4. {beqforge-0.2.3 → beqforge-0.2.4}/README.md +42 -3
  5. {beqforge-0.2.3 → beqforge-0.2.4}/TODO.md +1 -0
  6. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/__init__.py +1 -1
  7. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/AGENTS.md +1 -1
  8. beqforge-0.2.4/packaging/designer/beqforge-designer.cmd +33 -0
  9. {beqforge-0.2.3 → beqforge-0.2.4}/plans/done-design-and-pipeline.md +31 -0
  10. beqforge-0.2.4/plans/parameter-space-optimisation.md +75 -0
  11. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_designer_server.py +61 -5
  12. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_designer_server_startup.py +19 -2
  13. {beqforge-0.2.3 → beqforge-0.2.4}/tools/designer_server.py +93 -14
  14. {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_exe.py +21 -10
  15. {beqforge-0.2.3 → beqforge-0.2.4}/.dockerignore +0 -0
  16. {beqforge-0.2.3 → beqforge-0.2.4}/.github/dependabot.yml +0 -0
  17. {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-designer-image.yml +0 -0
  18. {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-device-check.yml +0 -0
  19. {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/dependabot-auto-merge.yaml +0 -0
  20. {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/tests.yml +0 -0
  21. {beqforge-0.2.3 → beqforge-0.2.4}/.gitignore +0 -0
  22. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/.gitignore +0 -0
  23. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/beqforge.iml +0 -0
  24. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/editor.xml +0 -0
  25. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/jsonSchemas.xml +0 -0
  26. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/misc.xml +0 -0
  27. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/modules.xml +0 -0
  28. {beqforge-0.2.3 → beqforge-0.2.4}/.idea/vcs.xml +0 -0
  29. {beqforge-0.2.3 → beqforge-0.2.4}/AGENTS.md +0 -0
  30. {beqforge-0.2.3 → beqforge-0.2.4}/CLAUDE.md +0 -0
  31. {beqforge-0.2.3 → beqforge-0.2.4}/LICENCE.md +0 -0
  32. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/AGENTS.md +0 -0
  33. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/biquad.py +0 -0
  34. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/provenance.py +0 -0
  35. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/publication.py +0 -0
  36. {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/types.py +0 -0
  37. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/__init__.py +0 -0
  38. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/accept.py +0 -0
  39. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/beqd.py +0 -0
  40. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/biquad.py +0 -0
  41. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/cache.py +0 -0
  42. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/charts.py +0 -0
  43. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/cli.py +0 -0
  44. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/design.py +0 -0
  45. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/designer.py +0 -0
  46. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/diagnose.py +0 -0
  47. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/explain.py +0 -0
  48. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/extraction.py +0 -0
  49. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/filters.py +0 -0
  50. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/harness.py +0 -0
  51. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/identify.py +0 -0
  52. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/material.py +0 -0
  53. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/pipeline.py +0 -0
  54. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/record.py +0 -0
  55. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/reference.py +0 -0
  56. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/rolloff.py +0 -0
  57. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/verify.py +0 -0
  58. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge-device-check.spec +0 -0
  59. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge.spec +0 -0
  60. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/AGENTS.md +0 -0
  61. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/__init__.py +0 -0
  62. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/analyse.py +0 -0
  63. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/audio.py +0 -0
  64. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/catalogue.py +0 -0
  65. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/cli.py +0 -0
  66. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/coefficients.py +0 -0
  67. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/engines.py +0 -0
  68. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/evidence.py +0 -0
  69. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/manifest.py +0 -0
  70. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/measurement.py +0 -0
  71. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/profiles.py +0 -0
  72. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/qualification.py +0 -0
  73. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/report.py +0 -0
  74. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/transactions.py +0 -0
  75. {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/verify.py +0 -0
  76. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/AGENTS.md +0 -0
  77. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/__init__.py +0 -0
  78. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/cache.py +0 -0
  79. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/cli.py +0 -0
  80. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/core.py +0 -0
  81. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/data/seed-manifest.json +0 -0
  82. {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/data/seed.json.gz +0 -0
  83. {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
  84. {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check-examples/simulation.json +0 -0
  85. {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check.md +0 -0
  86. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/README.md +0 -0
  87. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
  88. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
  89. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/example-1.png +0 -0
  90. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/example-2.png +0 -0
  91. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/entry-results.json.gz +0 -0
  92. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/statistics.json +0 -0
  93. {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser.md +0 -0
  94. {beqforge-0.2.3 → beqforge-0.2.4}/evidence_validation.json +0 -0
  95. {beqforge-0.2.3 → beqforge-0.2.4}/negative_corpus.json +0 -0
  96. {beqforge-0.2.3 → beqforge-0.2.4}/packaging/designer/Dockerfile +0 -0
  97. {beqforge-0.2.3 → beqforge-0.2.4}/packaging/designer/smoke.py +0 -0
  98. {beqforge-0.2.3 → beqforge-0.2.4}/packaging/device-check/NOTICE.txt +0 -0
  99. {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-catalogue-filter-optimisation.md +0 -0
  100. {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-device-precision-validation.md +0 -0
  101. {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-package-consolidation.md +0 -0
  102. {beqforge-0.2.3 → beqforge-0.2.4}/plans/R2a-server-stage-cache.md +0 -0
  103. {beqforge-0.2.3 → beqforge-0.2.4}/plans/done-baseline-evidence.md +0 -0
  104. {beqforge-0.2.3 → beqforge-0.2.4}/plans/research-design-decisions.md +0 -0
  105. {beqforge-0.2.3 → beqforge-0.2.4}/plans/test-portability-and-runtime.md +0 -0
  106. {beqforge-0.2.3 → beqforge-0.2.4}/plans/texture-yardstick-sub-band.md +0 -0
  107. {beqforge-0.2.3 → beqforge-0.2.4}/plans/texture-yardstick-sub-band.patch +0 -0
  108. {beqforge-0.2.3 → beqforge-0.2.4}/pyproject.toml +0 -0
  109. {beqforge-0.2.3 → beqforge-0.2.4}/tests/__init__.py +0 -0
  110. {beqforge-0.2.3 → beqforge-0.2.4}/tests/conftest.py +0 -0
  111. {beqforge-0.2.3 → beqforge-0.2.4}/tests/fixtures/optimiser_seed_entry.json +0 -0
  112. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_cli.py +0 -0
  113. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_accept.py +0 -0
  114. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_cache.py +0 -0
  115. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_cache_stores.py +0 -0
  116. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_channel_evidence.py +0 -0
  117. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_charts.py +0 -0
  118. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_corpus.py +0 -0
  119. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_design.py +0 -0
  120. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_designer.py +0 -0
  121. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_device_rate.py +0 -0
  122. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_diagnose.py +0 -0
  123. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_evidence.py +0 -0
  124. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_evidence_excess.py +0 -0
  125. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_exclusions.py +0 -0
  126. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_explain.py +0 -0
  127. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_extraction.py +0 -0
  128. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_filters.py +0 -0
  129. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_frozen_cache.py +0 -0
  130. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_guard.py +0 -0
  131. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_harness.py +0 -0
  132. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_headroom.py +0 -0
  133. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_hum.py +0 -0
  134. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_identify.py +0 -0
  135. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_jacobian.py +0 -0
  136. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_parallel.py +0 -0
  137. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_pipeline.py +0 -0
  138. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_playback.py +0 -0
  139. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_playback_headroom.py +0 -0
  140. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_publication.py +0 -0
  141. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_record.py +0 -0
  142. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_record_fingerprint.py +0 -0
  143. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_reference.py +0 -0
  144. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_references.py +0 -0
  145. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_ripple_band.py +0 -0
  146. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_rolloff.py +0 -0
  147. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_strategy_config.py +0 -0
  148. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_timings.py +0 -0
  149. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_verify.py +0 -0
  150. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_designer_image.py +0 -0
  151. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_analyse.py +0 -0
  152. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_catalogue.py +0 -0
  153. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_catalogue_predict.py +0 -0
  154. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_cli.py +0 -0
  155. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_engines.py +0 -0
  156. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_evidence.py +0 -0
  157. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_examples.py +0 -0
  158. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_manifest.py +0 -0
  159. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_measurement.py +0 -0
  160. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_qualification.py +0 -0
  161. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_transactions.py +0 -0
  162. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_verify.py +0 -0
  163. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_extract_layout.py +0 -0
  164. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser.py +0 -0
  165. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser_cache.py +0 -0
  166. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser_report.py +0 -0
  167. {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_package_boundaries.py +0 -0
  168. {beqforge-0.2.3 → beqforge-0.2.4}/tools/__init__.py +0 -0
  169. {beqforge-0.2.3 → beqforge-0.2.4}/tools/build_optimiser_seed.py +0 -0
  170. {beqforge-0.2.3 → beqforge-0.2.4}/tools/design_beq.py +0 -0
  171. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/a1_sweep.py +0 -0
  172. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/compare_records.py +0 -0
  173. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/compare_verdicts.py +0 -0
  174. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/inject_variants.py +0 -0
  175. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_compare.py +0 -0
  176. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_jacobian.py +0 -0
  177. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_run_pipeline.py +0 -0
  178. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_surrogate.py +0 -0
  179. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_greedy.py +0 -0
  180. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_seeded.py +0 -0
  181. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_structure.py +0 -0
  182. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_greedy.py +0 -0
  183. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_seeded.py +0 -0
  184. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_structure.py +0 -0
  185. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p20_smooth_drift.py +0 -0
  186. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/probe.py +0 -0
  187. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/refit_probe.py +0 -0
  188. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/request_timing.py +0 -0
  189. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/score_injected.py +0 -0
  190. {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/t8_floor_rules.py +0 -0
  191. {beqforge-0.2.3 → beqforge-0.2.4}/tools/extract.py +0 -0
  192. {beqforge-0.2.3 → beqforge-0.2.4}/tools/ledger_template.html +0 -0
  193. {beqforge-0.2.3 → beqforge-0.2.4}/tools/negative_corpus.py +0 -0
  194. {beqforge-0.2.3 → beqforge-0.2.4}/tools/optimiser_catalogue_report.py +0 -0
  195. {beqforge-0.2.3 → beqforge-0.2.4}/tools/prepare_device_check_helper.py +0 -0
  196. {beqforge-0.2.3 → beqforge-0.2.4}/tools/render_ledger.py +0 -0
  197. {beqforge-0.2.3 → beqforge-0.2.4}/tools/replay.py +0 -0
  198. {beqforge-0.2.3 → beqforge-0.2.4}/tools/seed_optimiser_cache.py +0 -0
  199. {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_device_check.py +0 -0
  200. {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_install.py +0 -0
  201. {beqforge-0.2.3 → beqforge-0.2.4}/tools/summarise.py +0 -0
  202. {beqforge-0.2.3 → beqforge-0.2.4}/tools/validate_evidence.py +0 -0
  203. {beqforge-0.2.3 → beqforge-0.2.4}/uv.lock +0 -0
@@ -0,0 +1 @@
1
+ *.cmd text eol=crlf
@@ -226,6 +226,14 @@ 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
+
229
237
  publish:
230
238
  name: Publish Python package to PyPI
231
239
  needs: [validated_tests, package, build, create_release]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: beqforge
3
- Version: 0.2.3
3
+ Version: 0.2.4
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,12 @@ 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. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
194
+ if set, stays each design's own). The share changes how long a fit takes, never its answer.
195
+
190
196
  Every `serve-designer` configuration option can also be set through an environment variable.
191
197
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
192
198
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -207,6 +213,7 @@ their existing CLI configuration.
207
213
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
208
214
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
209
215
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
216
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
210
217
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
211
218
 
212
219
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -270,9 +277,9 @@ The image's defaults, all overridable with the environment variables above:
270
277
 
271
278
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
272
279
  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.
280
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
281
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
282
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
276
283
 
277
284
  Build and check it locally from the repository root:
278
285
 
@@ -284,6 +291,38 @@ python3 packaging/designer/smoke.py beqforge-designer
284
291
  The smoke script checks `/health` and one design inline and one by reference;
285
292
  `tests/test_designer_image.py` runs the same check against an in-process server.
286
293
 
294
+ ### Running the designer on Windows
295
+
296
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
297
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
298
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
299
+ exe and double-click it, or run the exe directly:
300
+
301
+ ```bat
302
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
303
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
304
+ ```
305
+
306
+ The launcher sets the image's defaults through the environment variables above, each only if
307
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
308
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
309
+
310
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
311
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
312
+ networks.
313
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
314
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
315
+ work directory. Without it, beqdesigner must send audio inline.
316
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
317
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
318
+ Check `http://<address>:8420/health` from there first.
319
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
320
+ design more than one title at a time; `/health` reports it as `parallel`.
321
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
322
+ press Ctrl+C, to stop.
323
+
324
+ ### Building and releasing
325
+
287
326
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
288
327
  including `serve-designer`) is built for Linux/macOS/Windows by
289
328
  `.github/workflows/build-executable.yml` on every `vX.Y.Z` tag, and attached to the matching
@@ -161,6 +161,12 @@ 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. Each design's fit pool gets an equal share of the machine's cores (`BEQ_FIT_WORKERS`,
168
+ if set, stays each design's own). The share changes how long a fit takes, never its answer.
169
+
164
170
  Every `serve-designer` configuration option can also be set through an environment variable.
165
171
  Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
166
172
  environment list. `--help` lists the variable beside each option. Other subcommands retain
@@ -181,6 +187,7 @@ their existing CLI configuration.
181
187
  | `--record-dir` | `BEQFORGE_RECORD_DIR` | path |
182
188
  | `--cache-dir` | `BEQFORGE_CACHE_DIR` | path |
183
189
  | `--shared-root` | `BEQFORGE_SHARED_ROOT` | path |
190
+ | `--workers` | `BEQFORGE_WORKERS` | integer, 1-8 |
184
191
  | `--quiet` | `BEQFORGE_QUIET` | boolean |
185
192
 
186
193
  Booleans accept `true`/`false`, `yes`/`no`, `on`/`off` or `1`/`0`, ignoring case.
@@ -244,9 +251,9 @@ The image's defaults, all overridable with the environment variables above:
244
251
 
245
252
  It runs as uid/gid 1000 (`--build-arg PUID=… PGID=…` to change), which must be able to write
246
253
  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.
254
+ `HEALTHCHECK` asks `/health`, which answers while a design runs: designs run in separate
255
+ worker processes, `BEQFORGE_WORKERS` (default 1) at a time, so a caller sending more at once
256
+ queues them; size the caller's timeout for that. `docker stop` stops a design in progress.
250
257
 
251
258
  Build and check it locally from the repository root:
252
259
 
@@ -258,6 +265,38 @@ python3 packaging/designer/smoke.py beqforge-designer
258
265
  The smoke script checks `/health` and one design inline and one by reference;
259
266
  `tests/test_designer_image.py` runs the same check against an in-process server.
260
267
 
268
+ ### Running the designer on Windows
269
+
270
+ The release's `beqforge-windows-x86_64.exe` runs the same server natively, for a beqdesigner
271
+ container (or install) that calls out to a Windows machine. Put `beqforge-designer.cmd`, which
272
+ is attached to the same release (source: `packaging/designer/beqforge-designer.cmd`), beside the
273
+ exe and double-click it, or run the exe directly:
274
+
275
+ ```bat
276
+ beqforge-windows-x86_64.exe serve-designer --host 0.0.0.0 --port 8420 ^
277
+ --shared-root D:\beq\work --cache-dir %LOCALAPPDATA%\beqforge\cache
278
+ ```
279
+
280
+ The launcher sets the image's defaults through the environment variables above, each only if
281
+ not already set: `BEQFORGE_HOST=0.0.0.0`, `BEQFORGE_PORT=8420` and
282
+ `BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache`. Arguments pass through to `serve-designer`.
283
+
284
+ * **Listen on the network.** The CLI's default host is `127.0.0.1`, which a container cannot
285
+ reach; the launcher uses `0.0.0.0`. Windows Firewall asks the first time: allow private
286
+ networks.
287
+ * **Shared root.** For audio by reference, set `BEQFORGE_SHARED_ROOT` (or edit the commented
288
+ line in the launcher) to the Windows folder bind-mounted into beqdesigner's container as its
289
+ work directory. Without it, beqdesigner must send audio inline.
290
+ * **beqdesigner's side.** Under Docker Desktop on the same machine, register
291
+ `http://host.docker.internal:8420/design`; from another machine, use this one's LAN address.
292
+ Check `http://<address>:8420/health` from there first.
293
+ * **Several at once.** Set `BEQFORGE_WORKERS` (or pass `--workers 2`) for a machine that can
294
+ design more than one title at a time; `/health` reports it as `parallel`.
295
+ * **The rest.** The exe is unsigned, so SmartScreen warns on first run. `serve-designer` does not need `ffmpeg`. Close the window, or
296
+ press Ctrl+C, to stop.
297
+
298
+ ### Building and releasing
299
+
261
300
  For a machine without Python/`uv`, a standalone `beqforge` executable (every subcommand,
262
301
  including `serve-designer`) is built for Linux/macOS/Windows by
263
302
  `.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 | open | 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.4"
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 `filters.FIT_WORKERS // N` (`fit_share`) unless `BEQ_FIT_WORKERS` is set: 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. |
@@ -0,0 +1,33 @@
1
+ @echo off
2
+ rem Runs the beqforge designer for beqdesigner on Windows: `beqforge serve-designer`, with the
3
+ rem container image's defaults (README, "Running the designer on Windows"). Keep this file
4
+ rem beside the release's beqforge-windows-x86_64.exe and double-click it, or run it from a
5
+ rem prompt with any extra serve-designer options, e.g. `beqforge-designer.cmd --record-dir D:\beq\records`.
6
+ rem
7
+ rem Every BEQFORGE_* variable already set in the environment wins over the defaults below.
8
+ setlocal
9
+
10
+ rem beqdesigner's work directory, as this machine sees it, so audio arrives by reference rather
11
+ rem than inline. Uncomment and point it at the folder mounted into beqdesigner's container.
12
+ rem if not defined BEQFORGE_SHARED_ROOT set "BEQFORGE_SHARED_ROOT=D:\beq\work"
13
+
14
+ rem How many titles to design at once, if this machine can take more than one (0.2.4 on).
15
+ rem if not defined BEQFORGE_WORKERS set "BEQFORGE_WORKERS=2"
16
+
17
+ if not defined BEQFORGE_HOST set "BEQFORGE_HOST=0.0.0.0"
18
+ if not defined BEQFORGE_PORT set "BEQFORGE_PORT=8420"
19
+ if not defined BEQFORGE_CACHE_DIR set "BEQFORGE_CACHE_DIR=%LOCALAPPDATA%\beqforge\cache"
20
+
21
+ set "EXE=%~dp0beqforge-windows-x86_64.exe"
22
+ if not exist "%EXE%" set "EXE=%~dp0beqforge.exe"
23
+ if not exist "%EXE%" (
24
+ echo beqforge-windows-x86_64.exe was not found beside %~nx0
25
+ pause
26
+ exit /b 1
27
+ )
28
+
29
+ "%EXE%" serve-designer %*
30
+ set "STATUS=%ERRORLEVEL%"
31
+ rem keep the window open on a startup error, so a double-click shows why
32
+ if not "%STATUS%"=="0" pause
33
+ exit /b %STATUS%
@@ -748,6 +748,37 @@ beside the answer or the decline (probe unchanged; no decision touched).
748
748
  caller's; `beqforge_revision` is already in every response for it.
749
749
 
750
750
 
751
+ ## Several designs at once, advertised on `/health` (18 October 2026)
752
+
753
+ A Windows machine that could design two titles at once was added beside the container as a
754
+ second endpoint, and the only way to use it was two servers on two ports: one server had a
755
+ single design worker. Saying how many each endpoint takes in beqdesigner's profile also put a
756
+ fact about the designer's host in the caller's configuration.
757
+
758
+ `serve-designer --workers N` (`BEQFORGE_WORKERS`, 1-8, the range beqdesigner accepts for an
759
+ endpoint's `parallel`) now runs N `_DesignWorker`s, each still one spawned, single-threaded
760
+ process. A request takes the next idle worker and waits for one when none is, so arrival order
761
+ is kept and a worker that dies fails only its own request. `/health` gains `"parallel": N` for
762
+ a caller to size its sends from; beqdesigner was sent the caller-side proposal. With N > 1 each
763
+ worker's fit pool is `filters.FIT_WORKERS // N` (`fit_share`), so N designs do not each claim
764
+ the whole machine; an explicit `BEQ_FIT_WORKERS` is left as each design's own. `_run_fits`
765
+ returns results in submission order from fixed seeds, so pool size moves timing and never an
766
+ answer. Nothing under `beqforge/` changed, so the decision regression probe does not apply.
767
+
768
+ * **Checks.** New tests: with two workers a quick request is answered while a real design is
769
+ still running on the other (the same test fails with one worker, so it distinguishes them),
770
+ `/health` reports `parallel`, a count outside 1-8 is refused by the server and the CLI, the
771
+ option comes from the environment, and the fit share. Server, startup, image and parallel-fit
772
+ tests: 61 passed. `tools/smoke_test_exe.py` now starts the executable with `--workers 2` and
773
+ requires `parallel: 2`. Its three requests alternate between the workers, so both design for
774
+ real inside the frozen build and the cache hit is one worker reusing the other's entries. A
775
+ Linux PyInstaller build passed it, with no process left after shutdown. Windows and macOS
776
+ are covered by the release workflow's smoke test, not run here.
777
+ * **Windows packaging.** `packaging/designer/beqforge-designer.cmd` starts the release's
778
+ Windows executable with the container's defaults and is attached to each release; the README
779
+ gains "Running the designer on Windows".
780
+
781
+
751
782
  ## The designer server answers `/health` during a design (17 October 2026)
752
783
 
753
784
  A deployed image went unhealthy while it designed correctly. `serve-designer` was a plain
@@ -0,0 +1,75 @@
1
+ # Optimising a filter's parameters for several device profiles at once
2
+
3
+ **Status: design, not built.** A request from beqdesigner (October 2026), for a feature in its main window: a filter
4
+ designer picks one or more device profiles, sees what each does to the filter, and asks for the filter to be optimised
5
+ before it is published. The person who owns both projects decided that the search belongs here, in `beqoptimiser`, and
6
+ beqdesigner only provides the interface. Parameter search is already listed as a follow-up in
7
+ [F2-catalogue-filter-optimisation.md](F2-catalogue-filter-optimisation.md).
8
+
9
+ ## What is different from `optimise()`
10
+
11
+ `optimise()` searches the coefficients of one cascade for one rate and precision, and returns custom SOS. This request
12
+ needs:
13
+
14
+ 1. **Parameters out, not coefficients.** The result is each section's `freq`, `q` and `gain` (and its type unchanged).
15
+ A BEQ is published and loaded as parameters: beqdesigner's projects and exports, the catalogue, and ezbeq (which
16
+ rebuilds RBJ coefficients from the parameters at each device's rate when it has none published for it) all carry
17
+ parameters. Optimised parameters therefore need no new catalogue schema and reach every consumer as they are.
18
+ 2. **Several profiles jointly.** The same parameters are realised on every device. The search minimises the worst
19
+ matching-band error across all the profiles the caller passes (a minimax), so a change that helps one device cannot
20
+ quietly make another worse. The result reports each profile's error before and after.
21
+ 3. **Representable parameters.** Only values beqdesigner can show and the exports keep: `freq` to 0.01 Hz, `q` (and a
22
+ shelf's `S`) to 0.0001, `gain` to 0.01 dB. Search on that grid, or round and re-validate, so the result is exactly
23
+ what is published.
24
+
25
+ ## Proposed interface
26
+
27
+ ```python
28
+ optimise_parameters(
29
+ sections, # the cascade as authored: beqoptimiser.core.Section (type, freq, q, gain), in order
30
+ *,
31
+ profiles, # [(rate, precision), ...]: 48000 or 96000, with Float32() or FixedPoint(i, f)
32
+ settings=None, # Settings: margin, band, plus the parameter bounds below
33
+ ) -> ParameterResult
34
+ ```
35
+
36
+ `ParameterResult` carries the outcome (with the same `replacement`/`improvement`/`no_replacement` rules as
37
+ `optimise()`), the new sections when there is one, and per profile: the original error, the candidate's error and its
38
+ guard (out-of-band) error, with the numerical version and settings for provenance.
39
+
40
+ * **Reference:** each section's ideal float64 RBJ response, from the authored parameters, as now.
41
+ * **Realised:** for each profile, the coefficients computed from the candidate parameters at that rate in float64,
42
+ then stored with that profile's precision (`quantise`), as a device that receives parameters (or coefficients
43
+ computed from them) would hold them.
44
+ * **Bounds:** the parameters may only move as far as keeps the filter the author's: proposed defaults of ±1% in
45
+ frequency, ±2% in Q and ±0.1 dB in gain per section, configurable. A result outside them is not a correction of
46
+ the same filter.
47
+ * **Sections it cannot change:** the request may contain other biquads (pass filters, a gain stage). They stay
48
+ fixed but count in the cascade's response; a cascade with nothing searchable returns `no_replacement`.
49
+
50
+ ## Open points for this repository to decide
51
+
52
+ * **Whose RBJ:** the coefficients must be computed as the consumers compute them. beqdesigner (`model/iir.py`),
53
+ `beq_common.biquad` and ezbeq each implement the cookbook; check that they agree, including the shelf Q/S conversion,
54
+ to the precision a profile can resolve. If they do not, name the one a profile models.
55
+ * **Search method:** a coordinate or pattern search on the parameter grid, a scalar minimiser per parameter as now, or
56
+ something else; deterministic, bounded and cacheable as `optimise()` is.
57
+ * **Whether it ever helps:** benchmark against direct-coefficient search on the catalogue's float32/96 kHz worst cases
58
+ (e.g. the 5 Hz Q 6 peak the 2x4 HD pilot measured). Parameter search may recover little where the coefficient grid is
59
+ coarse; the outcome rules already report that honestly as `no_replacement`.
60
+
61
+ ## What beqdesigner will build around it
62
+
63
+ * Device profiles: one per device it exports to (miniDSP HD-class: float32 at 96 kHz; earlier miniDSPs: Q5.23 fixed
64
+ point at 48 kHz; HTP-1 and JRiver compute in double precision, so nothing to show), plus profiles a user defines
65
+ (rate and storage). It uses `beqoptimiser`'s `Float32`/`FixedPoint` and `magnitude` for part (a).
66
+ * Part (a), now: the main chart shows the filter as each chosen profile realises it, and its error against the ideal.
67
+ * Part (b), after this ships: an *Optimise for devices* action on the filter, which calls `optimise_parameters` with the
68
+ ticked profiles, shows the before/after errors per profile and lets the designer accept the new parameters.
69
+
70
+ ## Completion criteria
71
+
72
+ * `optimise_parameters` in `beqoptimiser` with tests covering: both rates, both precisions, the joint (minimax)
73
+ objective protecting each profile, the parameter grid and bounds, fixed non-searchable sections, and cache identity.
74
+ * A short benchmark against `optimise()` on the catalogue report's worst float32 cases, recorded here.
75
+ * Released in a `beqforge` version beqdesigner can pin.
@@ -20,6 +20,7 @@ import http.client
20
20
  import json
21
21
  import logging
22
22
  import threading
23
+ import time
23
24
  from dataclasses import replace
24
25
 
25
26
  import numpy as np
@@ -149,7 +150,12 @@ def _known_filter_request(duration_s: float = 150.0, seed: int = 101) -> dict:
149
150
  def test_health_check(server) -> None:
150
151
  status, body = _request(server, "GET", "/health")
151
152
  assert status == 200
152
- assert body == {"status": "ok", "contract_version": "1.2", "shared_root": False}
153
+ assert body == {
154
+ "status": "ok",
155
+ "contract_version": "1.2",
156
+ "shared_root": False,
157
+ "parallel": 1,
158
+ }
153
159
 
154
160
 
155
161
  def test_unknown_get_path_is_404(server) -> None:
@@ -250,7 +256,7 @@ def test_health_answers_while_a_design_runs(server) -> None:
250
256
  def test_a_dead_worker_is_a_500_and_the_next_request_gets_a_new_one() -> None:
251
257
  httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None)
252
258
  try:
253
- for process in list(httpd.worker._pool._processes.values()):
259
+ for process in list(httpd.workers[0]._pool._processes.values()):
254
260
  process.kill()
255
261
  process.join(timeout=10)
256
262
  status, body = _request(
@@ -267,8 +273,52 @@ def test_a_dead_worker_is_a_500_and_the_next_request_gets_a_new_one() -> None:
267
273
  _stop(httpd, thread)
268
274
 
269
275
 
270
- def _serve(params: PipelineParams, cache, shared_root=None):
271
- """A server of its own, with its own settings and its own worker process."""
276
+ def test_a_second_worker_designs_while_the_first_is_busy() -> None:
277
+ """With two workers a quick request is answered while a slow one is still designing;
278
+ with one it would queue behind it. `/health` says how many it takes at once."""
279
+ httpd, thread = _serve(PipelineParams(strategies=("flatten",)), None, workers=2)
280
+ try:
281
+ assert _request(httpd.server_address, "GET", "/health")[1]["parallel"] == 2
282
+ slow = []
283
+ designing = threading.Thread(
284
+ target=lambda: slow.append(
285
+ _request(
286
+ httpd.server_address,
287
+ "POST",
288
+ DESIGN_PATH,
289
+ body=_known_filter_request(),
290
+ timeout=DESIGN_TIMEOUT_S,
291
+ )
292
+ )
293
+ )
294
+ designing.start()
295
+ try:
296
+ time.sleep(0.5) # let the slow request take a worker first
297
+ status, body = _request(
298
+ httpd.server_address, "POST", DESIGN_PATH, _no_evidence_request()
299
+ )
300
+ assert status == 200
301
+ assert body["decline_reason"] == "channel_evidence_unavailable"
302
+ assert designing.is_alive()
303
+ finally:
304
+ designing.join(timeout=DESIGN_TIMEOUT_S)
305
+ assert slow[0][0] == 200
306
+ finally:
307
+ _stop(httpd, thread)
308
+
309
+
310
+ def test_a_worker_count_outside_the_callers_range_is_refused() -> None:
311
+ for workers in (0, 9):
312
+ with pytest.raises(ValueError, match="workers must be 1-8"):
313
+ DesignServer(
314
+ ("127.0.0.1", 0),
315
+ DesignSettings(PipelineParams(strategies=("flatten",))),
316
+ workers=workers,
317
+ )
318
+
319
+
320
+ def _serve(params: PipelineParams, cache, shared_root=None, workers: int = 1):
321
+ """A server of its own, with its own settings and its own worker processes."""
272
322
  # These tests exercise transport and cache identity/reuse, not section-budget
273
323
  # escalation. Still run the real pipeline with a bounded numerical search;
274
324
  # test_accepts_a_real_injected_rolloff_over_http retains the full budget.
@@ -277,6 +327,7 @@ def _serve(params: PipelineParams, cache, shared_root=None):
277
327
  DesignSettings(
278
328
  replace(params, max_sections=1), cache=cache, shared_root=shared_root
279
329
  ),
330
+ workers=workers,
280
331
  )
281
332
  thread = threading.Thread(target=httpd.serve_forever, daemon=True)
282
333
  thread.start()
@@ -436,7 +487,12 @@ def test_health_says_whether_references_are_accepted(tmp_path) -> None:
436
487
  finally:
437
488
  _stop(httpd, thread)
438
489
  assert status == 200
439
- assert body == {"status": "ok", "contract_version": "1.2", "shared_root": True}
490
+ assert body == {
491
+ "status": "ok",
492
+ "contract_version": "1.2",
493
+ "shared_root": True,
494
+ "parallel": 1,
495
+ }
440
496
 
441
497
 
442
498
  def test_a_request_by_reference_answers_as_the_same_request_inline(tmp_path) -> None:
@@ -113,6 +113,7 @@ def test_every_server_option_can_come_from_environment(startup, monkeypatch, tmp
113
113
  "RECORD_DIR": str(tmp_path / "records"),
114
114
  "CACHE_DIR": str(tmp_path / "cache"),
115
115
  "SHARED_ROOT": str(tmp_path / "shared"),
116
+ "WORKERS": "2",
116
117
  "QUIET": "1",
117
118
  }
118
119
  for name, value in settings.items():
@@ -134,7 +135,7 @@ def test_every_server_option_can_come_from_environment(startup, monkeypatch, tmp
134
135
  assert args.quiet is True
135
136
  (address, settings), options = constructor.call_args
136
137
  assert address == ("127.0.0.2", 9123)
137
- assert options == {"log_level": logging.WARNING}
138
+ assert options == {"log_level": logging.WARNING, "workers": 2}
138
139
  params = settings.params
139
140
  assert params.realisation.fs == 48000
140
141
  assert params.realisation.coefficient_bits == 24
@@ -177,7 +178,7 @@ def test_cli_replaces_environment_values_including_repeated_and_boolean_options(
177
178
  )
178
179
  (address, settings), options = constructor.call_args
179
180
  assert address == ("127.0.0.1", 8123)
180
- assert options == {"log_level": logging.INFO}
181
+ assert options == {"log_level": logging.INFO, "workers": 1}
181
182
  params = settings.params
182
183
  assert params.strategies == ("flatten", "counterfactual")
183
184
  assert params.exclude_bands_hz == ((30.0, 32.0),)
@@ -207,6 +208,8 @@ def test_false_environment_boolean(startup, monkeypatch, value):
207
208
  ("EXCLUDE", "[18, 22]", "must be a JSON array of pairs"),
208
209
  ("EXCLUDE", '[[18, "invalid"]]', "invalid float value"),
209
210
  ("QUIET", "perhaps", "BEQFORGE_QUIET must be true/false"),
211
+ ("WORKERS", "0", "workers must be 1-8"),
212
+ ("WORKERS", "9", "workers must be 1-8"),
210
213
  ],
211
214
  )
212
215
  def test_invalid_environment_fails_before_listening(
@@ -244,7 +247,21 @@ def test_help_lists_every_environment_option_even_with_invalid_environment(
244
247
  "RECORD_DIR",
245
248
  "CACHE_DIR",
246
249
  "SHARED_ROOT",
250
+ "WORKERS",
247
251
  "QUIET",
248
252
  ):
249
253
  assert f"BEQFORGE_{name}" in output
250
254
  constructor.assert_not_called()
255
+
256
+
257
+ def test_concurrent_designs_share_the_fit_pool(monkeypatch):
258
+ monkeypatch.delenv("BEQ_FIT_WORKERS", raising=False)
259
+ monkeypatch.setattr(designer_server.filters, "FIT_WORKERS", 7)
260
+ assert designer_server.fit_share(1) is None
261
+ assert designer_server.fit_share(2) == 3
262
+ assert designer_server.fit_share(8) == 1
263
+
264
+
265
+ def test_an_explicit_fit_pool_is_each_designs_own(monkeypatch):
266
+ monkeypatch.setenv("BEQ_FIT_WORKERS", "4")
267
+ assert designer_server.fit_share(2) is None