loopmath 0.2.1__tar.gz → 0.2.2__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.1 → loopmath-0.2.2}/PKG-INFO +1 -1
- {loopmath-0.2.1 → loopmath-0.2.2}/pyproject.toml +1 -1
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/__init__.py +1 -1
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/design.py +17 -7
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/fit.py +35 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/forest.py +1 -0
- loopmath-0.2.2/src/loopmath/belief/pricing.py +142 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/state.py +5 -2
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/context.py +132 -19
- loopmath-0.2.2/src/loopmath/builder/predict.py +306 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/server.py +15 -11
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_registry.py +1 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.toml +465 -3
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/build.py +25 -1
- loopmath-0.2.2/src/loopmath/priors/bundle/README.md +3 -0
- loopmath-0.2.2/src/loopmath/priors/bundle/lanes.jsonl.gz +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/manifest.json +65 -1
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/commands.py +3 -3
- loopmath-0.2.2/src/loopmath/priors/lanes.py +267 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/reduce.py +4 -2
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/registry.py +8 -5
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/commands.py +25 -11
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/engine.py +41 -13
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/search.py +9 -6
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/config.py +2 -1
- loopmath-0.2.2/src/loopmath/views/assets/builder.css +303 -0
- loopmath-0.2.2/src/loopmath/views/assets/builder.html +70 -0
- loopmath-0.2.2/src/loopmath/views/assets/builder.js +1314 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/plans.js +19 -3
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/PKG-INFO +1 -1
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/SOURCES.txt +5 -0
- loopmath-0.2.1/src/loopmath/builder/predict.py +0 -180
- loopmath-0.2.1/src/loopmath/priors/bundle/README.md +0 -1
- loopmath-0.2.1/src/loopmath/views/assets/builder.html +0 -109
- {loopmath-0.2.1 → loopmath-0.2.2}/LICENSE +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/README.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/setup.cfg +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp-v0.2.schema.json +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp-v0.schema.json +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/spec/ocp_conformance.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/__main__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapter_host.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/atrium.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/base.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/bb.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/bb_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_facts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_links.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_parse.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/omp_types.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/opencode.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/opencode_store.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/orca.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_graph.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_parse.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_types.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/otel_genai_usage.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/paseo.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/pi.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/pi_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/adapters/registry.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/block.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/check_pymc.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/compose.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/features.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/gaussian.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/lookahead.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/outcome.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/belief/priors.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/builder/cli.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_graph.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/cli_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/arms.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/cli_e0.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/estimate.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/estimate_rework.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/figures.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/io.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/e0/verb.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/explain.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_assembly.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_bayes.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_masks.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_predict.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_pricing.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/fit_transfer.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/gaps.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/gitwalk.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/grade.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/grade_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_git.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_join.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifact_kinds.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/artifacts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashparse.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashread_ops.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashwrite_ops.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/bashwrites.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/cache.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio_parse.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/codexio_scan.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_build.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_core.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_edges.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/dataset_nodes.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/extract.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_common.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_costcurve.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_css.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_data.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_force.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_render.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/html_swimlanes.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_cli.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_common.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_parse.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_pricing.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_build.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_dispatch.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_evidence.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_node.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_records.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_result.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_session.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_tools.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_validation.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_prompt_workflow.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_report.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labeler_score.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/labels.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/launch.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/launch_origin.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp_emit.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/ocp_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/render.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile_export.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/runfile_finish.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/scan.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/schema.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/graph/token_completeness.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/base.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/claude_code.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/claude_code_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/codex.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/codex_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_analysis.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_artifact.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_common.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_convert.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ingest/ocp_projection.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/artifacts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/clip.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/costs.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/match.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/settle.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/logmatch/tariff.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/canonical.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/conformance.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/contractv3.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/emit.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/migrate.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.2.schema.json +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.3.schema.json +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/schema/ocp-v0.schema.json +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/ocp/version.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/history.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/label.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/onboard/usual.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/output.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/pool.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/price.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/price_validation.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/prices.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/benchmarks.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/e0.jsonl.gz +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/rq1.jsonl.gz +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/bundle/sweep.jsonl.gz +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/e0.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/history.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/ocpdoc.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/rq1.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/show.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/sweep.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/priors/validate.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/receipts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/backlog.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/curve.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/gain.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/message.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/receipts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/stats.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/recommend/storeread.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/html.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_exclusions.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_format.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/report/terminal_read.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/research_defaults.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/research_paths.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring_comparisons.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/scoring_predictive.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/export.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/share/import_.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/doctor.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/install.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-import-runs/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-onboard/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-plan-task/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-record-run/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/loopmath-update-fit/SKILL.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/skill/skills/reference.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/budget.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/finish.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/fitjob.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/home.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/ids.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/lock.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/record.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/report.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/runs.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/store/status.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_bootstrap.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_estimation.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_frame.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_presentation.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/surface_support.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/taskmodel.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/types.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/README.md +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/base.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/common.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/estimates.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/graph.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/graph.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/plans.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/results.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/results.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/runs.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/viz.css +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/assets/viz.js +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/common.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/plans.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/posterior.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/views/runs.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/__init__.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/candidates.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/best_of_n.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/implement_review.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/plan_implement_review.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/solo.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/catalog/swarm.toml +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/commands.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/diff.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/format.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/graphview.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/ids.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/infer.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/models.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/ocp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath/workflows/shapes.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/dependency_links.txt +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/entry_points.txt +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/requires.txt +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/src/loopmath.egg-info/top_level.txt +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_atrium.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_bb.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_omp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_opencode.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_opencode_output_tokens.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_orca.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_otel_genai.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_paseo.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapter_services.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapters.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_adapters_pi.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_arms.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_graph.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_help_lists.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_labeler.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_main_module.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_plan_hidden.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_prices.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_cli_startup.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_color_proof.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_duplicate_guards.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_estimate.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_explain.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_fit.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_gaps.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_gitwalk.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_grade.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_artifact_recording.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_artifacts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_bashwrites.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_cache.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_codexio.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_dataset.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_extract.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_glue.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_html.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_labeler.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_labeler_evidence_matrix.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_launch.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_ocp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_render.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_runfile.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_graph_schema.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_home_guard.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_html.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_claude_code.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_codex.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ingest_ocp.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_io.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_not_run_summary.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_conformance.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_golden.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_open_enums.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_ocp_versioning.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_output_html_path.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_packaging_metadata.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_packaging_smoke.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_pool.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_preview_script.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_price.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_receipts.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_release_paths.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_runfile_privacy_n1b.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_scoring.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_split_rebindings.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_surface.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_taskmodel_features.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_terminal.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_types_control.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/tests/test_types_piece_prediction.py +0 -0
- {loopmath-0.2.1 → loopmath-0.2.2}/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.2"
|
|
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"
|
|
@@ -13,7 +13,7 @@ A term is `(node_id, parent_id, value)`. Rows:
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import math
|
|
16
|
-
from collections.abc import Iterable
|
|
16
|
+
from collections.abc import Iterable, Mapping
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
from functools import lru_cache
|
|
@@ -25,6 +25,7 @@ from ..types import (
|
|
|
25
25
|
Workflow,
|
|
26
26
|
)
|
|
27
27
|
from .forest import canonical_model_id, canonical_role, model_path
|
|
28
|
+
from .pricing import price_term
|
|
28
29
|
|
|
29
30
|
Term = tuple[str, "str | None", float]
|
|
30
31
|
|
|
@@ -324,12 +325,15 @@ def _effort_scaled(terms: tuple[Term, ...], weight: float) -> tuple[Term, ...]:
|
|
|
324
325
|
|
|
325
326
|
|
|
326
327
|
def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None, *,
|
|
327
|
-
source: str | None = None, effort: float = 1.0
|
|
328
|
+
source: str | None = None, effort: float = 1.0,
|
|
329
|
+
prices: Mapping[str, float] | None = None) -> tuple[Term, ...]:
|
|
328
330
|
"""Cost and tokens rows without the task part: setting, role, topology, position, round, control,
|
|
329
331
|
and with a `source` the position x source node `psrc:<shape>#<pos>|<source>` and the family x source
|
|
330
332
|
node `fsrc:<family>|<source>` (spec 04 section 1). `effort` is the value of the terms a timebox caps, the
|
|
331
333
|
effort and shape (topology, position) terms (`effort_weight`: below 1 under a timebox); the role, psrc and
|
|
332
|
-
fsrc terms stay at 1.
|
|
334
|
+
fsrc terms stay at 1. `prices` is a fit's price offsets (`FitState.price_offsets`, spec 04 section 2): the
|
|
335
|
+
model's `price:offset` term goes last. The cost head has that node and the tokens head does not, so a
|
|
336
|
+
prediction's shared row prices only its cost."""
|
|
333
337
|
s = setting or st.settings[piece]
|
|
334
338
|
h, m, e = _s(s)
|
|
335
339
|
shape = st.shape or st.workflow_id
|
|
@@ -343,7 +347,8 @@ def cost_rest(st: Structure, piece: str, k: int, setting: Setting | None = None,
|
|
|
343
347
|
fam = model_path(m)[1]
|
|
344
348
|
terms += ((f"psrc:{position}|{source}", f"position:{position}", 1.0),
|
|
345
349
|
(f"fsrc:{fam}|{source}", f"family:{fam}", 1.0))
|
|
346
|
-
return terms + tuple(round_terms(k)) + tuple(_control_terms(st.k_max, st.widths[piece]))
|
|
350
|
+
return (terms + tuple(round_terms(k)) + tuple(_control_terms(st.k_max, st.widths[piece]))
|
|
351
|
+
+ price_term(prices, m))
|
|
347
352
|
|
|
348
353
|
|
|
349
354
|
def gate_rest(st: Structure, gate: GateInfo, k: int) -> tuple[Term, ...]:
|
|
@@ -421,6 +426,7 @@ class AttemptObs:
|
|
|
421
426
|
weight: float
|
|
422
427
|
setting: Setting
|
|
423
428
|
repriced: bool = True # False: the recorded dollars, not the current tariff
|
|
429
|
+
streams: tuple[float, ...] = () # input, cache read, cache write, output tokens (pricing.STREAMS order)
|
|
424
430
|
|
|
425
431
|
|
|
426
432
|
@dataclass
|
|
@@ -458,8 +464,10 @@ def source_label(doc: dict) -> str:
|
|
|
458
464
|
ext = run.get("ext") or {}
|
|
459
465
|
share = ext.get("dev.loopmath.share")
|
|
460
466
|
if isinstance(share, dict):
|
|
461
|
-
|
|
462
|
-
|
|
467
|
+
# the importer writes `source` and `task.org` as `shared:<org_hash>` already: never prefix twice
|
|
468
|
+
org = (share.get("source") or share.get("org") or share.get("org_hash") or (run.get("task") or {}).get("org")
|
|
469
|
+
or "shared")
|
|
470
|
+
return "shared:" + str(org).removeprefix("shared:")
|
|
463
471
|
src = (run.get("task") or {}).get("source")
|
|
464
472
|
kind = src.get("kind") if isinstance(src, dict) else src
|
|
465
473
|
kind = str(kind or "").strip().lower()
|
|
@@ -761,7 +769,9 @@ def parse_run(doc: dict, *, now: datetime | None = None, rule: AcceptanceRule |
|
|
|
761
769
|
if weight <= 0 or ((usd is None or usd <= 0) and not tokens):
|
|
762
770
|
dropped["attempt without usable cost"] = dropped.get("attempt without usable cost", 0) + 1
|
|
763
771
|
continue
|
|
764
|
-
|
|
772
|
+
streams = tuple(_ocp_fields(cost)[name] for name in _OCP_TOKEN_FIELDS)
|
|
773
|
+
attempts.append(AttemptObs(piece, k, usd if usd and usd > 0 else None, tokens, weight, setting, repriced,
|
|
774
|
+
streams))
|
|
765
775
|
|
|
766
776
|
gates = _gate_observations(doc, st, by_piece_round)
|
|
767
777
|
use_rule = rule or rule_from_doc(doc)
|
|
@@ -36,6 +36,7 @@ from .design import (DESIGN_VERSION, Term, Unusable, cost_row, gate_row, parse_r
|
|
|
36
36
|
from .features import FeatureTally, add_horizon, drop_features, horizon_nodes, horizon_specs
|
|
37
37
|
from .block import leaf_split
|
|
38
38
|
from .forest import Forest, default_scale, fixed_sd, scale_group
|
|
39
|
+
from .pricing import PRICE_NODE, PRICE_PRIOR_SD, STREAMS, fit_offsets, price_term
|
|
39
40
|
from .gaussian import N_DRAWS, Factors, GaussianHead, HeadFit, LogisticHead, Prior
|
|
40
41
|
from .state import SCORE_CLAMP, inverse_transform, transform # noqa: F401 (re-exported)
|
|
41
42
|
|
|
@@ -163,6 +164,7 @@ class HeadRows:
|
|
|
163
164
|
sources: list[str] = field(default_factory=list)
|
|
164
165
|
types: list[str] = field(default_factory=list)
|
|
165
166
|
not_repriced: int = 0 # cost rows on recorded dollars, not the current tariff
|
|
167
|
+
prices: dict = field(default_factory=dict) # cost head: the fit's price offsets (pricing.fit_offsets)
|
|
166
168
|
|
|
167
169
|
def add(self, terms, y, w, run, source, ttype):
|
|
168
170
|
self.rows.append(terms)
|
|
@@ -213,6 +215,8 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
|
|
|
213
215
|
stored: set[str] = set() # run ids of the user's store, whose copy wins
|
|
214
216
|
overlap: Counter = Counter()
|
|
215
217
|
labels: Counter = Counter()
|
|
218
|
+
model_runs: dict[str, set[str]] = defaultdict(set) # the price offset's inputs (spec 04 section 2)
|
|
219
|
+
model_streams: dict[str, list[float]] = defaultdict(lambda: [0.0] * len(STREAMS))
|
|
216
220
|
for item in docs:
|
|
217
221
|
origin, doc = item if isinstance(item, tuple) else (None, item)
|
|
218
222
|
if origin is not None and _excluded(origin, without):
|
|
@@ -258,6 +262,10 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
|
|
|
258
262
|
if a.usd:
|
|
259
263
|
heads["cost"].add(terms, math.log(a.usd), a.weight, pr.run_id, pr.source, ttype)
|
|
260
264
|
heads["cost"].not_repriced += not a.repriced
|
|
265
|
+
model_runs[a.setting.model].add(pr.run_id)
|
|
266
|
+
acc = model_streams[a.setting.model]
|
|
267
|
+
for i, v in enumerate(a.streams):
|
|
268
|
+
acc[i] += v
|
|
261
269
|
if a.tokens:
|
|
262
270
|
heads["tokens"].add(terms, math.log(a.tokens), a.weight, pr.run_id, pr.source, ttype)
|
|
263
271
|
for gi, k, passed in pr.gates:
|
|
@@ -289,10 +297,34 @@ def collect_rows(docs: Iterable[dict | tuple[str | None, dict]], *, without: tup
|
|
|
289
297
|
coding = tally.horizon_coding() # the horizon terms need every run's horizon first (spec 04 section 1)
|
|
290
298
|
for hr in heads.values():
|
|
291
299
|
hr.rows = add_horizon(drop_features(hr.rows, tally.nodes), hr.runs, tally.run_horizon, coding)
|
|
300
|
+
add_prices(heads["cost"], {m: len(r) for m, r in model_runs.items()}, model_streams)
|
|
292
301
|
return (heads, score_info, dict(dropped), dict(runs_by_source), {c: dict(n) for c, n in config_support.items()},
|
|
293
302
|
dict(checks), dict(overlap), dict(labels), tally)
|
|
294
303
|
|
|
295
304
|
|
|
305
|
+
def add_prices(hr: HeadRows, runs: dict[str, int], streams: dict[str, list[float]]) -> None:
|
|
306
|
+
"""The price offset (spec 04 section 2): the fit's offsets from its runs and token mix under the current price
|
|
307
|
+
table, and each cost row with its model's `price:offset` term (`pricing.price_term`, as at prediction)."""
|
|
308
|
+
hr.prices = fit_offsets(runs, streams, design_rows._price_table())
|
|
309
|
+
offsets = hr.prices["models"]
|
|
310
|
+
if not offsets:
|
|
311
|
+
return
|
|
312
|
+
rows = []
|
|
313
|
+
for row in hr.rows:
|
|
314
|
+
model = next((n[len("model:"):] for n, _, _ in row if n.startswith("model:")), None)
|
|
315
|
+
term = price_term(offsets, model) if model else ()
|
|
316
|
+
rows.append(row + list(term) if term else row)
|
|
317
|
+
hr.rows = rows
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def price_spec(hr: HeadRows) -> list:
|
|
321
|
+
"""The prior that holds `price:offset` at 1, on the cost head of a fit with offsets, --no-prior included."""
|
|
322
|
+
if not hr.prices.get("models"):
|
|
323
|
+
return []
|
|
324
|
+
return [prior_data.FactorSpec("cost", [(PRICE_NODE, None, 1.0)], 1.0, PRICE_PRIOR_SD ** 2,
|
|
325
|
+
f"{PRICE_NODE} held at 1, prior N(1, {PRICE_PRIOR_SD}^2)")]
|
|
326
|
+
|
|
327
|
+
|
|
296
328
|
# ---------------------------------------------------------------- matrices and priors
|
|
297
329
|
|
|
298
330
|
def build_matrix(rows: list[list[Term]], forest: Forest, extra_nodes: Iterable[tuple[str, str | None]] = ()):
|
|
@@ -532,6 +564,8 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
|
|
|
532
564
|
nodes = horizon_nodes(hr.rows)
|
|
533
565
|
if nodes: # the horizon priors, --no-prior included (spec 04 section 1)
|
|
534
566
|
head_specs += horizon_specs(name, hr.kind, nodes)
|
|
567
|
+
if name == "cost": # the price offset (spec 04 section 2)
|
|
568
|
+
head_specs += price_spec(hr)
|
|
535
569
|
fitted[name] = fit_head(hr, forest, head_specs, eb=eb)
|
|
536
570
|
fit_id = new_fit_id(fits, now)
|
|
537
571
|
partial = fits / f"{fit_id}.partial"
|
|
@@ -592,6 +626,7 @@ def fit(home: Path, *, no_prior: bool = False, without: tuple[str, ...] = (), fu
|
|
|
592
626
|
"design_version": DESIGN_VERSION,
|
|
593
627
|
"features": {**tally.report, **({"error": features_error} if features_error else {})},
|
|
594
628
|
"horizons": tally.horizon_report(),
|
|
629
|
+
"price_offsets": heads_rows["cost"].prices,
|
|
595
630
|
}
|
|
596
631
|
if full:
|
|
597
632
|
meta["full"] = _full_check(fitted)
|
|
@@ -55,6 +55,7 @@ FIXED_SD: dict[str, tuple[float, float, float]] = {
|
|
|
55
55
|
"round": (2.0, 2.0, 2.0),
|
|
56
56
|
"control": (1.0, 1.0, 1.0),
|
|
57
57
|
"horizon": (10.0, 10.0, 10.0), # wide: the informative prior is a FactorSpec in fit() (spec 04 section 1)
|
|
58
|
+
"price": (10.0, 10.0, 10.0), # wide: fit() holds `price:offset` at 1 by a FactorSpec (spec 04 section 2)
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
HYPER_SD = 0.7 # sd of the log-normal hyperprior on each phi, around the defaults above
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The price offset on the cost head (spec 04 section 2, 0.2.2, lane 22C).
|
|
2
|
+
|
|
3
|
+
A model's run cost moves with its list price. Every cost row of a model version `m` (not the tokens, gate,
|
|
4
|
+
success or score rows) carries the fixed term `price:offset` with the value
|
|
5
|
+
|
|
6
|
+
o_m = s_m * log(P_m / R_m), s_m = n0 / (n0 + n_m),
|
|
7
|
+
|
|
8
|
+
and the node's coefficient is held at 1 by a tight prior, so the term is a fixed offset on the log cost scale,
|
|
9
|
+
the same at fit and at prediction:
|
|
10
|
+
|
|
11
|
+
- `P_m` is m's blended list price ($/Mtok, the current price table) under the token mix (the shares of input,
|
|
12
|
+
cache read, cache write and output tokens) of the cost rows of m's family in the fit.
|
|
13
|
+
- `R_m` is the run-weighted geometric mean of the blended prices, under the same mix, of the family's models
|
|
14
|
+
with cost rows. A family with no cost rows anchors on its provider the same way (the provider's mix and
|
|
15
|
+
models). No anchor, or no price for m, gives no term.
|
|
16
|
+
- `n_m` is m's runs with a cost row in the fit; `PRICE_OFFSET_RUNS` is n0: the price counts like n0 of the
|
|
17
|
+
model's own runs, and its share of the prediction shrinks as they accumulate.
|
|
18
|
+
|
|
19
|
+
`fit_offsets` computes the offset of every model with cost rows and of every model in the price table once per
|
|
20
|
+
fit; `meta.json` `price_offsets` stores them, and prediction and the workflow search read them from there.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
from collections.abc import Iterable, Mapping
|
|
27
|
+
|
|
28
|
+
from .forest import canonical_model_id, model_path
|
|
29
|
+
|
|
30
|
+
PRICE_NODE = "price:offset"
|
|
31
|
+
PRICE_LEVEL = "price"
|
|
32
|
+
# n0: the list price counts like this many of the model's own runs (as one benchmark result counts like
|
|
33
|
+
# BENCHMARK_PRIOR_WEIGHT = 5 runs). A heuristic, to tune when stores have new versions with a few runs.
|
|
34
|
+
PRICE_OFFSET_RUNS = 5.0
|
|
35
|
+
PRICE_PRIOR_SD = 1e-3 # the coefficient's prior N(1, sd^2): a fixed offset, not a learned elasticity
|
|
36
|
+
STREAMS = ("input", "cache_read", "cache_write", "output") # price table rate keys, in the order of `streams`
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mix_of(totals: Iterable[float]) -> tuple[float, ...] | None:
|
|
40
|
+
"""Token shares per stream (STREAMS order), or None without tokens."""
|
|
41
|
+
t = [max(0.0, float(x)) for x in totals]
|
|
42
|
+
s = sum(t)
|
|
43
|
+
return tuple(x / s for x in t) if s > 0 else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def blended(rate: Mapping[str, float] | None, mix: tuple[float, ...] | None) -> float | None:
|
|
47
|
+
"""$/Mtok under `mix`, or None when the model has no rate, no mix, or a blended price of 0 (free)."""
|
|
48
|
+
if rate is None or mix is None:
|
|
49
|
+
return None
|
|
50
|
+
p = sum(share * float(rate.get(stream) or 0.0) for share, stream in zip(mix, STREAMS))
|
|
51
|
+
return p if p > 0 else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def shrink(runs: float, n0: float = PRICE_OFFSET_RUNS) -> float:
|
|
55
|
+
return n0 / (n0 + max(0.0, float(runs)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _add(acc: dict[str, list[float]], key: str, values: Iterable[float]) -> None:
|
|
59
|
+
cur = acc.setdefault(key, [0.0] * len(STREAMS))
|
|
60
|
+
for i, v in enumerate(values):
|
|
61
|
+
cur[i] += float(v)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fit_offsets(runs: Mapping[str, int], streams: Mapping[str, Iterable[float]], table, *,
|
|
65
|
+
n0: float = PRICE_OFFSET_RUNS) -> dict:
|
|
66
|
+
"""The fit's `price_offsets`: {n0, models: {model: offset}, detail, anchors}.
|
|
67
|
+
|
|
68
|
+
`runs`: model (canonical id) to its runs with a cost row; `streams`: model to its token totals over those
|
|
69
|
+
rows (STREAMS order); `table`: a price.PriceTable, or None (no offsets). Offsets of 0 are left out of
|
|
70
|
+
`models`; `detail` says for each priced model which anchor it used, or why it has none."""
|
|
71
|
+
out: dict = {"n0": n0, "models": {}, "detail": {}, "anchors": {}}
|
|
72
|
+
if table is None:
|
|
73
|
+
return out
|
|
74
|
+
fam_tok: dict[str, list[float]] = {}
|
|
75
|
+
prov_tok: dict[str, list[float]] = {}
|
|
76
|
+
fam_models: dict[str, dict[str, int]] = {}
|
|
77
|
+
prov_models: dict[str, dict[str, int]] = {}
|
|
78
|
+
for model, n in sorted(runs.items()):
|
|
79
|
+
if n <= 0:
|
|
80
|
+
continue
|
|
81
|
+
prov, fam, _ = model_path(model)
|
|
82
|
+
fam_models.setdefault(fam, {})[model] = int(n)
|
|
83
|
+
prov_models.setdefault(prov, {})[model] = int(n)
|
|
84
|
+
values = list(streams.get(model) or [0.0] * len(STREAMS))
|
|
85
|
+
_add(fam_tok, fam, values)
|
|
86
|
+
_add(prov_tok, prov, values)
|
|
87
|
+
candidates = set(runs) | {canonical_model_id(k) for k in table.rates}
|
|
88
|
+
anchors: dict[str, dict | None] = {}
|
|
89
|
+
|
|
90
|
+
def anchor(key: str, members: dict[str, int], tokens: list[float] | None) -> dict | None:
|
|
91
|
+
if key in anchors:
|
|
92
|
+
return anchors[key]
|
|
93
|
+
mix = mix_of(tokens or [])
|
|
94
|
+
priced = {m: (n, blended(table.rate(m), mix)) for m, n in members.items()}
|
|
95
|
+
priced = {m: (n, p) for m, (n, p) in priced.items() if p is not None}
|
|
96
|
+
found = None
|
|
97
|
+
if mix is not None and priced:
|
|
98
|
+
total = sum(n for n, _ in priced.values())
|
|
99
|
+
log_ref = sum(n * math.log(p) for n, p in priced.values()) / total
|
|
100
|
+
found = {"mix": mix, "log_ref": log_ref,
|
|
101
|
+
"reference_usd_per_mtok": round(math.exp(log_ref), 6),
|
|
102
|
+
"models": {m: n for m, (n, _) in sorted(priced.items())},
|
|
103
|
+
"todo": sorted(m for m in priced if table.is_todo(m))}
|
|
104
|
+
anchors[key] = found
|
|
105
|
+
return found
|
|
106
|
+
|
|
107
|
+
for model in sorted(candidates):
|
|
108
|
+
prov, fam, _ = model_path(model)
|
|
109
|
+
a, key = None, None
|
|
110
|
+
if fam in fam_models:
|
|
111
|
+
key = f"family:{fam}"
|
|
112
|
+
a = anchor(key, fam_models[fam], fam_tok.get(fam))
|
|
113
|
+
if a is None and prov in prov_models and fam not in fam_models:
|
|
114
|
+
key = f"provider:{prov}"
|
|
115
|
+
a = anchor(key, prov_models[prov], prov_tok.get(prov))
|
|
116
|
+
if a is None:
|
|
117
|
+
out["detail"][model] = {"runs": int(runs.get(model, 0)), "note": "no anchor with runs and prices"}
|
|
118
|
+
continue
|
|
119
|
+
p = blended(table.rate(model), a["mix"])
|
|
120
|
+
if p is None:
|
|
121
|
+
out["detail"][model] = {"runs": int(runs.get(model, 0)), "anchor": key, "note": "no price"}
|
|
122
|
+
continue
|
|
123
|
+
n = int(runs.get(model, 0))
|
|
124
|
+
s = shrink(n, n0)
|
|
125
|
+
offset = s * (math.log(p) - a["log_ref"])
|
|
126
|
+
out["detail"][model] = {"runs": n, "anchor": key, "usd_per_mtok": round(p, 6), "shrink": round(s, 6),
|
|
127
|
+
"log_ratio": round(math.log(p) - a["log_ref"], 6), "offset": round(offset, 6),
|
|
128
|
+
**({"todo": True} if table.is_todo(model) else {})}
|
|
129
|
+
if offset != 0.0:
|
|
130
|
+
out["models"][model] = offset
|
|
131
|
+
out["anchors"] = {k: {**{kk: vv for kk, vv in v.items() if kk != "log_ref"}, "mix": [round(x, 6) for x in v["mix"]]}
|
|
132
|
+
for k, v in sorted(anchors.items()) if v}
|
|
133
|
+
return out
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def price_term(offsets: Mapping[str, float] | None, model: str) -> tuple[tuple[str, None, float], ...]:
|
|
137
|
+
"""The cost row's price term for `model` (a canonical id) under a fit's offsets: none for an offset of 0 or
|
|
138
|
+
a fit without offsets."""
|
|
139
|
+
if not offsets:
|
|
140
|
+
return ()
|
|
141
|
+
o = offsets.get(model)
|
|
142
|
+
return ((PRICE_NODE, None, float(o)),) if o else ()
|
|
@@ -67,7 +67,7 @@ MIN_SCORE_SWITCH = 5 # spec 04 section 2: g = p_reach once the score head has 5
|
|
|
67
67
|
BASE_HEADS = {"cost": ("gaussian", "cost"), "tokens": ("gaussian", "cost"), "gate": ("logistic", "logit"),
|
|
68
68
|
"success": ("logistic", "logit")}
|
|
69
69
|
TASK_CHAIN = ("task", "subtype", "repo", "type")
|
|
70
|
-
FIXED_LEVELS = ("fixed", "round", "control")
|
|
70
|
+
FIXED_LEVELS = ("fixed", "round", "control", "price")
|
|
71
71
|
SCORE_CLAMP = (0.005, 0.995)
|
|
72
72
|
_CACHE_LIMIT = 200_000
|
|
73
73
|
MEAN_NODES = 12 # Gauss-Hermite nodes over an unseen normal effect
|
|
@@ -339,6 +339,8 @@ class FitState:
|
|
|
339
339
|
self.sim_seed = _seed(self.seed_key, "simulate")
|
|
340
340
|
# spec 04 section 1: the value of the timebox-capped terms (effort, shape) in a timeboxed task's cost rows
|
|
341
341
|
self.timebox_effort = float(self.meta.get("timebox_effort", 1.0))
|
|
342
|
+
# spec 04 section 2: the fit's price offset per model (log scale); none in fits before 0.2.2
|
|
343
|
+
self.price_offsets: dict[str, float] = dict((self.meta.get("price_offsets") or {}).get("models") or {})
|
|
342
344
|
if heads is None:
|
|
343
345
|
heads = {}
|
|
344
346
|
for name, hmeta in (self.meta.get("heads") or {}).items():
|
|
@@ -418,7 +420,8 @@ class FitState:
|
|
|
418
420
|
for piece in st.pieces:
|
|
419
421
|
rounds = st.k_max if st.piece_loop.get(piece) is not None else 1
|
|
420
422
|
for k in range(1, rounds + 1):
|
|
421
|
-
cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort
|
|
423
|
+
cost[(piece, k)] = cost_rest(st, piece, k, source=PREDICT_SOURCE, effort=effort,
|
|
424
|
+
prices=self.price_offsets)
|
|
422
425
|
gate = {}
|
|
423
426
|
for gi, g in enumerate(st.gates):
|
|
424
427
|
for k in range(1, (st.k_max if st.gate_loop.get(gi) is not None else 1) + 1):
|
|
@@ -21,7 +21,8 @@ from typing import Any
|
|
|
21
21
|
from ..output import EXIT_NOT_FOUND, EXIT_OK, fail, home as home_dir
|
|
22
22
|
from ..recommend import commands as C
|
|
23
23
|
from ..recommend import engine, storeread
|
|
24
|
-
from ..recommend.
|
|
24
|
+
from ..recommend.curve import accepted_within
|
|
25
|
+
from ..recommend.engine import Recommendation, r6
|
|
25
26
|
from ..recommend.storeread import Conf
|
|
26
27
|
from ..types import AcceptanceRule, Configuration, Task
|
|
27
28
|
|
|
@@ -45,6 +46,8 @@ class Session:
|
|
|
45
46
|
cache: OrderedDict = field(default_factory=OrderedDict) # config id -> predict answer
|
|
46
47
|
context: dict[str, Any] | None = None
|
|
47
48
|
models: dict[str, int] | None = None # `known_models`, read once
|
|
49
|
+
home: Any = None # the store, for the runs behind each model
|
|
50
|
+
offered: list[str] = field(default_factory=list) # `commands.offered_models`: the models the page offers
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
# ---------------------------------------------------------------- start
|
|
@@ -70,6 +73,13 @@ def asked_task(task: Task, id_given: bool, belief: Any, rule: AcceptanceRule, co
|
|
|
70
73
|
return C.question_task(task, key_of(belief) if callable(key_of) else belief.fit_id, rule, configs)
|
|
71
74
|
|
|
72
75
|
|
|
76
|
+
def offered_kw(models: list[str]) -> dict[str, Any]:
|
|
77
|
+
"""`offered=` for `engine.recommend` when it takes it (0.2.2), so the builder asks what `recommend` asks."""
|
|
78
|
+
import inspect
|
|
79
|
+
|
|
80
|
+
return {"offered": models} if "offered" in inspect.signature(engine.recommend).parameters else {}
|
|
81
|
+
|
|
82
|
+
|
|
73
83
|
def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
|
|
74
84
|
"""The session, or None and the exit code after the message `recommend` would print."""
|
|
75
85
|
home = home_dir(getattr(args, "home", None))
|
|
@@ -98,6 +108,7 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
|
|
|
98
108
|
spend, cap = C.budget(home, conf)
|
|
99
109
|
settings = C.settings_from(conf, args, spend, cap)
|
|
100
110
|
models = C.model_list(getattr(args, "models", None))
|
|
111
|
+
offered = C.offered_models(conf, models)
|
|
101
112
|
except C.NotFound as exc:
|
|
102
113
|
return None, fail(str(exc), EXIT_NOT_FOUND)
|
|
103
114
|
except ValueError as exc:
|
|
@@ -125,12 +136,12 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
|
|
|
125
136
|
asked = asked_task(task, id_given, belief, rule, [usual, *(c for c, _ in configs)])
|
|
126
137
|
rec = engine.recommend(belief, asked, rule, usual=usual, usual_from=usual_from, configs=configs,
|
|
127
138
|
settings=settings, diff=C.diff_fn(), keep=[*(u.id for u in user),
|
|
128
|
-
*(r.id for r in recorded)]
|
|
139
|
+
*(r.id for r in recorded)],
|
|
140
|
+
**offered_kw(offered))
|
|
129
141
|
rec.task = task
|
|
130
142
|
start = None
|
|
131
143
|
if getattr(args, "start", None):
|
|
132
|
-
|
|
133
|
-
start = found.config if found is not None else storeread.find_config(home, args.start)
|
|
144
|
+
start = rec_config(rec, args.start) or storeread.find_config(home, args.start)
|
|
134
145
|
if start is None:
|
|
135
146
|
raise C.NotFound(f"configuration {args.start} not found in this recommendation, earlier "
|
|
136
147
|
f"recommendations or stored runs")
|
|
@@ -139,7 +150,20 @@ def build_session(args: argparse.Namespace) -> tuple[Session | None, int]:
|
|
|
139
150
|
except ValueError as exc:
|
|
140
151
|
return None, fail(str(exc))
|
|
141
152
|
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
|
|
153
|
+
return Session(belief, asked, rule, rec, fit, rec_id=getattr(args, "rec", None), start=start,
|
|
154
|
+
home=home, offered=offered), EXIT_OK
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def rec_config(rec: Recommendation, cfg_id: str) -> Configuration | None:
|
|
158
|
+
"""A configuration of the recommendation by id: a candidate (every choice is one), the reference or the
|
|
159
|
+
rescue workflow."""
|
|
160
|
+
found = rec.by_id(cfg_id)
|
|
161
|
+
if found is not None:
|
|
162
|
+
return found.config
|
|
163
|
+
for cfg in (rec.usual.config, rec.rescue_config):
|
|
164
|
+
if cfg is not None and cfg.id == cfg_id:
|
|
165
|
+
return cfg
|
|
166
|
+
return None
|
|
143
167
|
|
|
144
168
|
|
|
145
169
|
# ---------------------------------------------------------------- catalog
|
|
@@ -161,32 +185,93 @@ def rec_configs(rec: Recommendation) -> list[Configuration]:
|
|
|
161
185
|
|
|
162
186
|
|
|
163
187
|
def known_models(session: Session) -> dict[str, int]:
|
|
164
|
-
"""Model id -> runs
|
|
188
|
+
"""Model id -> runs the fit has behind it, for the offered models (0.2.2: `recommend`'s list, so a retired
|
|
189
|
+
model is not offered), in offered order; without a list, the fit's models and any a candidate uses."""
|
|
165
190
|
if session.models is None:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
for
|
|
169
|
-
|
|
191
|
+
fit = fit_models(session.belief)
|
|
192
|
+
if session.offered:
|
|
193
|
+
out = {m: fit.get(m, 0) for m in session.offered}
|
|
194
|
+
else:
|
|
195
|
+
out = fit
|
|
196
|
+
for cfg in rec_configs(session.rec):
|
|
197
|
+
for s in cfg.settings.values():
|
|
198
|
+
out.setdefault(s.model, 0)
|
|
170
199
|
session.models = out
|
|
171
200
|
return session.models
|
|
172
201
|
|
|
173
202
|
|
|
203
|
+
def retired_ids(rec: Recommendation) -> dict[str, list[str]]:
|
|
204
|
+
return dict(getattr(rec, "retired", None) or {})
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def recorded_runs(home: Any) -> list[tuple[Configuration, int]]:
|
|
208
|
+
"""Every configuration the store's runs used, with its number of runs (one run document read per
|
|
209
|
+
configuration, as `storeread.recorded_configs` reads them)."""
|
|
210
|
+
from collections import Counter
|
|
211
|
+
|
|
212
|
+
if home is None:
|
|
213
|
+
return []
|
|
214
|
+
counts: Counter = Counter()
|
|
215
|
+
run_of: dict[str, str] = {}
|
|
216
|
+
for row in storeread.run_rows(home):
|
|
217
|
+
cfg = row.get("config")
|
|
218
|
+
if cfg:
|
|
219
|
+
counts[str(cfg)] += 1
|
|
220
|
+
if row.get("run"):
|
|
221
|
+
run_of[str(cfg)] = str(row["run"])
|
|
222
|
+
out = []
|
|
223
|
+
for cfg_id, n in counts.items():
|
|
224
|
+
doc = storeread.read_json(home / "runs" / f"{run_of[cfg_id]}.ocp.json") if cfg_id in run_of else None
|
|
225
|
+
conf = doc.get("run", {}).get("configuration") if isinstance(doc, dict) else None
|
|
226
|
+
cfg = storeread.config_from_any(conf) if conf else None
|
|
227
|
+
if cfg is not None:
|
|
228
|
+
out.append((cfg, n))
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def runs_behind(home: Any) -> tuple[dict[str, dict[str, Any]], dict[str, int]]:
|
|
233
|
+
"""(by model: `{total, by_role, by_effort}`, by role: runs) from the user's recorded runs. A run counts
|
|
234
|
+
once per model, once per (model, role), once per (model, role, effort) and once per role."""
|
|
235
|
+
models: dict[str, dict[str, Any]] = {}
|
|
236
|
+
roles: dict[str, int] = {}
|
|
237
|
+
for cfg, n in recorded_runs(home):
|
|
238
|
+
used: set[tuple[str, str, str]] = set()
|
|
239
|
+
for p in cfg.workflow.pieces:
|
|
240
|
+
s = cfg.settings.get(p.id)
|
|
241
|
+
if s is not None and s.model:
|
|
242
|
+
used.add((s.model, p.role, s.effort or ""))
|
|
243
|
+
for role in {p.role for p in cfg.workflow.pieces}:
|
|
244
|
+
roles[role] = roles.get(role, 0) + n
|
|
245
|
+
for model in {m for m, _, _ in used}:
|
|
246
|
+
m = models.setdefault(model, {"total": 0, "by_role": {}, "by_effort": {}})
|
|
247
|
+
m["total"] += n
|
|
248
|
+
for role in {r for mm, r, _ in used if mm == model}:
|
|
249
|
+
m["by_role"][role] = m["by_role"].get(role, 0) + n
|
|
250
|
+
for _, role, effort in (u for u in used if u[0] == model):
|
|
251
|
+
e = m["by_effort"].setdefault(role, {})
|
|
252
|
+
e[effort] = e.get(effort, 0) + n
|
|
253
|
+
return models, roles
|
|
254
|
+
|
|
255
|
+
|
|
174
256
|
def catalog(session: Session) -> dict[str, Any]:
|
|
175
257
|
from ..workflows.format import KNOWN_ROLES, catalog as shapes
|
|
176
258
|
from ..workflows.models import DEFAULT_EFFORTS, efforts_for, family_of, harness_for, sort_efforts
|
|
177
259
|
|
|
178
260
|
seen = [s for cfg in rec_configs(session.rec) for s in cfg.settings.values()]
|
|
179
261
|
harnesses = list(dict.fromkeys([*DEFAULT_EFFORTS, *(s.harness for s in seen)]))
|
|
180
|
-
|
|
262
|
+
behind, role_runs = runs_behind(session.home)
|
|
263
|
+
empty = {"total": 0, "by_role": {}, "by_effort": {}}
|
|
264
|
+
order = {m: i for i, m in enumerate(known_models(session))}
|
|
265
|
+
models = sorted(order, key=lambda m: (-behind.get(m, empty)["total"], order[m]))
|
|
181
266
|
by_harness = {h: list(efforts_for(h)) for h in harnesses}
|
|
182
267
|
efforts = sort_efforts([e for v in by_harness.values() for e in v] + [s.effort for s in seen])
|
|
183
268
|
return {
|
|
184
269
|
"harnesses": harnesses,
|
|
185
|
-
"models": [{"id": m, "family": family_of(m), "harness": harness_for(m),
|
|
186
|
-
|
|
270
|
+
"models": [{"id": m, "family": family_of(m), "harness": harness_for(m),
|
|
271
|
+
"runs_behind": behind.get(m, empty)} for m in models],
|
|
187
272
|
"efforts": list(efforts),
|
|
188
273
|
"efforts_by_harness": by_harness,
|
|
189
|
-
"roles":
|
|
274
|
+
"roles": [{"id": r, "runs": role_runs.get(r, 0)} for r in dict.fromkeys([*KNOWN_ROLES, *role_runs])],
|
|
190
275
|
"shapes": [{"id": wf.id, "title": wf.title,
|
|
191
276
|
"pieces": [{"id": p.id, "role": p.role, "width": p.width} for p in wf.pieces],
|
|
192
277
|
"edges": [list(e) for e in wf.edges],
|
|
@@ -196,8 +281,27 @@ def catalog(session: Session) -> dict[str, Any]:
|
|
|
196
281
|
|
|
197
282
|
|
|
198
283
|
# ---------------------------------------------------------------- the context
|
|
284
|
+
def within_bands(bands: dict[str, Any], rescue: Any) -> dict[str, Any]:
|
|
285
|
+
"""`bands` with `p_accepted_within`, mapped end by end from the chance band (`g + (1 - g) p_fix` is
|
|
286
|
+
increasing in `g`, as `Recommendation.within` reads its interval)."""
|
|
287
|
+
chance = bands.get("chance") if isinstance(bands, dict) else None
|
|
288
|
+
if not isinstance(chance, dict):
|
|
289
|
+
return bands
|
|
290
|
+
|
|
291
|
+
def f(g: Any) -> float | None:
|
|
292
|
+
return None if g is None else r6(accepted_within(float(g), rescue))
|
|
293
|
+
|
|
294
|
+
return {**bands, "p_accepted_within": {lv: [f(v[0]), f(v[1])] for lv, v in chance.items()}}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def numbers_of(rec: Recommendation, pred: Any) -> dict[str, Any]:
|
|
298
|
+
"""`Recommendation.numbers()` with the `p_accepted_within` band."""
|
|
299
|
+
n = rec.numbers(pred)
|
|
300
|
+
return {**n, "bands": within_bands(n["bands"], rec.rescue)}
|
|
301
|
+
|
|
302
|
+
|
|
199
303
|
def candidate_entry(rec: Recommendation, c: Any) -> dict[str, Any]:
|
|
200
|
-
return {"config": c.config.to_dict(), "label": rec.label(c.config), "numbers": rec
|
|
304
|
+
return {"config": c.config.to_dict(), "label": rec.label(c.config), "numbers": numbers_of(rec, c.prediction),
|
|
201
305
|
"origin": c.origin, **rec.search_fields(c.config.id)}
|
|
202
306
|
|
|
203
307
|
|
|
@@ -210,12 +314,20 @@ def context_payload(session: Session) -> dict[str, Any]:
|
|
|
210
314
|
choices = []
|
|
211
315
|
for ch in core["choices"]:
|
|
212
316
|
c = rec.by_id(ch["config"])
|
|
213
|
-
choices.append({**ch, "
|
|
317
|
+
choices.append({**ch, "bands": within_bands(ch["bands"], rec.rescue),
|
|
318
|
+
"configuration": c.config.to_dict() if c is not None else None})
|
|
214
319
|
reference = {k: v for k, v in core["reference"].items() if k != "prediction"}
|
|
215
|
-
|
|
320
|
+
reference["numbers"] = numbers_of(rec, rec.usual.prediction)
|
|
321
|
+
retired = retired_ids(rec) # 0.2.2: a workflow on a retired model is the reference at most, never a candidate
|
|
322
|
+
offer = [c for c in rec.candidates if c.config.id not in retired]
|
|
323
|
+
top = offer[:TOP_CANDIDATES]
|
|
216
324
|
ids = {c.config.id for c in top}
|
|
217
|
-
|
|
218
|
-
|
|
325
|
+
# every option's workflows (the pair's exploration one too) and the rescue workflow, whatever the cutoff (22R B3)
|
|
326
|
+
wanted = {rec.usual.config.id, *(i for ch in core["choices"]
|
|
327
|
+
for i in (ch["config"], *ch.get("members", ()), ch.get("explore_config")) if i)}
|
|
328
|
+
if rec.rescue_config is not None:
|
|
329
|
+
wanted.add(rec.rescue_config.id)
|
|
330
|
+
top += [c for c in offer[TOP_CANDIDATES:]
|
|
219
331
|
if c.config.id not in ids and (c.origin in KEEP_ORIGINS or c.config.id in wanted)]
|
|
220
332
|
session.context = {
|
|
221
333
|
"schema": SCHEMA,
|
|
@@ -223,6 +335,7 @@ def context_payload(session: Session) -> dict[str, Any]:
|
|
|
223
335
|
"rule": core["rule"],
|
|
224
336
|
"fit": session.fit,
|
|
225
337
|
"rec": session.rec_id,
|
|
338
|
+
"goal_config_id": rec.goal.config.id,
|
|
226
339
|
"rescue": core["rescue"],
|
|
227
340
|
"reference": reference,
|
|
228
341
|
"choices": choices,
|