beqforge 0.2.4__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/workflows/build-designer-image.yml +6 -3
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/workflows/build-executable.yml +12 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/AGENTS.md +2 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/PKG-INFO +8 -4
- {beqforge-0.2.4 → beqforge-0.2.5}/README.md +7 -3
- {beqforge-0.2.4 → beqforge-0.2.5}/TODO.md +1 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/__init__.py +1 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/AGENTS.md +1 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/diagnose.py +98 -8
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/AGENTS.md +5 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/__init__.py +15 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/cache.py +182 -4
- beqforge-0.2.5/beqoptimiser/parameters.py +500 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser.md +50 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/done-design-and-pipeline.md +41 -2
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/parameter-space-optimisation.md +52 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_diagnose.py +49 -1
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_designer_server_startup.py +15 -4
- beqforge-0.2.5/tests/test_optimiser_parameters.py +200 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/designer_server.py +12 -7
- beqforge-0.2.5/tools/experiments/parameter_benchmark.py +152 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.dockerignore +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.gitattributes +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/dependabot.yml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/workflows/build-device-check.yml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/workflows/dependabot-auto-merge.yaml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.github/workflows/tests.yml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.gitignore +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/.gitignore +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/beqforge.iml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/editor.xml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/jsonSchemas.xml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/misc.xml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/modules.xml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/.idea/vcs.xml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/CLAUDE.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/LICENCE.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/AGENTS.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/biquad.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/provenance.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/publication.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beq_common/types.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/__init__.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/accept.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/beqd.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/biquad.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/cache.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/charts.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/cli.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/design.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/designer.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/explain.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/extraction.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/filters.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/harness.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/identify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/material.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/pipeline.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/record.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/reference.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/rolloff.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge/verify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge-device-check.spec +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge.spec +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/AGENTS.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/__init__.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/analyse.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/audio.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/catalogue.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/cli.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/coefficients.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/engines.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/evidence.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/manifest.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/measurement.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/profiles.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/qualification.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/report.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/transactions.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqforge_device_check/verify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/cli.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/core.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/data/seed-manifest.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/beqoptimiser/data/seed.json.gz +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/device-check-examples/minidsp-2x4hd-usb-loopback.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/device-check-examples/simulation.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/device-check.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/README.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/assets/aggregate-48000.png +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/assets/aggregate-96000.png +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/assets/example-1.png +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/assets/example-2.png +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/entry-results.json.gz +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/docs/optimiser-report/statistics.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/evidence_validation.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/negative_corpus.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/packaging/designer/Dockerfile +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/packaging/designer/beqforge-designer.cmd +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/packaging/designer/smoke.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/packaging/device-check/NOTICE.txt +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/F2-catalogue-filter-optimisation.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/F2-device-precision-validation.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/F2-package-consolidation.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/R2a-server-stage-cache.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/done-baseline-evidence.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/research-design-decisions.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/test-portability-and-runtime.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/texture-yardstick-sub-band.md +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/plans/texture-yardstick-sub-band.patch +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/pyproject.toml +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/__init__.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/conftest.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/fixtures/optimiser_seed_entry.json +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_cli.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_accept.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_cache.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_cache_stores.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_channel_evidence.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_charts.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_corpus.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_design.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_designer.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_designer_server.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_device_rate.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_evidence.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_evidence_excess.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_exclusions.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_explain.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_extraction.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_filters.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_frozen_cache.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_guard.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_harness.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_headroom.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_hum.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_identify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_jacobian.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_parallel.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_pipeline.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_playback.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_playback_headroom.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_publication.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_record.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_record_fingerprint.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_reference.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_references.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_ripple_band.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_rolloff.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_strategy_config.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_timings.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_design_verify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_designer_image.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_analyse.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_catalogue.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_catalogue_predict.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_cli.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_engines.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_evidence.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_examples.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_manifest.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_measurement.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_qualification.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_transactions.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_device_check_verify.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_extract_layout.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_optimiser.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_optimiser_cache.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_optimiser_report.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tests/test_package_boundaries.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/__init__.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/build_optimiser_seed.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/design_beq.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/a1_sweep.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/compare_records.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/compare_verdicts.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/inject_variants.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p14_compare.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p14_jacobian.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p14_run_pipeline.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p14_surrogate.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_compare_greedy.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_compare_seeded.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_compare_structure.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_greedy.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_seeded.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p18_structure.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/p20_smooth_drift.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/probe.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/refit_probe.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/request_timing.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/score_injected.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/experiments/t8_floor_rules.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/extract.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/ledger_template.html +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/negative_corpus.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/optimiser_catalogue_report.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/prepare_device_check_helper.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/render_ledger.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/replay.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/seed_optimiser_cache.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/smoke_test_device_check.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/smoke_test_exe.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/smoke_test_install.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/summarise.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/tools/validate_evidence.py +0 -0
- {beqforge-0.2.4 → beqforge-0.2.5}/uv.lock +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
name: Designer image
|
|
2
2
|
|
|
3
3
|
# The designer as a container, beside a beqdesigner pipeline service (README, "Running the
|
|
4
|
-
# designer in a container"). Every change to what goes into it is built and smoke-tested
|
|
5
|
-
#
|
|
4
|
+
# designer in a container"). Every change to what goes into it is built and smoke-tested.
|
|
5
|
+
# A release publishes linux/amd64 and linux/arm64 to GHCR as ghcr.io/<owner>/beqforge-designer:
|
|
6
|
+
# build-executable.yml calls this for a vX.Y.Z tag once the release tests have passed, as it
|
|
7
|
+
# does for the executables and PyPI, so an image never ships ahead of its tests.
|
|
6
8
|
# A manual run with `publish` publishes the image of an existing release from a later commit,
|
|
7
9
|
# only if the designer's runtime code is identical to that release's tag (as when the image
|
|
8
10
|
# recipe arrived after the release).
|
|
9
11
|
on:
|
|
10
12
|
push:
|
|
11
13
|
branches: [main]
|
|
12
|
-
tags: ['v*']
|
|
13
14
|
paths:
|
|
14
15
|
- 'packaging/designer/**'
|
|
15
16
|
- '.dockerignore'
|
|
@@ -30,6 +31,8 @@ on:
|
|
|
30
31
|
description: Publish the image as beq_common.__version__ (its vX.Y.Z tag must exist, with identical designer code)
|
|
31
32
|
type: boolean
|
|
32
33
|
default: false
|
|
34
|
+
# the release: build-executable.yml, from a tag, after its test gate
|
|
35
|
+
workflow_call:
|
|
33
36
|
|
|
34
37
|
permissions:
|
|
35
38
|
contents: read
|
|
@@ -234,6 +234,18 @@ jobs:
|
|
|
234
234
|
env:
|
|
235
235
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
236
236
|
|
|
237
|
+
image:
|
|
238
|
+
name: Publish designer image
|
|
239
|
+
needs: [validated_tests, package]
|
|
240
|
+
# see create_release for why the needed results are checked explicitly
|
|
241
|
+
if: >-
|
|
242
|
+
!cancelled() && startsWith(github.ref, 'refs/tags/') &&
|
|
243
|
+
needs.validated_tests.result == 'success' && needs.package.result == 'success'
|
|
244
|
+
uses: ./.github/workflows/build-designer-image.yml
|
|
245
|
+
permissions:
|
|
246
|
+
contents: read
|
|
247
|
+
packages: write
|
|
248
|
+
|
|
237
249
|
publish:
|
|
238
250
|
name: Publish Python package to PyPI
|
|
239
251
|
needs: [validated_tests, package, build, create_release]
|
|
@@ -60,7 +60,8 @@ or fixed ports), with coverage (`--cov`, configured in `pyproject.toml`, `multip
|
|
|
60
60
|
children included) uploaded to Codecov by each test job using the `CODECOV_TOKEN` secret;
|
|
61
61
|
release runs inherit it. Feature branch pushes test on Linux only and a newer push cancels the branch's
|
|
62
62
|
previous run; `main`, tags and manual runs test Linux, macOS and Windows and are never cancelled. `.github/workflows/build-executable.yml` builds release executables and distributions;
|
|
63
|
-
tag pushes publish to PyPI after validation and successful executable builds
|
|
63
|
+
tag pushes publish to PyPI after validation and successful executable builds, and the designer
|
|
64
|
+
image to GHCR after validation. Tags must match
|
|
64
65
|
the shared version exactly (`vX.Y.Z`). Manual runs build artifacts without publishing.
|
|
65
66
|
PyPI uses the `pypi` environment and Trusted Publishing; setup details are in the README.
|
|
66
67
|
Device-check preview executable packaging has its own workflow, not a separate Python release;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: beqforge
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Audio-derived BEQ design, published filter optimisation and DSP validation
|
|
5
5
|
License-File: LICENCE.md
|
|
6
6
|
Requires-Python: <3.15,>=3.13
|
|
@@ -190,8 +190,9 @@ measures cold and warm requests.
|
|
|
190
190
|
**Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
|
|
191
191
|
its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
|
|
192
192
|
caller can send each server as many designs as it takes rather than be configured with that
|
|
193
|
-
number.
|
|
194
|
-
if set, stays each design's own).
|
|
193
|
+
number. Concurrent designs share the machine's logical processors, SMT threads included, each
|
|
194
|
+
at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
|
|
195
|
+
The share changes how long a fit takes, never its answer.
|
|
195
196
|
|
|
196
197
|
Every `serve-designer` configuration option can also be set through an environment variable.
|
|
197
198
|
Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
|
|
@@ -259,8 +260,11 @@ pure `design(request) -> response` adapter, independently testable without a soc
|
|
|
259
260
|
|
|
260
261
|
`ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
|
|
261
262
|
pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
|
|
262
|
-
published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml`
|
|
263
|
+
published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
|
|
263
264
|
each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
|
|
265
|
+
The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
|
|
266
|
+
as it does for the executables and PyPI; pushes to `main` and pull requests build and
|
|
267
|
+
smoke-test the image without publishing it.
|
|
264
268
|
A manual run of that workflow with `publish` set publishes the current version's image from a
|
|
265
269
|
later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
|
|
266
270
|
image was published this way, the recipe having arrived after the release).
|
|
@@ -164,8 +164,9 @@ measures cold and warm requests.
|
|
|
164
164
|
**Several designs at once.** `--workers N` (1-8, default 1) runs N designs at once, each in
|
|
165
165
|
its own worker process; further requests queue. `GET /health` reports it as `parallel`, so a
|
|
166
166
|
caller can send each server as many designs as it takes rather than be configured with that
|
|
167
|
-
number.
|
|
168
|
-
if set, stays each design's own).
|
|
167
|
+
number. Concurrent designs share the machine's logical processors, SMT threads included, each
|
|
168
|
+
at most the width one design uses alone (`BEQ_FIT_WORKERS`, if set, stays each design's own).
|
|
169
|
+
The share changes how long a fit takes, never its answer.
|
|
169
170
|
|
|
170
171
|
Every `serve-designer` configuration option can also be set through an environment variable.
|
|
171
172
|
Explicit CLI options take precedence; for repeatable options, CLI values replace the entire
|
|
@@ -233,8 +234,11 @@ pure `design(request) -> response` adapter, independently testable without a soc
|
|
|
233
234
|
|
|
234
235
|
`ghcr.io/3ll3d00d/beqforge-designer:<version>` runs `beqforge serve-designer` for a beqdesigner
|
|
235
236
|
pipeline service beside it (beqdesigner's `docker/compose.example.yaml` runs both). It is
|
|
236
|
-
published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml`
|
|
237
|
+
published for linux/amd64 and linux/arm64 by `.github/workflows/build-designer-image.yml` for
|
|
237
238
|
each `vX.Y.Z` tag, from the same commit as that release; `latest` follows the newest release.
|
|
239
|
+
The release workflow (`build-executable.yml`) calls it only once the release tests have passed,
|
|
240
|
+
as it does for the executables and PyPI; pushes to `main` and pull requests build and
|
|
241
|
+
smoke-test the image without publishing it.
|
|
238
242
|
A manual run of that workflow with `publish` set publishes the current version's image from a
|
|
239
243
|
later commit, and refuses unless the designer's code is identical to that version's tag (0.2.0's
|
|
240
244
|
image was published this way, the recipe having arrived after the release).
|
|
@@ -43,7 +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 |
|
|
46
|
+
| 27 | Parameter-space optimisation for several profiles | implemented; release pending | `beqoptimiser.optimise_parameters` built and benchmarked (18 October 2026): within margin on 21/25 and 10/25 of the worst 48/96 kHz float32 entries against coefficient search's 20/25 and 16/25; see [the outcome](plans/parameter-space-optimisation.md#outcome-18-october-2026). Done once released in a version beqdesigner can pin. Originally: beqdesigner's main window will let a filter designer optimise a filter for chosen device profiles before publishing, in freq/Q/gain terms (a BEQ is published and loaded as parameters, so no catalogue schema change), minimising the worst error across all chosen profiles at once. The search belongs in `beqoptimiser`, beside `optimise()`; beqdesigner builds the interface. See [the request](plans/parameter-space-optimisation.md). |
|
|
47
47
|
|
|
48
48
|
## Review evidence
|
|
49
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 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
|
|
46
|
+
| `tools/designer_server.py` | `beqforge serve-designer` — the HTTP transport (`designer-interface.md` §7.1) around `beqforge/designer.py`. Server-wide flags for device realisation/strategies/exclusions; everything per-title comes from the request. A `ThreadingHTTPServer`, so `/health` answers during a design, with every design run in a long-lived worker process started with "spawn" (`_DesignWorker`): the fitter forks worker processes (`beqforge.filters.PARALLEL_FITS`) when a fit escalates past one section count, and forking a multi-threaded process risks a deadlock, so the process that forks must stay single-threaded — the worker's log records come back over a `SimpleQueue`, not a `Queue`, for that reason. A worker designs one title at a time; `--workers N` (1-8) runs N of them, each request taking the next idle one, and `/health` reports N as `parallel` so a caller can size its sends from the designer. With N > 1 each worker's fit pool is the logical processors less one, divided by N and capped at `filters.FIT_WORKERS` (`fit_share`), unless `BEQ_FIT_WORKERS` is set: concurrent designs are what can use SMT threads, and pool size changes timing, never an answer. A worker that dies answers 500 and is replaced; the others carry on. SIGTERM shuts down like Ctrl-C, stopping the worker's process group, fit pool included. Every response names its build (`beqforge_revision`); `--record-dir DIR` also writes each request's run record, named by a digest of the request audio, so a review-queue entry can be replayed. `--cache-dir DIR` keeps the stage cache there (`cache.DirStore`, one file per entry, atomic): a repeat request for a title skips the analysis — 110 s cold, 50 s warm on a 2-hour 8-channel title. Each request logs a timing line (read, parse, decode, design, respond, queued); `tools/experiments/request_timing.py` reports them. `--shared-root DIR` accepts audio by reference (contract 1.2, beqdesigner's `design/designer-by-reference.md` §3): an array names a WAV relative to DIR plus the SHA-256 of its decoded column, and `beqforge/reference.py` resolves, decodes and checks it; one that cannot be honoured is a 422 naming the array, never a decline. `GET /health` reports `contract_version`, `shared_root` and `parallel`. |
|
|
47
47
|
| `tools/replay.py` | Redraw charts and export to beqdesigner from a record — no rerun, no extraction. Refuses on a stale record unless `--force`. |
|
|
48
48
|
| `tools/extract.py` | ffmpeg → 1 kHz per-channel `.npz`. Requires an explicit supported channel layout; preserves layout provenance. Needs no beqdesigner. |
|
|
49
49
|
| `tools/summarise.py` | Sanity-check an extraction before using it. |
|
|
@@ -8,6 +8,7 @@ unknown source spectrum.
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
import math
|
|
11
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
11
12
|
from dataclasses import dataclass, field, replace
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
|
|
@@ -633,7 +634,7 @@ def diagnose(
|
|
|
633
634
|
# and reported the band top as where tracking stopped (A Few Good Men: 150.5-200 Hz found by
|
|
634
635
|
# one, none by the other).
|
|
635
636
|
_, mix_plateau = plateau_reference(mix_spectrum, spectrum_freqs, params)
|
|
636
|
-
from beqforge.extraction import ExtractionParams
|
|
637
|
+
from beqforge.extraction import ExtractionParams
|
|
637
638
|
|
|
638
639
|
extraction_params = extraction_params or ExtractionParams()
|
|
639
640
|
extraction_params = replace(
|
|
@@ -645,12 +646,18 @@ def diagnose(
|
|
|
645
646
|
),
|
|
646
647
|
)
|
|
647
648
|
|
|
649
|
+
plateaus = {
|
|
650
|
+
name: plateau_reference(
|
|
651
|
+
spectra[name][band], freqs, params, one_sided_fallback=False
|
|
652
|
+
)
|
|
653
|
+
for name in material.channels
|
|
654
|
+
}
|
|
655
|
+
evidence = _channel_evidence(material, params, extraction_params, plateaus, freqs)
|
|
656
|
+
|
|
648
657
|
channels: dict[str, ChannelDiagnosis] = {}
|
|
649
658
|
for name in material.channels:
|
|
650
659
|
response = spectra[name][band]
|
|
651
|
-
level, plateau =
|
|
652
|
-
response, freqs, params, one_sided_fallback=False
|
|
653
|
-
)
|
|
660
|
+
level, plateau = plateaus[name]
|
|
654
661
|
response = np.where(
|
|
655
662
|
unexcluded(freqs, params.exclude_bands_hz), response, np.nan
|
|
656
663
|
)
|
|
@@ -666,10 +673,19 @@ def diagnose(
|
|
|
666
673
|
passband_share = (
|
|
667
674
|
float(share[in_share_band].mean()) if in_share_band.any() else 0.0
|
|
668
675
|
)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
676
|
+
if name in evidence:
|
|
677
|
+
(_, channel_spread, _, _, tracking), envelope, records = evidence[name]
|
|
678
|
+
for record in records:
|
|
679
|
+
logging.getLogger(record.name).handle(record)
|
|
680
|
+
else:
|
|
681
|
+
(_, channel_spread, _, _, tracking), envelope = _one_channel(
|
|
682
|
+
material.channels[name],
|
|
683
|
+
material.fs,
|
|
684
|
+
params,
|
|
685
|
+
plateau,
|
|
686
|
+
freqs,
|
|
687
|
+
extraction_params,
|
|
688
|
+
)
|
|
673
689
|
channels[name] = ChannelDiagnosis(
|
|
674
690
|
name=name,
|
|
675
691
|
response_db=response,
|
|
@@ -707,6 +723,80 @@ def diagnose(
|
|
|
707
723
|
)
|
|
708
724
|
|
|
709
725
|
|
|
726
|
+
def _one_channel(samples, fs, params, plateau, freqs, extraction_params):
|
|
727
|
+
"""A channel's temporal evidence and envelopes: most of a cold analysis's time, and
|
|
728
|
+
independent of every other channel's."""
|
|
729
|
+
from beqforge.extraction import extract
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
_temporal_evidence(samples, fs, params, plateau, freqs),
|
|
733
|
+
extract(samples, fs, extraction_params),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class _Collect(logging.Handler):
|
|
738
|
+
def __init__(self):
|
|
739
|
+
super().__init__()
|
|
740
|
+
self.records: list[logging.LogRecord] = []
|
|
741
|
+
|
|
742
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
743
|
+
# made picklable: the message rendered, any traceback as text
|
|
744
|
+
record.msg, record.args = record.getMessage(), None
|
|
745
|
+
if record.exc_info:
|
|
746
|
+
record.exc_text = logging.Formatter().formatException(record.exc_info)
|
|
747
|
+
record.exc_info = None
|
|
748
|
+
self.records.append(record)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _one_channel_logged(level, *args):
|
|
752
|
+
"""`_one_channel` in a pool process, its log records returned rather than emitted, so the
|
|
753
|
+
parent logs them in channel order on every platform: a spawned process has none of the
|
|
754
|
+
parent's handlers, and a forked one would print them out of order."""
|
|
755
|
+
root = logging.getLogger()
|
|
756
|
+
handlers, previous = root.handlers[:], root.level
|
|
757
|
+
collect = _Collect()
|
|
758
|
+
root.handlers[:] = [collect]
|
|
759
|
+
root.setLevel(level)
|
|
760
|
+
try:
|
|
761
|
+
return (*_one_channel(*args), collect.records)
|
|
762
|
+
finally:
|
|
763
|
+
root.handlers[:] = handlers
|
|
764
|
+
root.setLevel(previous)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _channel_evidence(material, params, extraction_params, plateaus, freqs):
|
|
768
|
+
"""`_one_channel` for every channel, with its log records, computed in parallel: a
|
|
769
|
+
process per channel up to the fitter's pool size, when the fitter may run in parallel
|
|
770
|
+
(`filters.PARALLEL_FITS`, `filters.FIT_WORKERS`). Empty otherwise, and `diagnose` works
|
|
771
|
+
through the channels in turn as it always did. Each channel's work is seeded and
|
|
772
|
+
self-contained, so the answer is the serial one; only the time differs."""
|
|
773
|
+
from beqforge import filters
|
|
774
|
+
|
|
775
|
+
names = list(material.channels)
|
|
776
|
+
tasks = [
|
|
777
|
+
(
|
|
778
|
+
material.channels[n],
|
|
779
|
+
material.fs,
|
|
780
|
+
params,
|
|
781
|
+
plateaus[n][1],
|
|
782
|
+
freqs,
|
|
783
|
+
extraction_params,
|
|
784
|
+
)
|
|
785
|
+
for n in names
|
|
786
|
+
]
|
|
787
|
+
workers = min(len(names), filters.FIT_WORKERS) if filters.PARALLEL_FITS else 1
|
|
788
|
+
if workers < 2:
|
|
789
|
+
return {}
|
|
790
|
+
level = logging.getLogger().getEffectiveLevel()
|
|
791
|
+
with ProcessPoolExecutor(max_workers=workers) as pool:
|
|
792
|
+
results = list(
|
|
793
|
+
pool.map(
|
|
794
|
+
_one_channel_logged, [level] * len(tasks), *zip(*tasks, strict=True)
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
return dict(zip(names, results, strict=True))
|
|
798
|
+
|
|
799
|
+
|
|
710
800
|
def _temporal_evidence(
|
|
711
801
|
subject: np.ndarray,
|
|
712
802
|
fs: float,
|
|
@@ -41,7 +41,8 @@ strings with additive feedback; internal SOS uses subtractive feedback.
|
|
|
41
41
|
| File | Responsibility |
|
|
42
42
|
| --- | --- |
|
|
43
43
|
| `core.py` | Precision models, deterministic bounded search and independent validation |
|
|
44
|
-
| `
|
|
44
|
+
| `parameters.py` | Parameter (freq/Q/gain) search for several (rate, precision) profiles at once, on the publication grid |
|
|
45
|
+
| `cache.py` | Public cached optimisation (coefficients and parameters), immutable numerical results and seed lookup |
|
|
45
46
|
| `cli.py` | Catalogue preparation, current-entry metadata, variant export and CLI |
|
|
46
47
|
| `__init__.py` | Public library exports, including the cached `optimise` wrapper |
|
|
47
48
|
| `data/seed.json.gz`, `data/seed-manifest.json` | Bundled numerical results and provenance |
|
|
@@ -53,6 +54,9 @@ dependency versions and execution architecture.
|
|
|
53
54
|
Custom precision implementations without a complete explicit `cache_identity()` bypass caching.
|
|
54
55
|
Corrupt entries are misses; unwritable storage must not prevent calculation or seed reuse.
|
|
55
56
|
Use atomic writes and retain checksums, request identity and publication-policy validation.
|
|
57
|
+
Parameter results are keyed by that identity plus `parameters.py`'s own hash and
|
|
58
|
+
`PARAMETER_VERSION`, kept out of `implementation_identity()` so they never invalidate the seed;
|
|
59
|
+
put parameter-search code in `parameters.py`, never `core.py`, for the same reason.
|
|
56
60
|
|
|
57
61
|
Bundled seeds are reusable only in their recorded numerical environment. Changes to numerical
|
|
58
62
|
code or settings invalidate them; do not relabel old results as compatible. Bump
|
|
@@ -8,6 +8,9 @@ from .cache import (
|
|
|
8
8
|
from .cache import (
|
|
9
9
|
optimise as optimise,
|
|
10
10
|
)
|
|
11
|
+
from .cache import (
|
|
12
|
+
optimise_parameters as optimise_parameters,
|
|
13
|
+
)
|
|
11
14
|
from .core import (
|
|
12
15
|
FixedPoint as FixedPoint,
|
|
13
16
|
)
|
|
@@ -32,3 +35,15 @@ from .core import (
|
|
|
32
35
|
from .core import (
|
|
33
36
|
stable as stable,
|
|
34
37
|
)
|
|
38
|
+
from .parameters import (
|
|
39
|
+
Fixed as Fixed,
|
|
40
|
+
)
|
|
41
|
+
from .parameters import (
|
|
42
|
+
ParameterResult as ParameterResult,
|
|
43
|
+
)
|
|
44
|
+
from .parameters import (
|
|
45
|
+
ParameterSettings as ParameterSettings,
|
|
46
|
+
)
|
|
47
|
+
from .parameters import (
|
|
48
|
+
ProfileError as ProfileError,
|
|
49
|
+
)
|
|
@@ -17,16 +17,25 @@ from pathlib import Path
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
import scipy
|
|
19
19
|
|
|
20
|
-
from . import core
|
|
20
|
+
from . import core, parameters
|
|
21
21
|
from .core import (
|
|
22
22
|
NUMERICAL_VERSION,
|
|
23
|
+
PUBLISHED_OUTCOMES,
|
|
23
24
|
FixedPoint,
|
|
24
25
|
Float32,
|
|
25
26
|
Precision,
|
|
26
27
|
Result,
|
|
28
|
+
Section,
|
|
27
29
|
Settings,
|
|
28
30
|
stable,
|
|
29
31
|
)
|
|
32
|
+
from .parameters import (
|
|
33
|
+
PARAMETER_VERSION,
|
|
34
|
+
Fixed,
|
|
35
|
+
ParameterResult,
|
|
36
|
+
ParameterSettings,
|
|
37
|
+
ProfileError,
|
|
38
|
+
)
|
|
30
39
|
|
|
31
40
|
LOGGER = logging.getLogger(__name__)
|
|
32
41
|
CACHE_SCHEMA = 1
|
|
@@ -263,10 +272,13 @@ class ResultCache:
|
|
|
263
272
|
self.directory = Path(directory)
|
|
264
273
|
self.use_seed = use_seed
|
|
265
274
|
|
|
266
|
-
def get(self, request
|
|
275
|
+
def get(self, request, restore=None):
|
|
276
|
+
"""The cached result for a request, or None; `restore` checks an envelope against
|
|
277
|
+
its request (`_restore` for coefficient results, the default)."""
|
|
278
|
+
restore = restore or _restore
|
|
267
279
|
key = request[0]
|
|
268
280
|
try:
|
|
269
|
-
result =
|
|
281
|
+
result = restore(
|
|
270
282
|
json.loads((self.directory / f"{key}.json").read_text()), request
|
|
271
283
|
)
|
|
272
284
|
LOGGER.debug(f"Optimiser disk cache hit: {key}")
|
|
@@ -277,7 +289,7 @@ class ResultCache:
|
|
|
277
289
|
try:
|
|
278
290
|
envelope = _bundled_entries().get(key)
|
|
279
291
|
if envelope is not None:
|
|
280
|
-
result =
|
|
292
|
+
result = restore(envelope, request)
|
|
281
293
|
LOGGER.debug(f"Optimiser bundled cache hit: {key}")
|
|
282
294
|
return result
|
|
283
295
|
except (ValueError, KeyError, TypeError, OverflowError):
|
|
@@ -373,3 +385,169 @@ def optimise(
|
|
|
373
385
|
_restore(envelope, request)
|
|
374
386
|
store._write(envelope)
|
|
375
387
|
return result
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
PARAMETER_OUTCOMES = (
|
|
391
|
+
"within_margin",
|
|
392
|
+
"replacement",
|
|
393
|
+
"improvement",
|
|
394
|
+
"no_replacement",
|
|
395
|
+
"unresolved",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _section_identity(section) -> dict:
|
|
400
|
+
return {"kind": type(section).__name__, **asdict(section)}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _parameter_request(sections, *, profiles, settings=None):
|
|
404
|
+
"""(key, sections, profiles, settings), or None when a request cannot be cached: an
|
|
405
|
+
invalid one (left for `optimise_parameters` to refuse) or a precision without a complete
|
|
406
|
+
identity. Keyed by the coefficient optimiser's implementation identity plus this module's
|
|
407
|
+
own source and `PARAMETER_VERSION`, so the coefficient seed is unaffected."""
|
|
408
|
+
cfg = settings or ParameterSettings()
|
|
409
|
+
cascade = tuple(sections)
|
|
410
|
+
chosen = list(profiles)
|
|
411
|
+
if (
|
|
412
|
+
not cascade
|
|
413
|
+
or not chosen
|
|
414
|
+
or not all(isinstance(s, (Section, Fixed)) for s in cascade)
|
|
415
|
+
):
|
|
416
|
+
return None
|
|
417
|
+
identities = []
|
|
418
|
+
for rate, precision in chosen:
|
|
419
|
+
identity = _precision_identity(precision)
|
|
420
|
+
if identity is None or rate not in parameters.RATES:
|
|
421
|
+
return None
|
|
422
|
+
identities.append({"rate": rate, "name": precision.name, "identity": identity})
|
|
423
|
+
try:
|
|
424
|
+
implementation = implementation_identity() | {
|
|
425
|
+
"parameters": hashlib.sha256(
|
|
426
|
+
files("beqoptimiser").joinpath("parameters.py").read_bytes()
|
|
427
|
+
).hexdigest(),
|
|
428
|
+
"parameter_version": PARAMETER_VERSION,
|
|
429
|
+
}
|
|
430
|
+
payload = {
|
|
431
|
+
"sections": [_section_identity(s) for s in cascade],
|
|
432
|
+
"profiles": identities,
|
|
433
|
+
"settings": asdict(cfg),
|
|
434
|
+
}
|
|
435
|
+
key = _digest({"implementation": implementation, "request": payload})
|
|
436
|
+
except (OSError, TypeError, ValueError):
|
|
437
|
+
return None
|
|
438
|
+
return key, cascade, chosen, cfg
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _parameter_envelope(key: str, result: ParameterResult) -> dict:
|
|
442
|
+
body = _encode(
|
|
443
|
+
asdict(result)
|
|
444
|
+
| {
|
|
445
|
+
"sections": None
|
|
446
|
+
if result.sections is None
|
|
447
|
+
else [_section_identity(s) for s in result.sections]
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
return {
|
|
451
|
+
"schema": CACHE_SCHEMA,
|
|
452
|
+
"kind": "parameters",
|
|
453
|
+
"key": key,
|
|
454
|
+
"result": body,
|
|
455
|
+
"checksum": _digest(body),
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _decode_section(value: dict):
|
|
460
|
+
kind = value.pop("kind")
|
|
461
|
+
if kind == "Section":
|
|
462
|
+
return Section(**value)
|
|
463
|
+
if kind == "Fixed":
|
|
464
|
+
return Fixed(tuple((int(r), tuple(row)) for r, row in value["coefficients"]))
|
|
465
|
+
raise ValueError(f"unknown section kind {kind!r}")
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _restore_parameters(envelope: dict, request) -> ParameterResult:
|
|
469
|
+
key, cascade, chosen, cfg = request
|
|
470
|
+
if (
|
|
471
|
+
envelope["schema"] != CACHE_SCHEMA
|
|
472
|
+
or envelope.get("kind") != "parameters"
|
|
473
|
+
or envelope["key"] != key
|
|
474
|
+
or envelope["checksum"] != _digest(envelope["result"])
|
|
475
|
+
):
|
|
476
|
+
raise ValueError("invalid cache envelope")
|
|
477
|
+
body = _decode(envelope["result"])
|
|
478
|
+
body["band_hz"] = tuple(body["band_hz"])
|
|
479
|
+
body["profiles"] = tuple(ProfileError(**p) for p in body["profiles"])
|
|
480
|
+
if body["sections"] is not None:
|
|
481
|
+
body["sections"] = tuple(_decode_section(dict(s)) for s in body["sections"])
|
|
482
|
+
result = ParameterResult(**body)
|
|
483
|
+
expected = [(rate, p.name) for rate, p in chosen]
|
|
484
|
+
if (
|
|
485
|
+
result.version != PARAMETER_VERSION
|
|
486
|
+
or result.outcome not in PARAMETER_OUTCOMES
|
|
487
|
+
or result.margin_db != cfg.margin_db
|
|
488
|
+
or result.band_hz != cfg.band_hz
|
|
489
|
+
or [(p.rate, p.precision) for p in result.profiles]
|
|
490
|
+
!= list(dict.fromkeys(expected))
|
|
491
|
+
or np.isnan(result.original_error_db)
|
|
492
|
+
or result.original_error_db < 0
|
|
493
|
+
or result.evaluations < 0
|
|
494
|
+
):
|
|
495
|
+
raise ValueError("cache result does not match request")
|
|
496
|
+
if result.outcome in PUBLISHED_OUTCOMES:
|
|
497
|
+
if (
|
|
498
|
+
result.sections is None
|
|
499
|
+
or len(result.sections) != len(cascade)
|
|
500
|
+
or result.candidate_error_db is None
|
|
501
|
+
or not np.isfinite(result.candidate_error_db)
|
|
502
|
+
or result.original_error_db <= cfg.margin_db
|
|
503
|
+
):
|
|
504
|
+
raise ValueError("cached replacement violates publication policy")
|
|
505
|
+
for old, new in zip(cascade, result.sections, strict=True):
|
|
506
|
+
if type(old) is not type(new) or (isinstance(old, Fixed) and old != new):
|
|
507
|
+
raise ValueError("cached replacement changes the cascade's structure")
|
|
508
|
+
if isinstance(new, Section) and (
|
|
509
|
+
new.type != old.type
|
|
510
|
+
or round(new.freq, 2) != new.freq
|
|
511
|
+
or round(new.q, 4) != new.q
|
|
512
|
+
or round(new.gain, 2) != new.gain
|
|
513
|
+
):
|
|
514
|
+
raise ValueError("cached replacement is off the publication grid")
|
|
515
|
+
if result.outcome == "replacement" and (
|
|
516
|
+
result.candidate_error_db > cfg.margin_db
|
|
517
|
+
):
|
|
518
|
+
raise ValueError("cached replacement violates publication policy")
|
|
519
|
+
if (
|
|
520
|
+
result.outcome == "improvement"
|
|
521
|
+
and result.candidate_error_db
|
|
522
|
+
>= result.original_error_db - cfg.numerical_tolerance_db
|
|
523
|
+
):
|
|
524
|
+
raise ValueError("cached improvement violates publication policy")
|
|
525
|
+
elif result.sections is not None:
|
|
526
|
+
raise ValueError("unexpected cached replacement")
|
|
527
|
+
return result
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def optimise_parameters(
|
|
531
|
+
sections,
|
|
532
|
+
*,
|
|
533
|
+
profiles,
|
|
534
|
+
settings: ParameterSettings | None = None,
|
|
535
|
+
cache: ResultCache | bool | None = None,
|
|
536
|
+
) -> ParameterResult:
|
|
537
|
+
"""`parameters.optimise_parameters` with automatic cache reuse; use cache=False for a
|
|
538
|
+
fresh calculation."""
|
|
539
|
+
kwargs = {"profiles": profiles, "settings": settings}
|
|
540
|
+
if cache is False or os.environ.get("BEQOPTIMISER_CACHE") == "0":
|
|
541
|
+
return parameters.optimise_parameters(sections, **kwargs)
|
|
542
|
+
request = _parameter_request(sections, **kwargs)
|
|
543
|
+
if request is None:
|
|
544
|
+
return parameters.optimise_parameters(sections, **kwargs)
|
|
545
|
+
store = cache if isinstance(cache, ResultCache) else ResultCache()
|
|
546
|
+
result = store.get(request, _restore_parameters)
|
|
547
|
+
if result is not None:
|
|
548
|
+
return result
|
|
549
|
+
result = parameters.optimise_parameters(sections, **kwargs)
|
|
550
|
+
envelope = _parameter_envelope(request[0], result)
|
|
551
|
+
_restore_parameters(envelope, request)
|
|
552
|
+
store._write(envelope)
|
|
553
|
+
return result
|