loopmath 0.2.0__tar.gz → 0.2.1__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.
- {loopmath-0.2.0 → loopmath-0.2.1}/PKG-INFO +1 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/pyproject.toml +1 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/__init__.py +1 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/design.py +30 -5
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/fit.py +24 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/lookahead.py +3 -3
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/state.py +24 -8
- loopmath-0.2.1/src/loopmath/builder/__init__.py +1 -0
- loopmath-0.2.1/src/loopmath/builder/cli.py +34 -0
- loopmath-0.2.1/src/loopmath/builder/context.py +240 -0
- loopmath-0.2.1/src/loopmath/builder/predict.py +180 -0
- loopmath-0.2.1/src/loopmath/builder/server.py +167 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_registry.py +7 -3
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/commands.py +47 -11
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/curve.py +86 -5
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/engine.py +178 -28
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/message.py +101 -14
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/search.py +7 -6
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath/SKILL.md +1 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-import-runs/SKILL.md +5 -4
- loopmath-0.2.1/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +119 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-update-fit/SKILL.md +6 -3
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/reference.md +20 -4
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/commands.py +38 -3
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/config.py +19 -3
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/base.css +7 -0
- loopmath-0.2.1/src/loopmath/views/assets/builder.html +109 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/common.js +85 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/estimates.js +11 -6
- loopmath-0.2.1/src/loopmath/views/assets/plans.css +39 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/plans.js +233 -52
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/results.js +44 -16
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/runs.js +1 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/viz.css +14 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/viz.js +46 -21
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/plans.py +3 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/posterior.py +219 -20
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/PKG-INFO +1 -1
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/SOURCES.txt +7 -0
- loopmath-0.2.1/tests/test_cli_help_lists.py +40 -0
- loopmath-0.2.0/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +0 -95
- loopmath-0.2.0/src/loopmath/views/assets/plans.css +0 -9
- {loopmath-0.2.0 → loopmath-0.2.1}/LICENSE +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/README.md +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/setup.cfg +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp-v0.2.schema.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp-v0.schema.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/spec/ocp_conformance.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/__main__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapter_host.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/atrium.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/base.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/bb.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/bb_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_facts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_links.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_parse.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/omp_types.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/opencode.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/opencode_store.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/orca.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_graph.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_parse.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_types.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/otel_genai_usage.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/paseo.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/pi.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/pi_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/adapters/registry.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/block.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/check_pymc.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/compose.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/features.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/forest.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/gaussian.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/outcome.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/belief/priors.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_graph.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/cli_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/arms.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/cli_e0.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/estimate.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/estimate_rework.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/figures.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/io.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/e0/verb.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/explain.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_assembly.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_bayes.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_masks.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_predict.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_pricing.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/fit_transfer.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/gaps.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/gitwalk.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/grade.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/grade_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_git.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_join.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifact_kinds.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/artifacts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashparse.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashread_ops.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashwrite_ops.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/bashwrites.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/cache.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio_parse.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/codexio_scan.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_build.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_core.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_edges.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/dataset_nodes.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/extract.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_common.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_costcurve.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_css.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_data.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_force.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_render.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/html_swimlanes.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_cli.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_common.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_parse.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_pricing.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_build.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_dispatch.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_evidence.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_node.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_records.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_result.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_session.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_tools.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_validation.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_prompt_workflow.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_report.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labeler_score.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/labels.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/launch.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/launch_origin.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp_emit.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/ocp_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/render.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile_export.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/runfile_finish.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/scan.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/schema.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/graph/token_completeness.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/base.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/claude_code.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/claude_code_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/codex.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/codex_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_analysis.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_artifact.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_common.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_convert.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ingest/ocp_projection.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/artifacts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/clip.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/costs.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/match.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/settle.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/logmatch/tariff.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/canonical.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/conformance.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/contractv3.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/emit.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/migrate.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.2.schema.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.3.schema.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/schema/ocp-v0.schema.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/ocp/version.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/history.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/label.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/onboard/usual.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/output.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/pool.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/price.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/price_validation.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/prices.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/benchmarks.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/benchmarks.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/build.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/README.md +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/e0.jsonl.gz +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/manifest.json +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/rq1.jsonl.gz +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/bundle/sweep.jsonl.gz +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/e0.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/history.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/ocpdoc.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/reduce.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/registry.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/rq1.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/show.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/sweep.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/priors/validate.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/receipts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/backlog.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/gain.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/receipts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/stats.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/recommend/storeread.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/html.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_exclusions.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_format.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/report/terminal_read.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/research_defaults.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/research_paths.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring_comparisons.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/scoring_predictive.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/export.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/share/import_.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/doctor.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/install.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-onboard/SKILL.md +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/skill/skills/loopmath-record-run/SKILL.md +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/budget.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/finish.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/fitjob.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/home.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/ids.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/lock.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/record.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/report.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/runs.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/store/status.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_bootstrap.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_estimation.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_frame.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_presentation.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/surface_support.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/taskmodel.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/types.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/README.md +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/estimates.css +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/graph.css +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/graph.js +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/assets/results.css +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/common.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/views/runs.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/__init__.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/candidates.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/best_of_n.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/implement_review.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/plan_implement.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/plan_implement_review.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/solo.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/catalog/swarm.toml +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/commands.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/diff.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/format.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/graphview.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/ids.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/infer.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/models.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/ocp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath/workflows/shapes.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/dependency_links.txt +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/entry_points.txt +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/requires.txt +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/src/loopmath.egg-info/top_level.txt +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_atrium.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_bb.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_omp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_opencode.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_opencode_output_tokens.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_orca.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_otel_genai.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_paseo.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapter_services.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapters.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_adapters_pi.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_arms.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_graph.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_labeler.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_main_module.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_plan_hidden.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_prices.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_cli_startup.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_color_proof.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_duplicate_guards.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_estimate.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_explain.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_fit.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_gaps.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_gitwalk.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_grade.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_artifact_recording.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_artifacts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_bashwrites.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_cache.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_codexio.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_dataset.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_extract.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_glue.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_html.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_labeler.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_labeler_evidence_matrix.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_launch.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_ocp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_render.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_runfile.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_graph_schema.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_home_guard.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_html.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_claude_code.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_codex.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ingest_ocp.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_io.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_not_run_summary.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_conformance.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_golden.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_open_enums.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_ocp_versioning.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_output_html_path.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_packaging_metadata.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_packaging_smoke.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_pool.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_preview_script.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_price.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_receipts.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_release_paths.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_runfile_privacy_n1b.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_scoring.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_split_rebindings.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_surface.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_taskmodel_features.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_terminal.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_types_control.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_types_piece_prediction.py +0 -0
- {loopmath-0.2.0 → loopmath-0.2.1}/tests/test_v01_scaffold.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "loopmath"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Plan agent workflows, record how they went, and learn from your own Claude Code and Codex logs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -35,6 +35,11 @@ HEURISTIC_COST_WEIGHT = 0.7 # spec 04 section 2 and D27: allocated (heuristic)
|
|
|
35
35
|
USER_SOURCES = ("live", "backlog", "history", "user", "orchestrator", "onboard", "habit", "")
|
|
36
36
|
HORIZON_NODES = ("horizon:timebox", "horizon:log2h") # fixed terms of a timeboxed run (spec 04 section 1)
|
|
37
37
|
RQ1_EXT = "dev.loopmath.rq1" # loopmath-exp's ext key; its horizon_s is the fallback for older RQ1 runs
|
|
38
|
+
# 0.2.1 (F5, spec 04 section 1): in the cost and tokens rows of a timeboxed task the terms a timebox caps (effort
|
|
39
|
+
# and shape) enter at this value, not 1. Set from the RQ1 2 h runs: open-ended effort terms gave max over low x2.64
|
|
40
|
+
# (solo luna) and x2.23 (solo sol) against x1.34 and x1.27 observed; ln 1.34 / ln 2.64 = ln 1.27 / ln 2.23 = 0.30.
|
|
41
|
+
TIMEBOX_EFFORT_COST = 0.3
|
|
42
|
+
TIMEBOX_LEVELS = ("effort:", "family_effort:", "topology:", "position:")
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
def other_design(meta: dict) -> str | None:
|
|
@@ -304,17 +309,36 @@ def structure(config: Configuration) -> Structure:
|
|
|
304
309
|
|
|
305
310
|
# ---------------------------------------------------------------- rows
|
|
306
311
|
|
|
312
|
+
def effort_weight(task: Task, timebox_effort: float | None = None) -> float:
|
|
313
|
+
"""The value of the timebox-capped terms (TIMEBOX_LEVELS) in a cost or tokens row of `task`: `timebox_effort`
|
|
314
|
+
(default TIMEBOX_EFFORT_COST) when the task has a horizon, else 1 (spec 04 section 1, effort and shape under
|
|
315
|
+
a timebox)."""
|
|
316
|
+
if task_horizon(task) is None:
|
|
317
|
+
return 1.0
|
|
318
|
+
return float(TIMEBOX_EFFORT_COST if timebox_effort is None else timebox_effort)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@lru_cache(maxsize=65536)
|
|
322
|
+
def _effort_scaled(terms: tuple[Term, ...], weight: float) -> tuple[Term, ...]:
|
|
323
|
+
return tuple((n, p, v * weight) if n.startswith(TIMEBOX_LEVELS) else (n, p, v) for n, p, v in terms)
|
|
324
|
+
|
|
325
|
+
|
|
307
326
|
def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None, *,
|
|
308
|
-
source: str | None = None) -> tuple[Term, ...]:
|
|
327
|
+
source: str | None = None, effort: float = 1.0) -> tuple[Term, ...]:
|
|
309
328
|
"""Cost and tokens rows without the task part: setting, role, topology, position, round, control,
|
|
310
329
|
and with a `source` the position x source node `psrc:<shape>#<pos>|<source>` and the family x source
|
|
311
|
-
node `fsrc:<family>|<source>` (spec 04 section 1).
|
|
330
|
+
node `fsrc:<family>|<source>` (spec 04 section 1). `effort` is the value of the terms a timebox caps, the
|
|
331
|
+
effort and shape (topology, position) terms (`effort_weight`: below 1 under a timebox); the role, psrc and
|
|
332
|
+
fsrc terms stay at 1."""
|
|
312
333
|
s = setting or st.settings[piece]
|
|
313
334
|
h, m, e = _s(s)
|
|
314
335
|
shape = st.shape or st.workflow_id
|
|
315
336
|
position = f"{shape}#{st.pieces.index(piece)}"
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
st_terms = setting_terms(h, m, e)
|
|
338
|
+
shape_terms = ((f"topology:{shape}", None, 1.0), (f"position:{position}", f"topology:{shape}", 1.0))
|
|
339
|
+
if effort != 1.0:
|
|
340
|
+
st_terms, shape_terms = _effort_scaled(st_terms, float(effort)), _effort_scaled(shape_terms, float(effort))
|
|
341
|
+
terms = st_terms + role_terms(st.roles[piece], m) + shape_terms
|
|
318
342
|
if source is not None:
|
|
319
343
|
fam = model_path(m)[1]
|
|
320
344
|
terms += ((f"psrc:{position}|{source}", f"position:{position}", 1.0),
|
|
@@ -345,7 +369,8 @@ def run_rest(st: Structure) -> tuple[Term, ...]:
|
|
|
345
369
|
|
|
346
370
|
def cost_row(task: Task, source: str, st: Structure, piece: str, k: int,
|
|
347
371
|
setting: Setting | None = None, features: FeatureSet | None = None) -> list[Term]:
|
|
348
|
-
return task_terms(task, source, features) + list(cost_rest(st, piece, k, setting, source=source
|
|
372
|
+
return task_terms(task, source, features) + list(cost_rest(st, piece, k, setting, source=source,
|
|
373
|
+
effort=effort_weight(task)))
|
|
349
374
|
|
|
350
375
|
|
|
351
376
|
def gate_row(task: Task, source: str, st: Structure, gate: GateInfo, k: int,
|
|
@@ -29,6 +29,7 @@ import numpy as np
|
|
|
29
29
|
from scipy import sparse
|
|
30
30
|
|
|
31
31
|
from ..taskmodel import FeatureConfigError, FeatureSet
|
|
32
|
+
from . import design as design_rows
|
|
32
33
|
from . import priors as prior_data
|
|
33
34
|
from .design import (DESIGN_VERSION, Term, Unusable, cost_row, gate_row, parse_run, run_row, structure,
|
|
34
35
|
task_features, task_terms)
|
|
@@ -362,9 +363,27 @@ def _support(hr: HeadRows, X: sparse.csr_matrix, node_ids: list[str]) -> dict[st
|
|
|
362
363
|
|
|
363
364
|
|
|
364
365
|
def seed_for(fit_id: str, head: str) -> int:
|
|
366
|
+
"""A head's seed from the fit's `seed_key` (its fit id before 0.2.1)."""
|
|
365
367
|
return int.from_bytes(hashlib.sha256(f"{fit_id}/{head}".encode()).digest()[:4], "little")
|
|
366
368
|
|
|
367
369
|
|
|
370
|
+
def input_key(heads_rows: dict[str, HeadRows], specs: list, settings: dict) -> str:
|
|
371
|
+
"""The fit's `seed_key` (spec 04 section 3): the sha256 of the design version, the settings that change the
|
|
372
|
+
posterior, the prior factors and every head's rows (terms, response, weight, run, source), each head's rows
|
|
373
|
+
sorted, so the same data and settings seed the same draws whatever the store's reading order."""
|
|
374
|
+
h = hashlib.sha256()
|
|
375
|
+
h.update(repr((DESIGN_VERSION, design_rows.TIMEBOX_EFFORT_COST, design_rows.TIMEBOX_LEVELS,
|
|
376
|
+
sorted(settings.items()))).encode())
|
|
377
|
+
for spec in specs:
|
|
378
|
+
h.update(repr((spec.head, spec.terms, spec.mean, spec.var)).encode())
|
|
379
|
+
for name in sorted(heads_rows):
|
|
380
|
+
hr = heads_rows[name]
|
|
381
|
+
h.update(f"\n{name}:{len(hr.rows)}".encode())
|
|
382
|
+
for line in sorted(repr((t, y, w, r, src)) for t, y, w, r, src in zip(hr.rows, hr.y, hr.w, hr.runs, hr.sources)):
|
|
383
|
+
h.update(line.encode())
|
|
384
|
+
return h.hexdigest()
|
|
385
|
+
|
|
386
|
+
|
|
368
387
|
def fit_head(hr: HeadRows, forest: Forest, factor_specs: list, *, eb: bool = True) -> dict:
|
|
369
388
|
"""Fit one head; returns arrays and metadata for the state files."""
|
|
370
389
|
extra = [(n, p) for spec in factor_specs for n, p, _ in spec.terms]
|
|
@@ -502,6 +521,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
|
|
|
502
521
|
specs = []
|
|
503
522
|
if not no_prior and "benchmark" not in without:
|
|
504
523
|
specs = prior_data.benchmark_factors(benchmarks, weight=weight)
|
|
524
|
+
seed_key = input_key(heads_rows, specs, {"no_prior": no_prior, "without": sorted(without), "eb": eb,
|
|
525
|
+
"benchmark_prior_weight": weight})
|
|
505
526
|
forest = Forest()
|
|
506
527
|
fitted = {}
|
|
507
528
|
for name, hr in heads_rows.items():
|
|
@@ -546,6 +567,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
|
|
|
546
567
|
_atomic_json(partial / "design.json", design)
|
|
547
568
|
meta = {
|
|
548
569
|
"fit": fit_id, "created_at": now.isoformat(timespec="seconds"), "code_version": _code_version(),
|
|
570
|
+
"seed_key": seed_key, "timebox_effort": design_rows.TIMEBOX_EFFORT_COST,
|
|
571
|
+
"timebox_terms": list(design_rows.TIMEBOX_LEVELS),
|
|
549
572
|
"options": {"no_prior": no_prior, "without": list(without), "full": full, "eb": eb,
|
|
550
573
|
"benchmark_prior_weight": weight},
|
|
551
574
|
"runs_by_source": runs_by_source,
|
|
@@ -562,7 +585,7 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
|
|
|
562
585
|
"center": h["center"], "scale": h["scale"], "baseline": h["baseline"],
|
|
563
586
|
"iterations": h["fit"].iterations, "optimizer": h["fit"].info,
|
|
564
587
|
"log_evidence": _finite(h["fit"].log_evidence), "factors": h["factors"],
|
|
565
|
-
"seed": seed_for(
|
|
588
|
+
"seed": seed_for(seed_key, name)}
|
|
566
589
|
for name, h in fitted.items()},
|
|
567
590
|
"scores": score_info,
|
|
568
591
|
"draws": N_DRAWS,
|
|
@@ -165,7 +165,7 @@ def _prepare(state: FitState, task: Task, configs: list[Configuration], rule: Ac
|
|
|
165
165
|
tt = state.task_terms(task)
|
|
166
166
|
cost_rows, coef, owner_r, owner_c, run_rows, plans = [], [], [], [], [], []
|
|
167
167
|
for ci, (cfg, (_, d)) in enumerate(zip(configs, out)):
|
|
168
|
-
plan = state._plan(cfg)
|
|
168
|
+
plan = state._plan(cfg, state.effort_for(task))
|
|
169
169
|
plans.append(plan)
|
|
170
170
|
st = plan["st"]
|
|
171
171
|
reach = d["reach"] # E[r(k)] with the unseen task effect on the gates integrated out
|
|
@@ -193,7 +193,7 @@ def _prepare(state: FitState, task: Task, configs: list[Configuration], rule: Ac
|
|
|
193
193
|
def _explore_cost_obs(state: FitState, task: Task, config: Configuration) -> tuple[list[tuple], int]:
|
|
194
194
|
"""Composite observation: the mean of the round-1 cost rows of the configuration's pieces,
|
|
195
195
|
and the number of pieces (the observation's noise variance is sigma^2 / n)."""
|
|
196
|
-
plan = state._plan(config)
|
|
196
|
+
plan = state._plan(config, state.effort_for(task))
|
|
197
197
|
st = plan["st"]
|
|
198
198
|
tt = state.task_terms(task)
|
|
199
199
|
acc: dict[str, list] = {}
|
|
@@ -385,7 +385,7 @@ def _condition_head(head: HeadState, terms: Sequence[tuple], tau2: float) -> Hea
|
|
|
385
385
|
draws = mean[:, None] + U @ Z
|
|
386
386
|
meta = dict(head.meta) or {"engine": head.engine, "kind": head.kind, "sigma": head.sigma}
|
|
387
387
|
out = HeadState(head.name, meta, head.nodes + new_nodes, None, head.fit_id, mean=mean, U=U, draws=draws,
|
|
388
|
-
unit=Z)
|
|
388
|
+
unit=Z, seed_key=head.seed_key)
|
|
389
389
|
out.fitted = head.fitted
|
|
390
390
|
out.phi = dict(head.phi)
|
|
391
391
|
return out
|
|
@@ -152,7 +152,7 @@ class HeadState:
|
|
|
152
152
|
`loader(part)` returns a stored array or None when the fit did not store it."""
|
|
153
153
|
|
|
154
154
|
def __init__(self, name: str, meta: dict, nodes: Sequence[str], loader: Callable[[str], np.ndarray] | None,
|
|
155
|
-
fit_id: str, *, mean=None, U=None, draws=None, unit=None):
|
|
155
|
+
fit_id: str, *, mean=None, U=None, draws=None, unit=None, seed_key: str | None = None):
|
|
156
156
|
base = BASE_HEADS.get(name, ("gaussian", "score"))
|
|
157
157
|
self.name = name
|
|
158
158
|
self.engine = meta.get("engine") or base[0]
|
|
@@ -165,6 +165,7 @@ class HeadState:
|
|
|
165
165
|
self.seed = int(meta.get("seed", 0))
|
|
166
166
|
self.meta = meta
|
|
167
167
|
self.fit_id = fit_id
|
|
168
|
+
self.seed_key = seed_key or fit_id # spec 04 section 3: the fit's input hash; the fit id before 0.2.1
|
|
168
169
|
self.nodes = list(nodes)
|
|
169
170
|
self.index = {n: j for j, n in enumerate(self.nodes)}
|
|
170
171
|
self._loader = loader
|
|
@@ -252,7 +253,7 @@ class HeadState:
|
|
|
252
253
|
"""Standard normal draws of an unseen node, fixed by the fit, the head and the node id."""
|
|
253
254
|
z = self._virtual.get(node)
|
|
254
255
|
if z is None:
|
|
255
|
-
z = np.random.default_rng(_seed(self.
|
|
256
|
+
z = np.random.default_rng(_seed(self.seed_key, self.name, node)).standard_normal(N_DRAWS)
|
|
256
257
|
self._virtual[node] = z
|
|
257
258
|
return z
|
|
258
259
|
|
|
@@ -332,16 +333,21 @@ class FitState:
|
|
|
332
333
|
self._design = design
|
|
333
334
|
self._npz = None
|
|
334
335
|
self._cache: dict = {}
|
|
335
|
-
|
|
336
|
+
# spec 04 section 3: every draw is seeded from the fit's input hash, so identical fits give identical
|
|
337
|
+
# output; fits before 0.2.1 have no `seed_key` and keep their fit id
|
|
338
|
+
self.seed_key: str = self.meta.get("seed_key") or self.fit_id
|
|
339
|
+
self.sim_seed = _seed(self.seed_key, "simulate")
|
|
340
|
+
# spec 04 section 1: the value of the timebox-capped terms (effort, shape) in a timeboxed task's cost rows
|
|
341
|
+
self.timebox_effort = float(self.meta.get("timebox_effort", 1.0))
|
|
336
342
|
if heads is None:
|
|
337
343
|
heads = {}
|
|
338
344
|
for name, hmeta in (self.meta.get("heads") or {}).items():
|
|
339
345
|
key = name.replace(":", "@")
|
|
340
346
|
ids = [str(x) for x in self._array(f"{key}__ids")]
|
|
341
|
-
heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id)
|
|
347
|
+
heads[name] = HeadState(name, hmeta, ids, self._loader(key), self.fit_id, seed_key=self.seed_key)
|
|
342
348
|
for name in BASE_HEADS:
|
|
343
349
|
if name not in heads:
|
|
344
|
-
heads[name] = HeadState(name, {}, [], None, self.fit_id)
|
|
350
|
+
heads[name] = HeadState(name, {}, [], None, self.fit_id, seed_key=self.seed_key)
|
|
345
351
|
self.heads = heads
|
|
346
352
|
# the fit's feature declarations (the built-ins for fits before 0.2), so predictions read
|
|
347
353
|
# features the way the fit did
|
|
@@ -397,13 +403,22 @@ class FitState:
|
|
|
397
403
|
def _row(self, head: HeadState, rest: tuple) -> Row:
|
|
398
404
|
return self._cached(head, [rest])[0]
|
|
399
405
|
|
|
400
|
-
def
|
|
406
|
+
def effort_for(self, task: Task) -> float:
|
|
407
|
+
"""The value of the timebox-capped terms (effort and shape, `design.TIMEBOX_LEVELS`) in the task's cost
|
|
408
|
+
rows: the fit's `timebox_effort` when the task's resolved horizon (given, else recorded) is a timebox the
|
|
409
|
+
fit uses (it has timeboxed runs), else 1 (spec 04 section 1)."""
|
|
410
|
+
if self.timebox_effort == 1.0:
|
|
411
|
+
return 1.0
|
|
412
|
+
return self.timebox_effort if self.resolve_task(task)[1]["horizon"]["used"] else 1.0
|
|
413
|
+
|
|
414
|
+
def _plan(self, config: Configuration, effort: float = 1.0) -> dict:
|
|
415
|
+
"""The configuration's rows without the task part; `effort` from `effort_for(task)`."""
|
|
401
416
|
st = structure(config)
|
|
402
417
|
cost = {}
|
|
403
418
|
for piece in st.pieces:
|
|
404
419
|
rounds = st.k_max if st.piece_loop.get(piece) is not None else 1
|
|
405
420
|
for k in range(1, rounds + 1):
|
|
406
|
-
cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE)
|
|
421
|
+
cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort)
|
|
407
422
|
gate = {}
|
|
408
423
|
for gi, g in enumerate(st.gates):
|
|
409
424
|
for k in range(1, (st.k_max if st.gate_loop.get(gi) is not None else 1) + 1):
|
|
@@ -790,7 +805,8 @@ class FitState:
|
|
|
790
805
|
|
|
791
806
|
def _predict_many(self, task: Task, configs: Sequence[Configuration], rule: AcceptanceRule | None,
|
|
792
807
|
rescue_usd: float | None) -> list[tuple[Prediction, dict]]:
|
|
793
|
-
|
|
808
|
+
effort = self.effort_for(task)
|
|
809
|
+
plans = [self._plan(c, effort) for c in configs]
|
|
794
810
|
tparts = self._task_parts(task)
|
|
795
811
|
self._warm(plans)
|
|
796
812
|
return [self._predict(task, p, tparts, rule, rescue_usd) for p in plans]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""`loopmath builder`: the workflow builder (P8), a local page over the fitted model (spec 02, `builder`)."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The `loopmath builder` parser, added by `cli_registry.register` (spec 02, `builder`).
|
|
2
|
+
|
|
3
|
+
It takes the task and rule flags of `recommend`, from the registry's own helpers, and imports nothing
|
|
4
|
+
heavy: the handler is `builder.server:serve`, loaded only when the command runs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add(sub) -> None:
|
|
11
|
+
from ..cli_registry import FIT_HELP, _common, _lazy, _task_args
|
|
12
|
+
|
|
13
|
+
p = sub.add_parser("builder", help="build your own workflow on a local page and see its estimates change",
|
|
14
|
+
description="Serves the workflow builder on 127.0.0.1: pick a shape, change models, efforts, "
|
|
15
|
+
"widths and pieces, and see the estimated numbers for the task, the same ones "
|
|
16
|
+
"recommend gives. Ctrl+C stops it.")
|
|
17
|
+
_task_args(p)
|
|
18
|
+
rule = p.add_mutually_exclusive_group()
|
|
19
|
+
rule.add_argument("--rule", default=None, metavar="RULE", help="a named acceptance rule from config")
|
|
20
|
+
rule.add_argument("--target", default=None, metavar="NAME>=X", help="score-target rule, quoted: e.g. 'heldout_perf>=2400'")
|
|
21
|
+
p.add_argument("--goal", default=None, choices=["default", "p50", "p70", "p80", "p90", "p95", "p99"],
|
|
22
|
+
help="the pick to aim for, as for recommend (default: config goal)")
|
|
23
|
+
p.add_argument("--usual", default=None, metavar="CFG", help="a configuration id to treat as the usual one, as for recommend")
|
|
24
|
+
p.add_argument("--workflow", action="append", default=[], metavar="FILE.toml", help="also consider this workflow (repeatable)")
|
|
25
|
+
p.add_argument("--models", default=None, metavar="M,M,...", help="restrict candidate models")
|
|
26
|
+
p.add_argument("--fit", default=None, metavar="ID", help=FIT_HELP)
|
|
27
|
+
p.add_argument("--rec", default=None, metavar="REC", help="start from a saved recommendation: its task, rule and fit "
|
|
28
|
+
"(task flags given as well override its task)")
|
|
29
|
+
p.add_argument("--start", default=None, metavar="CFG", help="open this configuration first: a cfg_ id from the "
|
|
30
|
+
"recommendation, an earlier one or your runs")
|
|
31
|
+
p.add_argument("--port", type=int, default=0, metavar="N", help="port on 127.0.0.1 (default 0: a free port)")
|
|
32
|
+
p.add_argument("--no-open", action="store_true", help="print the URL without opening the browser")
|
|
33
|
+
_common(p, json_flag=False)
|
|
34
|
+
p.set_defaults(func=_lazy("loopmath.builder.server:serve"))
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""The builder's session: the fit and the recommendation it answers from, and `GET /api/context`.
|
|
2
|
+
|
|
3
|
+
At start `build_session` reads the task and rule as `recommend` does, loads the fit once and runs the
|
|
4
|
+
recommend engine once, with the same helpers in the same order as `recommend.commands.recommend`, so every
|
|
5
|
+
number the page shows is the one `recommend` gives. The session keeps the belief, the task as the belief
|
|
6
|
+
sees it, the rule and the recommendation in memory; `predict.py` answers edited configurations from them.
|
|
7
|
+
Nothing is written to the store.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import dataclasses
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ..output import EXIT_NOT_FOUND, EXIT_OK, fail, home as home_dir
|
|
22
|
+
from ..recommend import commands as C
|
|
23
|
+
from ..recommend import engine, storeread
|
|
24
|
+
from ..recommend.engine import Recommendation
|
|
25
|
+
from ..recommend.storeread import Conf
|
|
26
|
+
from ..types import AcceptanceRule, Configuration, Task
|
|
27
|
+
|
|
28
|
+
SCHEMA = "loopmath.builder.context/1"
|
|
29
|
+
TOP_CANDIDATES = 60 # the page's candidates: the first 60 by cost per accepted result, plus recorded ones
|
|
30
|
+
KEEP_ORIGINS = ("usual", "user", "recorded")
|
|
31
|
+
MADE_UP_TASK_ID = re.compile(r"^tsk_[0-9A-HJKMNP-TV-Z]{26}$") # `storeread.ulid()`: an id no user gave
|
|
32
|
+
TASK_KEYS = ("type", "repo", "title", "subtype", "org", "features", "base_commit")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Session:
|
|
37
|
+
belief: Any
|
|
38
|
+
task: Task # the task as the belief sees it (recommend's `asked`); predictions use this one
|
|
39
|
+
rule: AcceptanceRule
|
|
40
|
+
rec: Recommendation # `rec.task` is the task as shown
|
|
41
|
+
fit: dict[str, Any]
|
|
42
|
+
rec_id: str | None = None # `--rec`
|
|
43
|
+
start: Configuration | None = None # `--start`
|
|
44
|
+
lock: threading.Lock = field(default_factory=threading.Lock) # the belief caches rows; one predict at a time
|
|
45
|
+
cache: OrderedDict = field(default_factory=OrderedDict) # config id -> predict answer
|
|
46
|
+
context: dict[str, Any] | None = None
|
|
47
|
+
models: dict[str, int] | None = None # `known_models`, read once
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------- start
|
|
51
|
+
def rec_task(stored: dict[str, Any], args: argparse.Namespace) -> tuple[Task, bool]:
|
|
52
|
+
"""A stored recommendation's task, with the task flags given on top. Its id counts as the user's only
|
|
53
|
+
when it is not one `recommend` made up, so the belief sees the same question."""
|
|
54
|
+
t = stored.get("task") if isinstance(stored.get("task"), dict) else {}
|
|
55
|
+
d = {k: t[k] for k in TASK_KEYS if t.get(k) is not None}
|
|
56
|
+
tid = str(t.get("id") or "")
|
|
57
|
+
given = bool(tid) and not MADE_UP_TASK_ID.match(tid)
|
|
58
|
+
if given:
|
|
59
|
+
d["id"] = tid
|
|
60
|
+
return C.task_from_dict(d, overrides=args), given
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def asked_task(task: Task, id_given: bool, belief: Any, rule: AcceptanceRule, configs: list) -> Task:
|
|
64
|
+
"""The task as `recommend` asks the belief: the user's when they named it, else `commands.question_task`
|
|
65
|
+
keyed as `recommend` keys it, by `commands.question_key(belief)` once `recommend` has it (0.2.1 lane
|
|
66
|
+
21F: the fit's input seed key, so identical fits agree), else by the fit id as in 0.2.0."""
|
|
67
|
+
if id_given:
|
|
68
|
+
return task
|
|
69
|
+
key_of = getattr(C, "question_key", None)
|
|
70
|
+
return C.question_task(task, key_of(belief) if callable(key_of) else belief.fit_id, rule, configs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
|
|
74
|
+
"""The session, or None and the exit code after the message `recommend` would print."""
|
|
75
|
+
home = home_dir(getattr(args, "home", None))
|
|
76
|
+
now = storeread.now_local()
|
|
77
|
+
stored = None
|
|
78
|
+
try:
|
|
79
|
+
if getattr(args, "rec", None):
|
|
80
|
+
stored = storeread.load_rec(home, args.rec)
|
|
81
|
+
if stored is None:
|
|
82
|
+
raise C.NotFound(f"recommendation {args.rec} not found in {home / 'recs'}")
|
|
83
|
+
task, id_given = rec_task(stored, args)
|
|
84
|
+
ref = stored.get("reference") or {}
|
|
85
|
+
if not getattr(args, "usual", None) and ref.get("from") == "flag":
|
|
86
|
+
args.usual = (ref.get("config") or {}).get("id")
|
|
87
|
+
goal = (stored.get("goal") or {}).get("choice")
|
|
88
|
+
if not getattr(args, "goal", None) and isinstance(goal, str) and goal.startswith("p"):
|
|
89
|
+
args.goal = goal
|
|
90
|
+
else:
|
|
91
|
+
task, id_given = C.task_from_args(args)
|
|
92
|
+
conf = Conf.load(home)
|
|
93
|
+
if stored is not None and not (getattr(args, "rule", None) or getattr(args, "target", None)) \
|
|
94
|
+
and isinstance(stored.get("rule"), dict):
|
|
95
|
+
rule = AcceptanceRule.from_dict(stored["rule"])
|
|
96
|
+
else:
|
|
97
|
+
rule = C.rule_from_args(args, conf)
|
|
98
|
+
spend, cap = C.budget(home, conf)
|
|
99
|
+
settings = C.settings_from(conf, args, spend, cap)
|
|
100
|
+
models = C.model_list(getattr(args, "models", None))
|
|
101
|
+
except C.NotFound as exc:
|
|
102
|
+
return None, fail(str(exc), EXIT_NOT_FOUND)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
return None, fail(str(exc))
|
|
105
|
+
fit_id = getattr(args, "fit", None)
|
|
106
|
+
if stored is not None and not fit_id: # the recommendation's own fit while it is kept, else the latest
|
|
107
|
+
fit_id = (stored.get("fit") or {}).get("id")
|
|
108
|
+
if fit_id and not (home / "fits" / str(fit_id)).is_dir():
|
|
109
|
+
print(f"warning: {args.rec} was made with fit {fit_id}, which is no longer kept; using the latest fit",
|
|
110
|
+
file=sys.stderr)
|
|
111
|
+
fit_id = None
|
|
112
|
+
belief, code = C.load_belief(home, fit_id)
|
|
113
|
+
if belief is None:
|
|
114
|
+
return None, code
|
|
115
|
+
try: # `recommend.commands.recommend`, step for step
|
|
116
|
+
usual, usual_from = C.resolve_usual(home, conf, task, getattr(args, "usual", None), models)
|
|
117
|
+
recorded = [c for c, _ in storeread.recorded_configs(home, task.type, task.repo)[0]]
|
|
118
|
+
if usual_from == "default" and recorded:
|
|
119
|
+
asked = asked_task(task, id_given, belief, rule, recorded)
|
|
120
|
+
best = C.reference_for(belief, asked, rule, recorded, models)
|
|
121
|
+
if best is not None:
|
|
122
|
+
usual, usual_from = best, "recorded"
|
|
123
|
+
user = [C.workflow_file_config(p, usual) for p in getattr(args, "workflow", None) or []]
|
|
124
|
+
configs = C.candidate_configs(task, usual, conf, models, user + C.store_user_configs(home, usual), recorded)
|
|
125
|
+
asked = asked_task(task, id_given, belief, rule, [usual, *(c for c, _ in configs)])
|
|
126
|
+
rec = engine.recommend(belief, asked, rule, usual=usual, usual_from=usual_from, configs=configs,
|
|
127
|
+
settings=settings, diff=C.diff_fn(), keep=[*(u.id for u in user),
|
|
128
|
+
*(r.id for r in recorded)])
|
|
129
|
+
rec.task = task
|
|
130
|
+
start = None
|
|
131
|
+
if getattr(args, "start", None):
|
|
132
|
+
found = rec.by_id(args.start)
|
|
133
|
+
start = found.config if found is not None else storeread.find_config(home, args.start)
|
|
134
|
+
if start is None:
|
|
135
|
+
raise C.NotFound(f"configuration {args.start} not found in this recommendation, earlier "
|
|
136
|
+
f"recommendations or stored runs")
|
|
137
|
+
except C.NotFound as exc:
|
|
138
|
+
return None, fail(str(exc), EXIT_NOT_FOUND)
|
|
139
|
+
except ValueError as exc:
|
|
140
|
+
return None, fail(str(exc))
|
|
141
|
+
fit = {**C.fit_info(belief, now), "runs": int(getattr(belief, "n_runs", 0) or 0)}
|
|
142
|
+
return Session(belief, asked, rule, rec, fit, rec_id=getattr(args, "rec", None), start=start), EXIT_OK
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------- catalog
|
|
146
|
+
def fit_models(belief: Any) -> dict[str, int]:
|
|
147
|
+
"""Models the fit has a node for, with the most runs any head has behind that node."""
|
|
148
|
+
design = getattr(belief, "design", None)
|
|
149
|
+
support = (design.get("support") if isinstance(design, dict) else None) or {}
|
|
150
|
+
out: dict[str, int] = {}
|
|
151
|
+
for head in support.values():
|
|
152
|
+
for node, val in (head or {}).items():
|
|
153
|
+
if isinstance(node, str) and node.startswith("model:"):
|
|
154
|
+
n = int(val[0]) if isinstance(val, (list, tuple)) and val else 0
|
|
155
|
+
out[node[len("model:"):]] = max(out.get(node[len("model:"):], 0), n)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def rec_configs(rec: Recommendation) -> list[Configuration]:
|
|
160
|
+
return [rec.usual.config, *(c.config for c in rec.candidates)]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def known_models(session: Session) -> dict[str, int]:
|
|
164
|
+
"""Model id -> runs behind it: the fit's models and any a candidate uses (0 runs when the fit has no node)."""
|
|
165
|
+
if session.models is None:
|
|
166
|
+
out = fit_models(session.belief)
|
|
167
|
+
for cfg in rec_configs(session.rec):
|
|
168
|
+
for s in cfg.settings.values():
|
|
169
|
+
out.setdefault(s.model, 0)
|
|
170
|
+
session.models = out
|
|
171
|
+
return session.models
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def catalog(session: Session) -> dict[str, Any]:
|
|
175
|
+
from ..workflows.format import KNOWN_ROLES, catalog as shapes
|
|
176
|
+
from ..workflows.models import DEFAULT_EFFORTS, efforts_for, family_of, harness_for, sort_efforts
|
|
177
|
+
|
|
178
|
+
seen = [s for cfg in rec_configs(session.rec) for s in cfg.settings.values()]
|
|
179
|
+
harnesses = list(dict.fromkeys([*DEFAULT_EFFORTS, *(s.harness for s in seen)]))
|
|
180
|
+
models = sorted(known_models(session).items(), key=lambda kv: (-kv[1], kv[0]))
|
|
181
|
+
by_harness = {h: list(efforts_for(h)) for h in harnesses}
|
|
182
|
+
efforts = sort_efforts([e for v in by_harness.values() for e in v] + [s.effort for s in seen])
|
|
183
|
+
return {
|
|
184
|
+
"harnesses": harnesses,
|
|
185
|
+
"models": [{"id": m, "family": family_of(m), "harness": harness_for(m), "runs_behind": n}
|
|
186
|
+
for m, n in models],
|
|
187
|
+
"efforts": list(efforts),
|
|
188
|
+
"efforts_by_harness": by_harness,
|
|
189
|
+
"roles": list(KNOWN_ROLES),
|
|
190
|
+
"shapes": [{"id": wf.id, "title": wf.title,
|
|
191
|
+
"pieces": [{"id": p.id, "role": p.role, "width": p.width} for p in wf.pieces],
|
|
192
|
+
"edges": [list(e) for e in wf.edges],
|
|
193
|
+
"gates": [g.to_dict() for g in wf.control.gates],
|
|
194
|
+
"workflow": wf.to_dict()} for wf in shapes().values()],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------- the context
|
|
199
|
+
def candidate_entry(rec: Recommendation, c: Any) -> dict[str, Any]:
|
|
200
|
+
return {"config": c.config.to_dict(), "label": rec.label(c.config), "numbers": rec.numbers(c.prediction),
|
|
201
|
+
"origin": c.origin, **rec.search_fields(c.config.id)}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def context_payload(session: Session) -> dict[str, Any]:
|
|
205
|
+
"""`GET /api/context` (spec 02, `builder`), built once per session."""
|
|
206
|
+
if session.context is not None:
|
|
207
|
+
return session.context
|
|
208
|
+
rec = session.rec
|
|
209
|
+
core = rec.payload()
|
|
210
|
+
choices = []
|
|
211
|
+
for ch in core["choices"]:
|
|
212
|
+
c = rec.by_id(ch["config"])
|
|
213
|
+
choices.append({**ch, "configuration": c.config.to_dict() if c is not None else None})
|
|
214
|
+
reference = {k: v for k, v in core["reference"].items() if k != "prediction"}
|
|
215
|
+
top = rec.candidates[:TOP_CANDIDATES]
|
|
216
|
+
ids = {c.config.id for c in top}
|
|
217
|
+
wanted = {rec.usual.config.id, *(ch["config"] for ch in core["choices"])}
|
|
218
|
+
top += [c for c in rec.candidates[TOP_CANDIDATES:]
|
|
219
|
+
if c.config.id not in ids and (c.origin in KEEP_ORIGINS or c.config.id in wanted)]
|
|
220
|
+
session.context = {
|
|
221
|
+
"schema": SCHEMA,
|
|
222
|
+
"task": C.task_block(rec.task, session.belief),
|
|
223
|
+
"rule": core["rule"],
|
|
224
|
+
"fit": session.fit,
|
|
225
|
+
"rec": session.rec_id,
|
|
226
|
+
"rescue": core["rescue"],
|
|
227
|
+
"reference": reference,
|
|
228
|
+
"choices": choices,
|
|
229
|
+
"candidates": [candidate_entry(rec, c) for c in top],
|
|
230
|
+
"catalog": catalog(session),
|
|
231
|
+
"start": session.start.to_dict() if session.start is not None else None,
|
|
232
|
+
}
|
|
233
|
+
return session.context
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def with_numbers(rec: Recommendation, **by_id: dict[str, Any]) -> Recommendation:
|
|
237
|
+
"""The recommendation with more medians (and bands) by config id, so `numbers()` formats a new prediction
|
|
238
|
+
exactly as it formats a candidate's."""
|
|
239
|
+
changes = {k: {**getattr(rec, k), **v} for k, v in by_id.items() if v and hasattr(rec, k)}
|
|
240
|
+
return dataclasses.replace(rec, **changes) if changes else rec
|