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.
- beqforge-0.2.4/.gitattributes +1 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-executable.yml +8 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/PKG-INFO +43 -4
- {beqforge-0.2.3 → beqforge-0.2.4}/README.md +42 -3
- {beqforge-0.2.3 → beqforge-0.2.4}/TODO.md +1 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/__init__.py +1 -1
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/AGENTS.md +1 -1
- beqforge-0.2.4/packaging/designer/beqforge-designer.cmd +33 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/done-design-and-pipeline.md +31 -0
- beqforge-0.2.4/plans/parameter-space-optimisation.md +75 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_designer_server.py +61 -5
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_designer_server_startup.py +19 -2
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/designer_server.py +93 -14
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_exe.py +21 -10
- {beqforge-0.2.3 → beqforge-0.2.4}/.dockerignore +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/dependabot.yml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-designer-image.yml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/build-device-check.yml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/dependabot-auto-merge.yaml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.github/workflows/tests.yml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.gitignore +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/.gitignore +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/beqforge.iml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/editor.xml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/jsonSchemas.xml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/misc.xml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/modules.xml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/.idea/vcs.xml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/AGENTS.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/CLAUDE.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/LICENCE.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/AGENTS.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/biquad.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/provenance.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/publication.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beq_common/types.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/__init__.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/accept.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/beqd.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/biquad.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/charts.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/cli.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/design.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/designer.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/diagnose.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/explain.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/extraction.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/filters.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/harness.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/identify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/material.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/pipeline.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/record.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/reference.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/rolloff.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge/verify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge-device-check.spec +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge.spec +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/AGENTS.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/__init__.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/analyse.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/audio.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/catalogue.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/cli.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/coefficients.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/engines.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/evidence.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/manifest.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/measurement.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/profiles.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/qualification.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/report.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/transactions.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqforge_device_check/verify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/AGENTS.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/__init__.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/cli.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/core.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/data/seed-manifest.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/beqoptimiser/data/seed.json.gz +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check-examples/simulation.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/device-check.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/README.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/example-1.png +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/assets/example-2.png +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/entry-results.json.gz +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser-report/statistics.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/docs/optimiser.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/evidence_validation.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/negative_corpus.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/packaging/designer/Dockerfile +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/packaging/designer/smoke.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/packaging/device-check/NOTICE.txt +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-catalogue-filter-optimisation.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-device-precision-validation.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/F2-package-consolidation.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/R2a-server-stage-cache.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/done-baseline-evidence.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/research-design-decisions.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/test-portability-and-runtime.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/texture-yardstick-sub-band.md +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/plans/texture-yardstick-sub-band.patch +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/pyproject.toml +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/__init__.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/conftest.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/fixtures/optimiser_seed_entry.json +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_cli.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_accept.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_cache_stores.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_channel_evidence.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_charts.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_corpus.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_design.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_designer.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_device_rate.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_diagnose.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_evidence.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_evidence_excess.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_exclusions.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_explain.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_extraction.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_filters.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_frozen_cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_guard.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_harness.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_headroom.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_hum.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_identify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_jacobian.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_parallel.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_pipeline.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_playback.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_playback_headroom.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_publication.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_record.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_record_fingerprint.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_reference.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_references.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_ripple_band.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_rolloff.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_strategy_config.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_timings.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_design_verify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_designer_image.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_analyse.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_catalogue.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_catalogue_predict.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_cli.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_engines.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_evidence.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_examples.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_manifest.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_measurement.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_qualification.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_transactions.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_device_check_verify.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_extract_layout.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser_cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_optimiser_report.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tests/test_package_boundaries.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/__init__.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/build_optimiser_seed.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/design_beq.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/a1_sweep.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/compare_records.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/compare_verdicts.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/inject_variants.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_compare.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_jacobian.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_run_pipeline.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p14_surrogate.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_greedy.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_seeded.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_compare_structure.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_greedy.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_seeded.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p18_structure.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/p20_smooth_drift.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/probe.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/refit_probe.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/request_timing.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/score_injected.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/experiments/t8_floor_rules.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/extract.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/ledger_template.html +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/negative_corpus.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/optimiser_catalogue_report.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/prepare_device_check_helper.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/render_ledger.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/replay.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/seed_optimiser_cache.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_device_check.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/smoke_test_install.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/summarise.py +0 -0
- {beqforge-0.2.3 → beqforge-0.2.4}/tools/validate_evidence.py +0 -0
- {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
|
+
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
|
|
274
|
-
worker
|
|
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
|
|
248
|
-
worker
|
|
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
|
|
|
@@ -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
|
|
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 == {
|
|
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.
|
|
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
|
|
271
|
-
"""
|
|
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 == {
|
|
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
|