agentevolve-optimizer 0.5.0__py3-none-any.whl
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.
- agent_evolve/__init__.py +722 -0
- agent_evolve/agentic.py +2800 -0
- agent_evolve/api.py +767 -0
- agent_evolve/application/__init__.py +1580 -0
- agent_evolve/application/action_allocation.py +744 -0
- agent_evolve/application/action_allocation_frame.py +347 -0
- agent_evolve/application/action_allocation_frame_commit.py +185 -0
- agent_evolve/application/action_allocation_frame_commit_v3.py +184 -0
- agent_evolve/application/action_allocation_frame_v3.py +338 -0
- agent_evolve/application/action_archive_value.py +497 -0
- agent_evolve/application/action_evidence_consistency.py +455 -0
- agent_evolve/application/action_forecast_partitioning.py +1471 -0
- agent_evolve/application/action_metric_projection.py +211 -0
- agent_evolve/application/action_role_value.py +680 -0
- agent_evolve/application/action_score_authorities.py +363 -0
- agent_evolve/application/action_structural_signature.py +116 -0
- agent_evolve/application/action_target_realization.py +402 -0
- agent_evolve/application/agentic_evolution.py +7734 -0
- agent_evolve/application/agentic_portfolio_residual_expert.py +835 -0
- agent_evolve/application/anchor_residual_identification.py +463 -0
- agent_evolve/application/archive_conditioned_action_target.py +208 -0
- agent_evolve/application/artifact_journal.py +246 -0
- agent_evolve/application/artifact_replay.py +347 -0
- agent_evolve/application/budgeted_optimizer.py +1828 -0
- agent_evolve/application/calibrated_campaign.py +485 -0
- agent_evolve/application/calibrated_current_prefix_forecast_opportunity.py +322 -0
- agent_evolve/application/calibrated_positive_gain_opportunity.py +1581 -0
- agent_evolve/application/campaign_capacity_recourse.py +254 -0
- agent_evolve/application/campaign_contextual_outcomes.py +119 -0
- agent_evolve/application/campaign_diagnostic_blocks.py +930 -0
- agent_evolve/application/campaign_evidence_registry.py +262 -0
- agent_evolve/application/campaign_execution.py +2537 -0
- agent_evolve/application/campaign_generation_audit.py +942 -0
- agent_evolve/application/campaign_learning.py +1812 -0
- agent_evolve/application/campaign_learning_runtime.py +1977 -0
- agent_evolve/application/campaign_search_phase.py +227 -0
- agent_evolve/application/campaign_selector_context_extension.py +220 -0
- agent_evolve/application/campaign_variation_envelope.py +649 -0
- agent_evolve/application/campaign_variation_trace.py +451 -0
- agent_evolve/application/candidate_archive_consequence.py +128 -0
- agent_evolve/application/causal_opportunity_portfolio_gate.py +385 -0
- agent_evolve/application/composite_outcome_updater.py +145 -0
- agent_evolve/application/composition_portfolio_selection.py +363 -0
- agent_evolve/application/concurrent_stage.py +144 -0
- agent_evolve/application/contextual_action_allocation.py +181 -0
- agent_evolve/application/contextual_campaign_outcomes.py +267 -0
- agent_evolve/application/contextual_campaign_planning.py +1366 -0
- agent_evolve/application/contextual_delayed_credit.py +651 -0
- agent_evolve/application/contextual_search_controller.py +2374 -0
- agent_evolve/application/current_prefix_forecast_opportunity.py +714 -0
- agent_evolve/application/decision_metric_projection.py +112 -0
- agent_evolve/application/derived_action_semantics.py +129 -0
- agent_evolve/application/detailed_evaluation.py +449 -0
- agent_evolve/application/earned_lineage.py +1011 -0
- agent_evolve/application/effective_choice_audit.py +484 -0
- agent_evolve/application/empirical_consequence_calibration.py +908 -0
- agent_evolve/application/evaluation_accounting.py +325 -0
- agent_evolve/application/evaluation_cache.py +199 -0
- agent_evolve/application/evaluation_escrow.py +547 -0
- agent_evolve/application/evaluation_recourse.py +253 -0
- agent_evolve/application/event_recorder.py +151 -0
- agent_evolve/application/evolution_campaign.py +1840 -0
- agent_evolve/application/executable_hypothesis.py +323 -0
- agent_evolve/application/factorial_branch_pilot.py +772 -0
- agent_evolve/application/finite_acquisition_capacity_recourse.py +672 -0
- agent_evolve/application/finite_acquisition_residual_expert.py +373 -0
- agent_evolve/application/finite_acquisition_variation_envelope.py +802 -0
- agent_evolve/application/finite_action_hypothesis_semantics.py +446 -0
- agent_evolve/application/finite_action_selection.py +188 -0
- agent_evolve/application/finite_action_set.py +306 -0
- agent_evolve/application/finite_action_transition.py +537 -0
- agent_evolve/application/finite_variation_eligibility.py +296 -0
- agent_evolve/application/forecast_geometry_portfolio.py +799 -0
- agent_evolve/application/forecast_opportunity_shadow_calibration.py +316 -0
- agent_evolve/application/front_proximity_admission.py +311 -0
- agent_evolve/application/front_proximity_parent_basis.py +458 -0
- agent_evolve/application/frozen_hurdle_score.py +659 -0
- agent_evolve/application/g3_causal_screen.py +2257 -0
- agent_evolve/application/g3_causal_validation.py +1046 -0
- agent_evolve/application/g3_postseal_curation.py +818 -0
- agent_evolve/application/gated_agentic_generator.py +205 -0
- agent_evolve/application/generation_feedback.py +293 -0
- agent_evolve/application/generative_proposal_journal.py +185 -0
- agent_evolve/application/geometry_conditional_elasticity.py +453 -0
- agent_evolve/application/global_wave_action_allocation.py +1151 -0
- agent_evolve/application/head_mass_conditional_seat.py +268 -0
- agent_evolve/application/identifiable_reflection_evidence.py +1147 -0
- agent_evolve/application/identifiable_reflection_learning.py +395 -0
- agent_evolve/application/identifiable_reflection_request.py +364 -0
- agent_evolve/application/in_memory_residual_archive.py +341 -0
- agent_evolve/application/insight_memory.py +1804 -0
- agent_evolve/application/live_runtime_manifest.py +758 -0
- agent_evolve/application/llm_task_queue.py +769 -0
- agent_evolve/application/matched_finite_action_block.py +409 -0
- agent_evolve/application/materialized_action_broker.py +2328 -0
- agent_evolve/application/materialized_action_constraints.py +83 -0
- agent_evolve/application/materialized_variation.py +211 -0
- agent_evolve/application/multi_option_evolution.py +1536 -0
- agent_evolve/application/outcome_adaptive_action_racing.py +2827 -0
- agent_evolve/application/outcome_adaptive_residual_campaign_runtime.py +580 -0
- agent_evolve/application/outcome_adaptive_residual_portfolio_evolution.py +3671 -0
- agent_evolve/application/outcome_conditioned_portfolio_selection.py +1374 -0
- agent_evolve/application/outcome_relation.py +193 -0
- agent_evolve/application/paired_allocation_comparison.py +241 -0
- agent_evolve/application/paired_block_schedule.py +127 -0
- agent_evolve/application/parent_measurement.py +226 -0
- agent_evolve/application/pareto_archive.py +811 -0
- agent_evolve/application/portfolio_campaign_runtime.py +4739 -0
- agent_evolve/application/portfolio_evolution.py +2950 -0
- agent_evolve/application/portfolio_hypothesis_observations.py +814 -0
- agent_evolve/application/portfolio_memory_attribution.py +581 -0
- agent_evolve/application/portfolio_memory_dose.py +788 -0
- agent_evolve/application/portfolio_memory_matched_control.py +938 -0
- agent_evolve/application/portfolio_memory_transfer.py +297 -0
- agent_evolve/application/portfolio_optimization_memory.py +363 -0
- agent_evolve/application/portfolio_outcome_feedback.py +1613 -0
- agent_evolve/application/portfolio_projection.py +335 -0
- agent_evolve/application/portfolio_recombination.py +2032 -0
- agent_evolve/application/post_evolution_reflection.py +834 -0
- agent_evolve/application/postcommit_rank_authority.py +245 -0
- agent_evolve/application/precommitted_portfolio_racing.py +2762 -0
- agent_evolve/application/prequential_archive_opportunity_calibration.py +1154 -0
- agent_evolve/application/prequential_residual_exploration.py +343 -0
- agent_evolve/application/prequential_score_portfolio.py +954 -0
- agent_evolve/application/projections.py +292 -0
- agent_evolve/application/protected_action_committee.py +1027 -0
- agent_evolve/application/protected_branch_pilot.py +376 -0
- agent_evolve/application/protected_current_prefix_forecast_opportunity.py +552 -0
- agent_evolve/application/provider_replay.py +910 -0
- agent_evolve/application/rank_balanced_causal_pilot.py +1372 -0
- agent_evolve/application/recombination_residual_expert.py +403 -0
- agent_evolve/application/reflection_workflow.py +571 -0
- agent_evolve/application/region_conditional_credit.py +911 -0
- agent_evolve/application/residual_campaign_runtime.py +531 -0
- agent_evolve/application/residual_headroom_campaign_runtime.py +459 -0
- agent_evolve/application/residual_headroom_ledger.py +1544 -0
- agent_evolve/application/residual_learning_transaction.py +396 -0
- agent_evolve/application/residual_portfolio_evolution.py +1228 -0
- agent_evolve/application/residual_reachability.py +749 -0
- agent_evolve/application/residual_stage_credit.py +499 -0
- agent_evolve/application/same_prefix_paired_audit.py +1580 -0
- agent_evolve/application/semantic_coverage_score_portfolio.py +838 -0
- agent_evolve/application/sequential_lineage_allocation.py +1017 -0
- agent_evolve/application/sequential_market_replay.py +1395 -0
- agent_evolve/application/sequential_residual_campaign_runtime.py +305 -0
- agent_evolve/application/sequential_residual_portfolio_evolution.py +940 -0
- agent_evolve/application/single_score_action_allocation.py +299 -0
- agent_evolve/application/source_exposure_allocation.py +906 -0
- agent_evolve/application/staged_memory.py +210 -0
- agent_evolve/application/stratified_cold_start_allocation.py +732 -0
- agent_evolve/application/support_guarded_hurdle_score.py +549 -0
- agent_evolve/application/target_conditioned_action_forecast.py +595 -0
- agent_evolve/application/target_conditioned_campaign.py +566 -0
- agent_evolve/application/treatment_assignment.py +201 -0
- agent_evolve/application/trusted_objective_evidence.py +217 -0
- agent_evolve/application/two_stage_action_evolution.py +1131 -0
- agent_evolve/application/v8lite_allocation_policy.py +1083 -0
- agent_evolve/application/v9_candidate_policy.py +1303 -0
- agent_evolve/bootstrap.py +108 -0
- agent_evolve/campaign_presets.py +517 -0
- agent_evolve/campaign_profiles.py +452 -0
- agent_evolve/campaign_variation_topology.py +288 -0
- agent_evolve/campaign_workload.py +950 -0
- agent_evolve/cli.py +797 -0
- agent_evolve/contract.py +241 -0
- agent_evolve/core/__init__.py +91 -0
- agent_evolve/core/action_semantics.py +411 -0
- agent_evolve/core/authored.py +105 -0
- agent_evolve/core/formatting.py +286 -0
- agent_evolve/core/optimization_semantics.py +324 -0
- agent_evolve/core/problem.py +167 -0
- agent_evolve/core/results.py +323 -0
- agent_evolve/core/stats.py +70 -0
- agent_evolve/core/telemetry.py +100 -0
- agent_evolve/domain/__init__.py +89 -0
- agent_evolve/domain/artifact.py +162 -0
- agent_evolve/domain/durable_text.py +68 -0
- agent_evolve/domain/event.py +1454 -0
- agent_evolve/domain/finite_action_set.py +426 -0
- agent_evolve/domain/finite_variation.py +526 -0
- agent_evolve/domain/generative_emission.py +559 -0
- agent_evolve/domain/ids.py +163 -0
- agent_evolve/domain/inline_text.py +106 -0
- agent_evolve/domain/insight.py +27 -0
- agent_evolve/domain/lineage.py +737 -0
- agent_evolve/domain/llm_task_queue.py +960 -0
- agent_evolve/domain/outcome.py +96 -0
- agent_evolve/domain/patch.py +854 -0
- agent_evolve/domain/typed_json.py +542 -0
- agent_evolve/domain/variation_space.py +158 -0
- agent_evolve/driver.py +1014 -0
- agent_evolve/harness/__init__.py +29 -0
- agent_evolve/harness/base.py +242 -0
- agent_evolve/harness/directives.py +163 -0
- agent_evolve/harness/generative_seal.py +479 -0
- agent_evolve/harness/registry.py +41 -0
- agent_evolve/infrastructure/__init__.py +39 -0
- agent_evolve/infrastructure/artifacts/__init__.py +6 -0
- agent_evolve/infrastructure/artifacts/_verification.py +67 -0
- agent_evolve/infrastructure/artifacts/filesystem.py +343 -0
- agent_evolve/infrastructure/artifacts/in_memory.py +73 -0
- agent_evolve/infrastructure/asyncio_runtime.py +109 -0
- agent_evolve/infrastructure/authored_runtime.py +188 -0
- agent_evolve/infrastructure/authored_worker.py +171 -0
- agent_evolve/infrastructure/clock.py +53 -0
- agent_evolve/infrastructure/events/__init__.py +6 -0
- agent_evolve/infrastructure/events/_validation.py +89 -0
- agent_evolve/infrastructure/events/in_memory.py +56 -0
- agent_evolve/infrastructure/events/jsonl.py +193 -0
- agent_evolve/infrastructure/exception_provenance.py +215 -0
- agent_evolve/infrastructure/ids.py +118 -0
- agent_evolve/infrastructure/lineage_codec.py +1836 -0
- agent_evolve/infrastructure/outcome_adaptive_phase_journal.py +170 -0
- agent_evolve/infrastructure/residual_headroom_journal.py +221 -0
- agent_evolve/infrastructure/resource_lease.py +370 -0
- agent_evolve/infrastructure/sanitization/__init__.py +8 -0
- agent_evolve/infrastructure/sanitization/strict_json.py +484 -0
- agent_evolve/infrastructure/sequential_phase_journal.py +170 -0
- agent_evolve/infrastructure/stream_liveness.py +383 -0
- agent_evolve/infrastructure/subprocess_boundary.py +136 -0
- agent_evolve/integrations/__init__.py +1 -0
- agent_evolve/integrations/botorch/__init__.py +28 -0
- agent_evolve/integrations/botorch/finite_qlognehvi.py +190 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch.py +155 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_identity.py +20 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_batch_worker.py +55 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_identity.py +22 -0
- agent_evolve/integrations/botorch/finite_qlognehvi_worker.py +55 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi.py +261 -0
- agent_evolve/integrations/botorch/subprocess_qlognehvi_batch.py +273 -0
- agent_evolve/integrations/completion.py +242 -0
- agent_evolve/integrations/pydantic_ai/__init__.py +441 -0
- agent_evolve/integrations/pydantic_ai/action_forecast.py +1068 -0
- agent_evolve/integrations/pydantic_ai/agentic_generator.py +2308 -0
- agent_evolve/integrations/pydantic_ai/async_generator.py +1604 -0
- agent_evolve/integrations/pydantic_ai/boundary_codec.py +1526 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_campaign.py +756 -0
- agent_evolve/integrations/pydantic_ai/calibrated_portfolio_selection.py +7537 -0
- agent_evolve/integrations/pydantic_ai/campaign_acquisition.py +609 -0
- agent_evolve/integrations/pydantic_ai/execution_binding.py +138 -0
- agent_evolve/integrations/pydantic_ai/forecast_geometry_action_committee.py +217 -0
- agent_evolve/integrations/pydantic_ai/harness.py +159 -0
- agent_evolve/integrations/pydantic_ai/heterogeneous_model_execution.py +306 -0
- agent_evolve/integrations/pydantic_ai/hierarchical_residual_adaptive_semantic_view.py +179 -0
- agent_evolve/integrations/pydantic_ai/json_schema_dialect.py +108 -0
- agent_evolve/integrations/pydantic_ai/materialized_hierarchical_residual_expert.py +952 -0
- agent_evolve/integrations/pydantic_ai/materialized_portfolio_judge.py +520 -0
- agent_evolve/integrations/pydantic_ai/model_execution_profile.py +659 -0
- agent_evolve/integrations/pydantic_ai/outbound_request_manifest.py +1170 -0
- agent_evolve/integrations/pydantic_ai/portable_residual_consequence_features.py +575 -0
- agent_evolve/integrations/pydantic_ai/portfolio_selection.py +422 -0
- agent_evolve/integrations/pydantic_ai/progress_aware_openrouter.py +416 -0
- agent_evolve/integrations/pydantic_ai/provider_attempt_join.py +1523 -0
- agent_evolve/integrations/pydantic_ai/provider_free_calibrated_runner.py +607 -0
- agent_evolve/integrations/pydantic_ai/queued_runner.py +2634 -0
- agent_evolve/integrations/pydantic_ai/reconciled_residual_reachability.py +1417 -0
- agent_evolve/integrations/pydantic_ai/residual_forecast_geometry.py +445 -0
- agent_evolve/integrations/pydantic_ai/residual_reachability.py +674 -0
- agent_evolve/integrations/pydantic_ai/residual_semantic_cells.py +239 -0
- agent_evolve/integrations/pydantic_ai/sealed_output_replay.py +1068 -0
- agent_evolve/integrations/pydantic_ai/semantic_coverage_residual_portfolio.py +770 -0
- agent_evolve/integrations/pydantic_ai/semantic_decision_replay.py +383 -0
- agent_evolve/integrations/pydantic_ai/support_adaptive_residual_portfolio.py +135 -0
- agent_evolve/integrations/pydantic_ai/trusted_residual_prompt_context.py +143 -0
- agent_evolve/integrations/pydantic_ai/validated_openrouter_model.py +107 -0
- agent_evolve/integrations/pymoo_adapter.py +242 -0
- agent_evolve/policies/__init__.py +17 -0
- agent_evolve/policies/check.py +469 -0
- agent_evolve/policies/emit_scaffold.py +451 -0
- agent_evolve/policies/feedback/__init__.py +37 -0
- agent_evolve/policies/feedback/held_out_asn.py +1325 -0
- agent_evolve/policies/genetic.py +607 -0
- agent_evolve/policies/llm_backoff.py +183 -0
- agent_evolve/policies/llm_chooser.py +226 -0
- agent_evolve/policies/llm_generator.py +1760 -0
- agent_evolve/policies/llm_init.py +267 -0
- agent_evolve/policies/llm_operator.py +109 -0
- agent_evolve/policies/llm_prior.py +194 -0
- agent_evolve/policies/llm_surrogate.py +334 -0
- agent_evolve/policies/measurement_evidence.py +704 -0
- agent_evolve/policies/memory/__init__.py +223 -0
- agent_evolve/policies/memory/balanced_subset_blocks.py +707 -0
- agent_evolve/policies/memory/compatibility_matching.py +593 -0
- agent_evolve/policies/memory/global_falsification.py +1841 -0
- agent_evolve/policies/memory/prompt_shape.py +503 -0
- agent_evolve/policies/memory/randomized_subset.py +714 -0
- agent_evolve/policies/memory/staged_causal.py +1270 -0
- agent_evolve/policies/memory/treatment_compliance.py +759 -0
- agent_evolve/policies/objective_resolution/__init__.py +17 -0
- agent_evolve/policies/objective_resolution/fixed_grid.py +364 -0
- agent_evolve/policies/operator_portfolio.py +407 -0
- agent_evolve/policies/reguidance.py +1133 -0
- agent_evolve/policies/reward/__init__.py +83 -0
- agent_evolve/policies/reward/affine_candidate_consequence.py +156 -0
- agent_evolve/policies/reward/affine_candidate_consequence_3d.py +159 -0
- agent_evolve/policies/reward/affine_hypervolume.py +490 -0
- agent_evolve/policies/reward/affine_hypervolume_3d.py +567 -0
- agent_evolve/policies/reward/contextual_marginal_utility.py +318 -0
- agent_evolve/policies/reward/frozen_archive.py +360 -0
- agent_evolve/policies/reward/frozen_wave_archive.py +368 -0
- agent_evolve/policies/search_state.py +208 -0
- agent_evolve/policies/selection/__init__.py +345 -0
- agent_evolve/policies/selection/acquisition_certified_slate.py +684 -0
- agent_evolve/policies/selection/affine_frontier_context.py +330 -0
- agent_evolve/policies/selection/affine_frontier_target.py +473 -0
- agent_evolve/policies/selection/archive_elite.py +1346 -0
- agent_evolve/policies/selection/calibrated_portfolio_binding.py +640 -0
- agent_evolve/policies/selection/calibrated_slate.py +1394 -0
- agent_evolve/policies/selection/calibrated_slate_codec.py +579 -0
- agent_evolve/policies/selection/common_candidate_pool.py +685 -0
- agent_evolve/policies/selection/diagnostic_sampling.py +319 -0
- agent_evolve/policies/selection/disjoint_pairs.py +479 -0
- agent_evolve/policies/selection/elite_explorer.py +719 -0
- agent_evolve/policies/selection/finite_action.py +187 -0
- agent_evolve/policies/selection/finite_option_prompt_projection.py +377 -0
- agent_evolve/policies/selection/finite_palette_evidence.py +247 -0
- agent_evolve/policies/selection/forecast_calibration.py +922 -0
- agent_evolve/policies/selection/frontier_probe_slate.py +814 -0
- agent_evolve/policies/selection/frozen_archive_pairs.py +762 -0
- agent_evolve/policies/selection/full_support_slate.py +91 -0
- agent_evolve/policies/selection/meaningful_direction.py +240 -0
- agent_evolve/policies/selection/memory_dose_feasibility.py +259 -0
- agent_evolve/policies/selection/model_anchored_slate.py +826 -0
- agent_evolve/policies/selection/phenotype_recourse.py +979 -0
- agent_evolve/policies/selection/proposal_support.py +368 -0
- agent_evolve/policies/selection/random_portfolio.py +254 -0
- agent_evolve/policies/selection/regret_bounded_slate.py +1084 -0
- agent_evolve/policies/selection/residual_frontier.py +463 -0
- agent_evolve/policies/selection/residual_frontier_target.py +605 -0
- agent_evolve/policies/selection/structural_posterior_slate.py +1571 -0
- agent_evolve/policies/selection/target_conditioned_allocator.py +648 -0
- agent_evolve/policies/selection/target_conditioned_features.py +812 -0
- agent_evolve/policies/selection/target_conditioned_prequential.py +1527 -0
- agent_evolve/policies/selection/task_keyed_palette.py +906 -0
- agent_evolve/policies/semantics.py +147 -0
- agent_evolve/policies/structure.py +362 -0
- agent_evolve/policies/structured_output_budget.py +62 -0
- agent_evolve/policies/surrogate.py +696 -0
- agent_evolve/policies/variation/__init__.py +1 -0
- agent_evolve/policies/variation/compositional_finite_catalog.py +426 -0
- agent_evolve/policies/variation/crossover_inheritance.py +575 -0
- agent_evolve/policies/variation/disjoint_recombination.py +611 -0
- agent_evolve/policies/variation/exact_composition_capacity.py +214 -0
- agent_evolve/policies/variation/exact_parent_crossover.py +950 -0
- agent_evolve/policies/variation/multiscale_restart_catalog.py +372 -0
- agent_evolve/policies/variation/source_union_finite_catalog.py +403 -0
- agent_evolve/policies/variation/typed_patch.py +1981 -0
- agent_evolve/policies/weighted_prior.py +394 -0
- agent_evolve/ports/__init__.py +383 -0
- agent_evolve/ports/action_allocation.py +733 -0
- agent_evolve/ports/action_allocation_frame.py +1153 -0
- agent_evolve/ports/action_allocation_frame_commit.py +294 -0
- agent_evolve/ports/action_allocation_frame_commit_v3.py +432 -0
- agent_evolve/ports/action_allocation_frame_v3.py +995 -0
- agent_evolve/ports/action_forecast.py +1568 -0
- agent_evolve/ports/action_metric_projection.py +165 -0
- agent_evolve/ports/agentic_generator.py +1561 -0
- agent_evolve/ports/archive_context.py +136 -0
- agent_evolve/ports/artifact_sanitizer.py +44 -0
- agent_evolve/ports/artifact_store.py +225 -0
- agent_evolve/ports/clock.py +13 -0
- agent_evolve/ports/contextual_search_allocation.py +827 -0
- agent_evolve/ports/decision_metric_projection.py +258 -0
- agent_evolve/ports/event_store.py +55 -0
- agent_evolve/ports/executable_hypothesis.py +557 -0
- agent_evolve/ports/finite_acquisition.py +377 -0
- agent_evolve/ports/finite_acquisition_batch.py +296 -0
- agent_evolve/ports/finite_acquisition_batch_json.py +164 -0
- agent_evolve/ports/finite_acquisition_json.py +247 -0
- agent_evolve/ports/finite_acquisition_space.py +168 -0
- agent_evolve/ports/finite_action_selection.py +348 -0
- agent_evolve/ports/finite_action_set.py +256 -0
- agent_evolve/ports/frontier_target.py +396 -0
- agent_evolve/ports/generation_failure.py +43 -0
- agent_evolve/ports/hard_feasibility.py +233 -0
- agent_evolve/ports/id_factory.py +34 -0
- agent_evolve/ports/llm_task_queue.py +93 -0
- agent_evolve/ports/objective_resolution.py +419 -0
- agent_evolve/ports/paired_allocation_comparison.py +401 -0
- agent_evolve/ports/paired_block_schedule.py +475 -0
- agent_evolve/ports/parent_measurement.py +336 -0
- agent_evolve/ports/portfolio_memory_dose.py +643 -0
- agent_evolve/ports/portfolio_selection.py +3169 -0
- agent_evolve/ports/postcommit_rank_authority.py +467 -0
- agent_evolve/ports/presented_action_evidence.py +794 -0
- agent_evolve/ports/resource_lease.py +162 -0
- agent_evolve/ports/structured_generator.py +734 -0
- agent_evolve/ports/structured_output_budget.py +120 -0
- agent_evolve/ports/subprocess_boundary.py +138 -0
- agent_evolve/ports/treatment_assignment.py +466 -0
- agent_evolve/ports/variation_catalog.py +76 -0
- agent_evolve/ports/variation_source.py +226 -0
- agent_evolve/proposal_mode.py +157 -0
- agent_evolve/proposers/__init__.py +10 -0
- agent_evolve/proposers/random_proposer.py +188 -0
- agent_evolve/provider_accounting.py +163 -0
- agent_evolve/py.typed +0 -0
- agent_evolve/reference_method.py +1570 -0
- agent_evolve/session/__init__.py +11 -0
- agent_evolve/session/authorship.py +864 -0
- agent_evolve/session/evaluate.py +236 -0
- agent_evolve/session/fidelity.py +237 -0
- agent_evolve/session/genetic_loop.py +742 -0
- agent_evolve/session/loop.py +803 -0
- agent_evolve/session/screening.py +671 -0
- agent_evolve/settings.py +376 -0
- agent_evolve/workload_kit.py +368 -0
- agent_evolve/workload_prompt.py +398 -0
- agentevolve_optimizer-0.5.0.dist-info/METADATA +599 -0
- agentevolve_optimizer-0.5.0.dist-info/RECORD +414 -0
- agentevolve_optimizer-0.5.0.dist-info/WHEEL +5 -0
- agentevolve_optimizer-0.5.0.dist-info/entry_points.txt +2 -0
- agentevolve_optimizer-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentevolve_optimizer-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1581 @@
|
|
|
1
|
+
"""Calibrated positive-gain opportunity without adverse-scenario gating.
|
|
2
|
+
|
|
3
|
+
The forecast-opportunity challenger this module replaces required positive
|
|
4
|
+
gain in the ADVERSE scenario before it would recommend anything, so every
|
|
5
|
+
uncertain candidate abstained and uncertainty was mapped to zero acquisition
|
|
6
|
+
authority. This policy removes hard abstention: every eligible candidate
|
|
7
|
+
receives a defined score, which may be non-positive, and the horizon policy
|
|
8
|
+
decides exploit-versus-explore.
|
|
9
|
+
|
|
10
|
+
Evidence combination:
|
|
11
|
+
|
|
12
|
+
* forecast geometry — scenario quantile points are valued strictly against
|
|
13
|
+
the CURRENT ARCHIVE through an injected gain port, so parent-relative
|
|
14
|
+
improvement contributes nothing;
|
|
15
|
+
* prequential conversion evidence — per (engine, rank-band) conversion rates
|
|
16
|
+
from outcomes observed BEFORE the current decision, with hierarchical
|
|
17
|
+
Beta shrinkage cell -> engine -> global; and
|
|
18
|
+
* a configurable mixture of the two (log-odds for probabilities, arithmetic
|
|
19
|
+
for magnitudes).
|
|
20
|
+
|
|
21
|
+
``score = p_positive_archive_gain * expected_positive_gain
|
|
22
|
+
- lambda_ * tail_risk
|
|
23
|
+
+ beta * (future_seats_remaining / horizon_total)
|
|
24
|
+
* value_of_information``
|
|
25
|
+
|
|
26
|
+
Value of information is exactly zero when no future seat remains, preserving
|
|
27
|
+
the V7 terminal semantics that a terminal seat never purchases information.
|
|
28
|
+
The policy knows no workload, objective name, model, provider, or prompt.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import math
|
|
36
|
+
import re
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from typing import Protocol, runtime_checkable
|
|
39
|
+
|
|
40
|
+
from agent_evolve.application.rank_balanced_causal_pilot import (
|
|
41
|
+
rank_band_index,
|
|
42
|
+
)
|
|
43
|
+
from agent_evolve.domain.patch import require_sha256
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_ID = (
|
|
47
|
+
"calibrated_positive_gain_opportunity"
|
|
48
|
+
)
|
|
49
|
+
CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_VERSION = 1
|
|
50
|
+
LOWER_QUANTILE_SCENARIO_ID = "p10"
|
|
51
|
+
CENTRAL_QUANTILE_SCENARIO_ID = "p50"
|
|
52
|
+
UPPER_QUANTILE_SCENARIO_ID = "p90"
|
|
53
|
+
_QUANTILE_SCENARIO_IDS = (
|
|
54
|
+
LOWER_QUANTILE_SCENARIO_ID,
|
|
55
|
+
CENTRAL_QUANTILE_SCENARIO_ID,
|
|
56
|
+
UPPER_QUANTILE_SCENARIO_ID,
|
|
57
|
+
)
|
|
58
|
+
_TOKEN = re.compile(r"^[a-z][a-z0-9_.:-]{0,255}$")
|
|
59
|
+
_DEFINITION_DOMAIN = (
|
|
60
|
+
b"agent-evolve:calibrated-positive-gain-policy:v1\x00"
|
|
61
|
+
)
|
|
62
|
+
_ARCHIVE_DOMAIN = (
|
|
63
|
+
b"agent-evolve:calibrated-positive-gain-archive:v1\x00"
|
|
64
|
+
)
|
|
65
|
+
_SCORE_DOMAIN = b"agent-evolve:calibrated-positive-gain-score:v1\x00"
|
|
66
|
+
_RANKING_DOMAIN = (
|
|
67
|
+
b"agent-evolve:calibrated-positive-gain-ranking:v1\x00"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
ObjectivePoint = tuple[tuple[str, float], ...]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _canonical_json(value: object) -> bytes:
|
|
74
|
+
return json.dumps(
|
|
75
|
+
value,
|
|
76
|
+
allow_nan=False,
|
|
77
|
+
ensure_ascii=True,
|
|
78
|
+
separators=(",", ":"),
|
|
79
|
+
sort_keys=True,
|
|
80
|
+
).encode("ascii", errors="strict")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _hash(domain: bytes, value: object) -> str:
|
|
84
|
+
return hashlib.sha256(domain + _canonical_json(value)).hexdigest()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _require_token(value: str, *, name: str) -> None:
|
|
88
|
+
if type(value) is not str or _TOKEN.fullmatch(value) is None:
|
|
89
|
+
raise ValueError(f"{name} must use the closed token grammar")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _require_objective_point(value: ObjectivePoint, *, name: str) -> None:
|
|
93
|
+
if (
|
|
94
|
+
type(value) is not tuple
|
|
95
|
+
or not value
|
|
96
|
+
or value != tuple(sorted(value))
|
|
97
|
+
):
|
|
98
|
+
raise ValueError(f"{name} must be non-empty and canonical")
|
|
99
|
+
metric_ids: list[str] = []
|
|
100
|
+
for item in value:
|
|
101
|
+
if type(item) is not tuple or len(item) != 2:
|
|
102
|
+
raise TypeError(f"{name} must contain metric pairs")
|
|
103
|
+
metric_id, metric_value = item
|
|
104
|
+
_require_token(metric_id, name=f"{name} metric_id")
|
|
105
|
+
if type(metric_value) is not float or not math.isfinite(metric_value):
|
|
106
|
+
raise TypeError(f"{name} values must be finite exact floats")
|
|
107
|
+
metric_ids.append(metric_id)
|
|
108
|
+
if len(metric_ids) != len(set(metric_ids)):
|
|
109
|
+
raise ValueError(f"{name} repeats a metric")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _objective_point_record(value: ObjectivePoint) -> list[dict[str, object]]:
|
|
113
|
+
return [
|
|
114
|
+
{"metric_id": metric_id, "value_hex": metric_value.hex()}
|
|
115
|
+
for metric_id, metric_value in value
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _point_values(
|
|
120
|
+
point: ObjectivePoint,
|
|
121
|
+
metric_ids: tuple[str, ...],
|
|
122
|
+
) -> tuple[float, ...]:
|
|
123
|
+
mapping = dict(point)
|
|
124
|
+
if tuple(sorted(mapping)) != metric_ids:
|
|
125
|
+
raise ValueError("objective point uses a foreign metric frame")
|
|
126
|
+
return tuple(mapping[metric_id] for metric_id in metric_ids)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def non_dominated(
|
|
130
|
+
points: tuple[ObjectivePoint, ...],
|
|
131
|
+
metric_ids: tuple[str, ...],
|
|
132
|
+
) -> tuple[tuple[float, ...], ...]:
|
|
133
|
+
"""Exact minimization Pareto filter over an archive."""
|
|
134
|
+
|
|
135
|
+
raw = sorted({_point_values(point, metric_ids) for point in points})
|
|
136
|
+
return tuple(
|
|
137
|
+
point
|
|
138
|
+
for point in raw
|
|
139
|
+
if not any(
|
|
140
|
+
other != point
|
|
141
|
+
and all(
|
|
142
|
+
o <= p for o, p in zip(other, point, strict=True)
|
|
143
|
+
)
|
|
144
|
+
for other in raw
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def chebyshev_excess(
|
|
150
|
+
anchor: ObjectivePoint,
|
|
151
|
+
archive_points: tuple[ObjectivePoint, ...],
|
|
152
|
+
metric_ids: tuple[str, ...],
|
|
153
|
+
) -> float:
|
|
154
|
+
"""Worst-axis improvement the anchor already holds over the front.
|
|
155
|
+
|
|
156
|
+
Minimization convention. For each non-dominated archive point the
|
|
157
|
+
anchor's worst-axis excess is ``max_j(anchor_j - front_j)``; the returned
|
|
158
|
+
value is the smallest such excess over the front. It is <= 0 exactly
|
|
159
|
+
when the anchor weakly dominates some front point (the anchor is at
|
|
160
|
+
least as good on every axis), and it grows with how much the anchor
|
|
161
|
+
would have to improve on its worst axis to reach the nearest front
|
|
162
|
+
point. The archive is Pareto-filtered first: dominated archive points
|
|
163
|
+
would otherwise lower the excess of anchors that no front point can
|
|
164
|
+
reach.
|
|
165
|
+
|
|
166
|
+
Scale-free in the sense that matters here: it lives in whatever frame
|
|
167
|
+
the archive lives in, and the policy consumes only its ORDER.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
if not archive_points:
|
|
171
|
+
raise ValueError("archive_points must be non-empty")
|
|
172
|
+
values = _point_values(anchor, metric_ids)
|
|
173
|
+
best: float | None = None
|
|
174
|
+
for other in non_dominated(archive_points, metric_ids):
|
|
175
|
+
worst = max(a - b for a, b in zip(values, other, strict=True))
|
|
176
|
+
if best is None or worst < best:
|
|
177
|
+
best = worst
|
|
178
|
+
return float(best if best is not None else 0.0)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@runtime_checkable
|
|
182
|
+
class ArchiveConditionedGainPort(Protocol):
|
|
183
|
+
"""Value one hypothetical objective point against an archive.
|
|
184
|
+
|
|
185
|
+
Implementations own objective senses, normalization, and reference
|
|
186
|
+
points. The returned value must be the exact non-negative marginal
|
|
187
|
+
archive utility of adding the point; a point dominated by the archive
|
|
188
|
+
must return exactly zero. Parents are never visible to this port, so
|
|
189
|
+
parent-relative improvement cannot leak into opportunity.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
utility_id: str
|
|
193
|
+
utility_version: int
|
|
194
|
+
definition_sha256: str
|
|
195
|
+
|
|
196
|
+
def marginal_archive_gain(
|
|
197
|
+
self,
|
|
198
|
+
archive_points: tuple[ObjectivePoint, ...],
|
|
199
|
+
objective_point: ObjectivePoint,
|
|
200
|
+
) -> float: ...
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def validate_archive_conditioned_gain_port(
|
|
204
|
+
value: ArchiveConditionedGainPort,
|
|
205
|
+
) -> tuple[str, int, str]:
|
|
206
|
+
if not isinstance(value, ArchiveConditionedGainPort):
|
|
207
|
+
raise TypeError(
|
|
208
|
+
"gain port must implement ArchiveConditionedGainPort"
|
|
209
|
+
)
|
|
210
|
+
identity = (
|
|
211
|
+
value.utility_id,
|
|
212
|
+
value.utility_version,
|
|
213
|
+
value.definition_sha256,
|
|
214
|
+
)
|
|
215
|
+
_require_token(identity[0], name="gain utility_id")
|
|
216
|
+
if type(identity[1]) is not int or identity[1] <= 0:
|
|
217
|
+
raise ValueError("gain utility_version must be positive")
|
|
218
|
+
require_sha256(identity[2], "gain utility definition_sha256")
|
|
219
|
+
return identity
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass(frozen=True, slots=True)
|
|
223
|
+
class PositiveGainForecast:
|
|
224
|
+
"""Sealed scenario-quantile forecast for one candidate."""
|
|
225
|
+
|
|
226
|
+
quantile_points: tuple[tuple[str, ObjectivePoint], ...]
|
|
227
|
+
reliability: float = 1.0
|
|
228
|
+
|
|
229
|
+
def __post_init__(self) -> None:
|
|
230
|
+
if (
|
|
231
|
+
type(self.quantile_points) is not tuple
|
|
232
|
+
or tuple(value[0] for value in self.quantile_points)
|
|
233
|
+
!= tuple(sorted(_QUANTILE_SCENARIO_IDS))
|
|
234
|
+
):
|
|
235
|
+
raise ValueError(
|
|
236
|
+
"quantile_points must cover exactly the canonical "
|
|
237
|
+
"p10/p50/p90 scenarios"
|
|
238
|
+
)
|
|
239
|
+
frames = set()
|
|
240
|
+
for scenario_id, point in self.quantile_points:
|
|
241
|
+
_require_objective_point(
|
|
242
|
+
point,
|
|
243
|
+
name=f"{scenario_id} objective point",
|
|
244
|
+
)
|
|
245
|
+
frames.add(tuple(metric_id for metric_id, _value in point))
|
|
246
|
+
if len(frames) != 1:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"all quantile points must share one objective frame"
|
|
249
|
+
)
|
|
250
|
+
if (
|
|
251
|
+
type(self.reliability) is not float
|
|
252
|
+
or not math.isfinite(self.reliability)
|
|
253
|
+
or not 0.0 <= self.reliability <= 1.0
|
|
254
|
+
):
|
|
255
|
+
raise ValueError("reliability must lie in [0, 1]")
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def from_parent_and_deltas(
|
|
259
|
+
cls,
|
|
260
|
+
*,
|
|
261
|
+
parent_point: ObjectivePoint,
|
|
262
|
+
quantile_deltas: tuple[tuple[str, tuple[float, ...]], ...],
|
|
263
|
+
reliability: float = 1.0,
|
|
264
|
+
) -> PositiveGainForecast:
|
|
265
|
+
"""Absolute scenario points from a parent point plus per-metric deltas.
|
|
266
|
+
|
|
267
|
+
A proposal-time self-report is a per-metric scenario DELTA against
|
|
268
|
+
the candidate's parent, so the absolute scenario point is the parent
|
|
269
|
+
point plus that delta. ``quantile_deltas`` pairs each metric with
|
|
270
|
+
one delta per canonical scenario, ordered exactly as
|
|
271
|
+
``LOWER/CENTRAL/UPPER``; the deltas must live in the same frame as
|
|
272
|
+
``parent_point``, and the arithmetic is a pure translation, so any
|
|
273
|
+
affine renormalization of the frame commutes with this constructor.
|
|
274
|
+
|
|
275
|
+
The only reason a self-report cannot become a forecast is missing
|
|
276
|
+
evidence: a candidate with no parent, or a report that does not cover
|
|
277
|
+
every metric, has no absolute point and must stay forecast-free. The
|
|
278
|
+
caller decides that by not calling this.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
_require_objective_point(parent_point, name="parent_point")
|
|
282
|
+
if type(quantile_deltas) is not tuple:
|
|
283
|
+
raise TypeError("quantile_deltas must be an exact tuple")
|
|
284
|
+
deltas: dict[str, tuple[float, ...]] = {}
|
|
285
|
+
for item in quantile_deltas:
|
|
286
|
+
if type(item) is not tuple or len(item) != 2:
|
|
287
|
+
raise TypeError(
|
|
288
|
+
"quantile_deltas must pair a metric with its deltas"
|
|
289
|
+
)
|
|
290
|
+
metric_id, scenario_deltas = item
|
|
291
|
+
_require_token(metric_id, name="quantile delta metric_id")
|
|
292
|
+
if (
|
|
293
|
+
type(scenario_deltas) is not tuple
|
|
294
|
+
or len(scenario_deltas) != len(_QUANTILE_SCENARIO_IDS)
|
|
295
|
+
or any(
|
|
296
|
+
type(value) is not float or not math.isfinite(value)
|
|
297
|
+
for value in scenario_deltas
|
|
298
|
+
)
|
|
299
|
+
):
|
|
300
|
+
raise TypeError(
|
|
301
|
+
"each metric needs one finite delta per scenario"
|
|
302
|
+
)
|
|
303
|
+
if metric_id in deltas:
|
|
304
|
+
raise ValueError("quantile_deltas repeat a metric")
|
|
305
|
+
deltas[metric_id] = scenario_deltas
|
|
306
|
+
parent = dict(parent_point)
|
|
307
|
+
if tuple(sorted(deltas)) != tuple(sorted(parent)):
|
|
308
|
+
raise ValueError(
|
|
309
|
+
"quantile_deltas must cover the parent's exact frame"
|
|
310
|
+
)
|
|
311
|
+
return cls(
|
|
312
|
+
quantile_points=tuple(
|
|
313
|
+
(
|
|
314
|
+
scenario_id,
|
|
315
|
+
tuple(
|
|
316
|
+
sorted(
|
|
317
|
+
(
|
|
318
|
+
metric_id,
|
|
319
|
+
parent[metric_id]
|
|
320
|
+
+ deltas[metric_id][index],
|
|
321
|
+
)
|
|
322
|
+
for metric_id in parent
|
|
323
|
+
)
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
for index, scenario_id in enumerate(
|
|
327
|
+
_QUANTILE_SCENARIO_IDS
|
|
328
|
+
)
|
|
329
|
+
),
|
|
330
|
+
reliability=float(reliability),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def point(self, scenario_id: str) -> ObjectivePoint:
|
|
334
|
+
for value_id, point in self.quantile_points:
|
|
335
|
+
if value_id == scenario_id:
|
|
336
|
+
return point
|
|
337
|
+
raise ValueError("forecast omits the requested scenario")
|
|
338
|
+
|
|
339
|
+
def to_record(self) -> dict[str, object]:
|
|
340
|
+
self.__post_init__()
|
|
341
|
+
return {
|
|
342
|
+
"quantile_points": [
|
|
343
|
+
{
|
|
344
|
+
"scenario_id": scenario_id,
|
|
345
|
+
"objective_point": _objective_point_record(point),
|
|
346
|
+
}
|
|
347
|
+
for scenario_id, point in self.quantile_points
|
|
348
|
+
],
|
|
349
|
+
"reliability_hex": self.reliability.hex(),
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dataclass(frozen=True, slots=True)
|
|
354
|
+
class PositiveGainCandidate:
|
|
355
|
+
"""Outcome-blind view of one unevaluated eligible candidate."""
|
|
356
|
+
|
|
357
|
+
action_sha256: str
|
|
358
|
+
engine_id: str
|
|
359
|
+
native_rank: int
|
|
360
|
+
lane_size: int
|
|
361
|
+
forecast: PositiveGainForecast | None = None
|
|
362
|
+
frozen_score: float | None = None
|
|
363
|
+
#: The objective point this action was derived from, known at proposal
|
|
364
|
+
#: time and never an outcome of this action. Absent when the action has
|
|
365
|
+
#: no parent geometry (a global acquisition lane, say).
|
|
366
|
+
anchor_point: ObjectivePoint | None = None
|
|
367
|
+
|
|
368
|
+
def __post_init__(self) -> None:
|
|
369
|
+
require_sha256(self.action_sha256, "action_sha256")
|
|
370
|
+
_require_token(self.engine_id, name="engine_id")
|
|
371
|
+
if (
|
|
372
|
+
type(self.native_rank) is not int
|
|
373
|
+
or type(self.lane_size) is not int
|
|
374
|
+
or self.native_rank <= 0
|
|
375
|
+
or self.lane_size <= 0
|
|
376
|
+
or self.native_rank > self.lane_size
|
|
377
|
+
):
|
|
378
|
+
raise ValueError("native rank must fit the positive lane size")
|
|
379
|
+
if self.forecast is not None:
|
|
380
|
+
if type(self.forecast) is not PositiveGainForecast:
|
|
381
|
+
raise TypeError("forecast must be exact or None")
|
|
382
|
+
self.forecast.__post_init__()
|
|
383
|
+
if self.frozen_score is not None and (
|
|
384
|
+
type(self.frozen_score) is not float
|
|
385
|
+
or not math.isfinite(self.frozen_score)
|
|
386
|
+
or not 0.0 <= self.frozen_score <= 1.0
|
|
387
|
+
):
|
|
388
|
+
raise ValueError("frozen_score must lie in [0, 1] or be None")
|
|
389
|
+
if self.anchor_point is not None:
|
|
390
|
+
_require_objective_point(self.anchor_point, name="anchor_point")
|
|
391
|
+
|
|
392
|
+
def to_record(self) -> dict[str, object]:
|
|
393
|
+
self.__post_init__()
|
|
394
|
+
return {
|
|
395
|
+
"action_sha256": self.action_sha256,
|
|
396
|
+
"engine_id": self.engine_id,
|
|
397
|
+
"native_rank": self.native_rank,
|
|
398
|
+
"lane_size": self.lane_size,
|
|
399
|
+
"forecast": (
|
|
400
|
+
None
|
|
401
|
+
if self.forecast is None
|
|
402
|
+
else self.forecast.to_record()
|
|
403
|
+
),
|
|
404
|
+
"frozen_score_hex": (
|
|
405
|
+
None
|
|
406
|
+
if self.frozen_score is None
|
|
407
|
+
else self.frozen_score.hex()
|
|
408
|
+
),
|
|
409
|
+
"anchor_point": (
|
|
410
|
+
None
|
|
411
|
+
if self.anchor_point is None
|
|
412
|
+
else _objective_point_record(self.anchor_point)
|
|
413
|
+
),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@dataclass(frozen=True, slots=True)
|
|
418
|
+
class ObservedConversionOutcome:
|
|
419
|
+
"""One real outcome observed strictly before the current decision."""
|
|
420
|
+
|
|
421
|
+
observation_ordinal: int
|
|
422
|
+
engine_id: str
|
|
423
|
+
native_rank: int
|
|
424
|
+
lane_size: int
|
|
425
|
+
feasible: bool
|
|
426
|
+
marginal_archive_gain: float
|
|
427
|
+
|
|
428
|
+
def __post_init__(self) -> None:
|
|
429
|
+
if (
|
|
430
|
+
type(self.observation_ordinal) is not int
|
|
431
|
+
or self.observation_ordinal <= 0
|
|
432
|
+
):
|
|
433
|
+
raise ValueError("observation_ordinal must be positive")
|
|
434
|
+
_require_token(self.engine_id, name="engine_id")
|
|
435
|
+
if (
|
|
436
|
+
type(self.native_rank) is not int
|
|
437
|
+
or type(self.lane_size) is not int
|
|
438
|
+
or self.native_rank <= 0
|
|
439
|
+
or self.lane_size <= 0
|
|
440
|
+
or self.native_rank > self.lane_size
|
|
441
|
+
):
|
|
442
|
+
raise ValueError("native rank must fit the positive lane size")
|
|
443
|
+
if type(self.feasible) is not bool:
|
|
444
|
+
raise TypeError("feasible must be exact")
|
|
445
|
+
if (
|
|
446
|
+
type(self.marginal_archive_gain) is not float
|
|
447
|
+
or not math.isfinite(self.marginal_archive_gain)
|
|
448
|
+
or self.marginal_archive_gain < 0.0
|
|
449
|
+
):
|
|
450
|
+
raise ValueError(
|
|
451
|
+
"marginal_archive_gain must be finite and non-negative"
|
|
452
|
+
)
|
|
453
|
+
if not self.feasible and self.marginal_archive_gain != 0.0:
|
|
454
|
+
raise ValueError("an infeasible outcome cannot contribute gain")
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def positive(self) -> bool:
|
|
458
|
+
return self.marginal_archive_gain > 0.0
|
|
459
|
+
|
|
460
|
+
def to_record(self) -> dict[str, object]:
|
|
461
|
+
self.__post_init__()
|
|
462
|
+
return {
|
|
463
|
+
"observation_ordinal": self.observation_ordinal,
|
|
464
|
+
"engine_id": self.engine_id,
|
|
465
|
+
"native_rank": self.native_rank,
|
|
466
|
+
"lane_size": self.lane_size,
|
|
467
|
+
"feasible": self.feasible,
|
|
468
|
+
"positive": self.positive,
|
|
469
|
+
"marginal_archive_gain_hex": (
|
|
470
|
+
self.marginal_archive_gain.hex()
|
|
471
|
+
),
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@dataclass(frozen=True, slots=True)
|
|
476
|
+
class CalibratedPositiveGainScore:
|
|
477
|
+
"""Complete score card for one eligible candidate; never abstains."""
|
|
478
|
+
|
|
479
|
+
action_sha256: str
|
|
480
|
+
p_feasible: float
|
|
481
|
+
p_positive_archive_gain: float
|
|
482
|
+
expected_positive_gain: float
|
|
483
|
+
tail_risk: float
|
|
484
|
+
value_of_information: float
|
|
485
|
+
uncertainty: float
|
|
486
|
+
effective_sample_size: float
|
|
487
|
+
score: float
|
|
488
|
+
forecast_probability: float | None
|
|
489
|
+
forecast_magnitude: float | None
|
|
490
|
+
forecast_nondominated_fraction: float | None
|
|
491
|
+
conversion_probability: float
|
|
492
|
+
conversion_magnitude: float
|
|
493
|
+
frozen_score_probability: float | None
|
|
494
|
+
frozen_evidence_weight: float
|
|
495
|
+
#: Chebyshev excess of the candidate's anchor over the current
|
|
496
|
+
#: archive front; smaller means the parent sits nearer the front.
|
|
497
|
+
anchor_excess: float | None = None
|
|
498
|
+
#: Smallest max-norm distance from the candidate's anchor to any anchor
|
|
499
|
+
#: this market already bought; ``inf`` when nothing is bought yet.
|
|
500
|
+
anchor_dispersion: float | None = None
|
|
501
|
+
score_sha256: str = field(init=False)
|
|
502
|
+
|
|
503
|
+
def __post_init__(self) -> None:
|
|
504
|
+
require_sha256(self.action_sha256, "action_sha256")
|
|
505
|
+
for name in ("p_feasible", "p_positive_archive_gain"):
|
|
506
|
+
value = getattr(self, name)
|
|
507
|
+
if (
|
|
508
|
+
type(value) is not float
|
|
509
|
+
or not math.isfinite(value)
|
|
510
|
+
or not 0.0 <= value <= 1.0
|
|
511
|
+
):
|
|
512
|
+
raise ValueError(f"{name} must lie in [0, 1]")
|
|
513
|
+
for name in (
|
|
514
|
+
"expected_positive_gain",
|
|
515
|
+
"tail_risk",
|
|
516
|
+
"value_of_information",
|
|
517
|
+
"uncertainty",
|
|
518
|
+
"conversion_magnitude",
|
|
519
|
+
):
|
|
520
|
+
value = getattr(self, name)
|
|
521
|
+
if type(value) is not float or not math.isfinite(value) or value < 0.0:
|
|
522
|
+
raise ValueError(f"{name} must be finite and non-negative")
|
|
523
|
+
if (
|
|
524
|
+
type(self.effective_sample_size) is not float
|
|
525
|
+
or not math.isfinite(self.effective_sample_size)
|
|
526
|
+
or self.effective_sample_size <= 0.0
|
|
527
|
+
):
|
|
528
|
+
raise ValueError("effective_sample_size must be positive")
|
|
529
|
+
if type(self.score) is not float or not math.isfinite(self.score):
|
|
530
|
+
raise ValueError("score must be a finite float")
|
|
531
|
+
forecast_fields = (
|
|
532
|
+
self.forecast_probability,
|
|
533
|
+
self.forecast_magnitude,
|
|
534
|
+
self.forecast_nondominated_fraction,
|
|
535
|
+
)
|
|
536
|
+
if any(value is None for value in forecast_fields) != all(
|
|
537
|
+
value is None for value in forecast_fields
|
|
538
|
+
):
|
|
539
|
+
raise ValueError(
|
|
540
|
+
"forecast components must be jointly present or absent"
|
|
541
|
+
)
|
|
542
|
+
for value in forecast_fields:
|
|
543
|
+
if value is not None and (
|
|
544
|
+
type(value) is not float
|
|
545
|
+
or not math.isfinite(value)
|
|
546
|
+
or value < 0.0
|
|
547
|
+
):
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"forecast components must be finite and non-negative"
|
|
550
|
+
)
|
|
551
|
+
if (
|
|
552
|
+
type(self.conversion_probability) is not float
|
|
553
|
+
or not math.isfinite(self.conversion_probability)
|
|
554
|
+
or not 0.0 <= self.conversion_probability <= 1.0
|
|
555
|
+
):
|
|
556
|
+
raise ValueError("conversion_probability must lie in [0, 1]")
|
|
557
|
+
if self.frozen_score_probability is not None and (
|
|
558
|
+
type(self.frozen_score_probability) is not float
|
|
559
|
+
or not math.isfinite(self.frozen_score_probability)
|
|
560
|
+
or not 0.0 <= self.frozen_score_probability <= 1.0
|
|
561
|
+
):
|
|
562
|
+
raise ValueError(
|
|
563
|
+
"frozen_score_probability must lie in [0, 1] or be None"
|
|
564
|
+
)
|
|
565
|
+
if (
|
|
566
|
+
type(self.frozen_evidence_weight) is not float
|
|
567
|
+
or not math.isfinite(self.frozen_evidence_weight)
|
|
568
|
+
or not 0.0 <= self.frozen_evidence_weight <= 1.0
|
|
569
|
+
):
|
|
570
|
+
raise ValueError("frozen_evidence_weight must lie in [0, 1]")
|
|
571
|
+
if (
|
|
572
|
+
self.frozen_score_probability is None
|
|
573
|
+
and self.frozen_evidence_weight != 0.0
|
|
574
|
+
):
|
|
575
|
+
raise ValueError(
|
|
576
|
+
"frozen evidence weight requires a frozen score"
|
|
577
|
+
)
|
|
578
|
+
if self.anchor_excess is not None and (
|
|
579
|
+
type(self.anchor_excess) is not float
|
|
580
|
+
or not math.isfinite(self.anchor_excess)
|
|
581
|
+
):
|
|
582
|
+
raise ValueError("anchor_excess must be finite or None")
|
|
583
|
+
if self.anchor_dispersion is not None and (
|
|
584
|
+
type(self.anchor_dispersion) is not float
|
|
585
|
+
or math.isnan(self.anchor_dispersion)
|
|
586
|
+
or self.anchor_dispersion < 0.0
|
|
587
|
+
):
|
|
588
|
+
raise ValueError(
|
|
589
|
+
"anchor_dispersion must be non-negative or None"
|
|
590
|
+
)
|
|
591
|
+
if (self.anchor_excess is None) != (
|
|
592
|
+
self.anchor_dispersion is None
|
|
593
|
+
):
|
|
594
|
+
raise ValueError(
|
|
595
|
+
"anchor geometry must be jointly present or absent"
|
|
596
|
+
)
|
|
597
|
+
object.__setattr__(
|
|
598
|
+
self,
|
|
599
|
+
"score_sha256",
|
|
600
|
+
_hash(_SCORE_DOMAIN, self._unsigned_record()),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
604
|
+
# The anchor keys are emitted only when the archive-geometry
|
|
605
|
+
# channel is active, so a score card produced with the channel off
|
|
606
|
+
# keeps its pre-existing bytes and hash exactly.
|
|
607
|
+
anchor: dict[str, object] = (
|
|
608
|
+
{}
|
|
609
|
+
if self.anchor_excess is None
|
|
610
|
+
else {
|
|
611
|
+
"anchor_excess_hex": self.anchor_excess.hex(),
|
|
612
|
+
"anchor_dispersion_hex": (
|
|
613
|
+
"inf"
|
|
614
|
+
if math.isinf(self.anchor_dispersion)
|
|
615
|
+
else self.anchor_dispersion.hex()
|
|
616
|
+
),
|
|
617
|
+
}
|
|
618
|
+
)
|
|
619
|
+
return {
|
|
620
|
+
**anchor,
|
|
621
|
+
"schema_version": 1,
|
|
622
|
+
"action_sha256": self.action_sha256,
|
|
623
|
+
"p_feasible_hex": self.p_feasible.hex(),
|
|
624
|
+
"p_positive_archive_gain_hex": (
|
|
625
|
+
self.p_positive_archive_gain.hex()
|
|
626
|
+
),
|
|
627
|
+
"expected_positive_gain_hex": (
|
|
628
|
+
self.expected_positive_gain.hex()
|
|
629
|
+
),
|
|
630
|
+
"tail_risk_hex": self.tail_risk.hex(),
|
|
631
|
+
"value_of_information_hex": (
|
|
632
|
+
self.value_of_information.hex()
|
|
633
|
+
),
|
|
634
|
+
"uncertainty_hex": self.uncertainty.hex(),
|
|
635
|
+
"effective_sample_size_hex": (
|
|
636
|
+
self.effective_sample_size.hex()
|
|
637
|
+
),
|
|
638
|
+
"score_hex": self.score.hex(),
|
|
639
|
+
"forecast_probability_hex": (
|
|
640
|
+
None
|
|
641
|
+
if self.forecast_probability is None
|
|
642
|
+
else self.forecast_probability.hex()
|
|
643
|
+
),
|
|
644
|
+
"forecast_magnitude_hex": (
|
|
645
|
+
None
|
|
646
|
+
if self.forecast_magnitude is None
|
|
647
|
+
else self.forecast_magnitude.hex()
|
|
648
|
+
),
|
|
649
|
+
"forecast_nondominated_fraction_hex": (
|
|
650
|
+
None
|
|
651
|
+
if self.forecast_nondominated_fraction is None
|
|
652
|
+
else self.forecast_nondominated_fraction.hex()
|
|
653
|
+
),
|
|
654
|
+
"conversion_probability_hex": (
|
|
655
|
+
self.conversion_probability.hex()
|
|
656
|
+
),
|
|
657
|
+
"conversion_magnitude_hex": (
|
|
658
|
+
self.conversion_magnitude.hex()
|
|
659
|
+
),
|
|
660
|
+
"frozen_score_probability_hex": (
|
|
661
|
+
None
|
|
662
|
+
if self.frozen_score_probability is None
|
|
663
|
+
else self.frozen_score_probability.hex()
|
|
664
|
+
),
|
|
665
|
+
"frozen_evidence_weight_hex": (
|
|
666
|
+
self.frozen_evidence_weight.hex()
|
|
667
|
+
),
|
|
668
|
+
"abstained": False,
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
def to_record(self) -> dict[str, object]:
|
|
672
|
+
self.__post_init__()
|
|
673
|
+
return {
|
|
674
|
+
**self._unsigned_record(),
|
|
675
|
+
"score_sha256": self.score_sha256,
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclass(frozen=True, slots=True)
|
|
680
|
+
class CalibratedPositiveGainRanking:
|
|
681
|
+
"""One cutoff-bound, always-defined ranking of eligible candidates."""
|
|
682
|
+
|
|
683
|
+
policy_id: str
|
|
684
|
+
policy_version: int
|
|
685
|
+
policy_definition_sha256: str
|
|
686
|
+
archive_sha256: str
|
|
687
|
+
future_seats_remaining: int
|
|
688
|
+
horizon_total: int
|
|
689
|
+
scores: tuple[CalibratedPositiveGainScore, ...]
|
|
690
|
+
ranked_action_sha256s: tuple[str, ...]
|
|
691
|
+
ranking_sha256: str = field(init=False)
|
|
692
|
+
|
|
693
|
+
def __post_init__(self) -> None:
|
|
694
|
+
_require_token(self.policy_id, name="policy_id")
|
|
695
|
+
if type(self.policy_version) is not int or self.policy_version <= 0:
|
|
696
|
+
raise ValueError("policy_version must be positive")
|
|
697
|
+
require_sha256(
|
|
698
|
+
self.policy_definition_sha256,
|
|
699
|
+
"policy_definition_sha256",
|
|
700
|
+
)
|
|
701
|
+
require_sha256(self.archive_sha256, "archive_sha256")
|
|
702
|
+
if (
|
|
703
|
+
type(self.future_seats_remaining) is not int
|
|
704
|
+
or type(self.horizon_total) is not int
|
|
705
|
+
or self.horizon_total <= 0
|
|
706
|
+
or not 0 <= self.future_seats_remaining <= self.horizon_total
|
|
707
|
+
):
|
|
708
|
+
raise ValueError(
|
|
709
|
+
"future seats must fit the positive horizon total"
|
|
710
|
+
)
|
|
711
|
+
if (
|
|
712
|
+
type(self.scores) is not tuple
|
|
713
|
+
or not self.scores
|
|
714
|
+
or any(
|
|
715
|
+
type(value) is not CalibratedPositiveGainScore
|
|
716
|
+
for value in self.scores
|
|
717
|
+
)
|
|
718
|
+
):
|
|
719
|
+
raise TypeError("scores must contain exact score cards")
|
|
720
|
+
for value in self.scores:
|
|
721
|
+
value.__post_init__()
|
|
722
|
+
action_ids = tuple(value.action_sha256 for value in self.scores)
|
|
723
|
+
if action_ids != tuple(sorted(set(action_ids))):
|
|
724
|
+
raise ValueError("scores must be action-canonical")
|
|
725
|
+
if (
|
|
726
|
+
type(self.ranked_action_sha256s) is not tuple
|
|
727
|
+
or tuple(sorted(self.ranked_action_sha256s)) != action_ids
|
|
728
|
+
):
|
|
729
|
+
raise ValueError(
|
|
730
|
+
"ranking must order the exact eligible market"
|
|
731
|
+
)
|
|
732
|
+
object.__setattr__(
|
|
733
|
+
self,
|
|
734
|
+
"ranking_sha256",
|
|
735
|
+
_hash(_RANKING_DOMAIN, self._unsigned_record()),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def score_for(self, action_sha256: str) -> CalibratedPositiveGainScore:
|
|
739
|
+
for value in self.scores:
|
|
740
|
+
if value.action_sha256 == action_sha256:
|
|
741
|
+
return value
|
|
742
|
+
raise ValueError("action is outside the ranked market")
|
|
743
|
+
|
|
744
|
+
def _unsigned_record(self) -> dict[str, object]:
|
|
745
|
+
return {
|
|
746
|
+
"schema_version": 1,
|
|
747
|
+
"policy": {
|
|
748
|
+
"policy_id": self.policy_id,
|
|
749
|
+
"policy_version": self.policy_version,
|
|
750
|
+
"definition_sha256": self.policy_definition_sha256,
|
|
751
|
+
},
|
|
752
|
+
"archive_sha256": self.archive_sha256,
|
|
753
|
+
"future_seats_remaining": self.future_seats_remaining,
|
|
754
|
+
"horizon_total": self.horizon_total,
|
|
755
|
+
"score_sha256s": [
|
|
756
|
+
value.score_sha256 for value in self.scores
|
|
757
|
+
],
|
|
758
|
+
"ranked_action_sha256s": list(self.ranked_action_sha256s),
|
|
759
|
+
"hard_abstention": False,
|
|
760
|
+
"eligible_candidate_outcomes_observed": False,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
def to_record(self, *, include_scores: bool = False) -> dict[str, object]:
|
|
764
|
+
self.__post_init__()
|
|
765
|
+
result = {
|
|
766
|
+
**self._unsigned_record(),
|
|
767
|
+
"ranking_sha256": self.ranking_sha256,
|
|
768
|
+
}
|
|
769
|
+
if include_scores:
|
|
770
|
+
result["scores"] = [
|
|
771
|
+
value.to_record() for value in self.scores
|
|
772
|
+
]
|
|
773
|
+
return result
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _beta_posterior_mean(
|
|
777
|
+
*,
|
|
778
|
+
prior_mean: float,
|
|
779
|
+
prior_strength: float,
|
|
780
|
+
successes: float,
|
|
781
|
+
failures: float,
|
|
782
|
+
) -> float:
|
|
783
|
+
return (prior_strength * prior_mean + successes) / (
|
|
784
|
+
prior_strength + successes + failures
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _shrunk_magnitude(
|
|
789
|
+
*,
|
|
790
|
+
prior_mean: float,
|
|
791
|
+
prior_strength: float,
|
|
792
|
+
positive_gain_sum: float,
|
|
793
|
+
positive_count: float,
|
|
794
|
+
) -> float:
|
|
795
|
+
return (prior_strength * prior_mean + positive_gain_sum) / (
|
|
796
|
+
prior_strength + positive_count
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
@dataclass(frozen=True, slots=True)
|
|
801
|
+
class _ConversionCellEvidence:
|
|
802
|
+
observation_count: float
|
|
803
|
+
positive_count: float
|
|
804
|
+
feasible_count: float
|
|
805
|
+
positive_gain_sum: float
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@dataclass(frozen=True, slots=True)
|
|
809
|
+
class CalibratedPositiveGainOpportunityPolicy:
|
|
810
|
+
"""Rank every eligible candidate by calibrated positive archive gain."""
|
|
811
|
+
|
|
812
|
+
archive_gain_utility: ArchiveConditionedGainPort = field(
|
|
813
|
+
repr=False,
|
|
814
|
+
compare=False,
|
|
815
|
+
)
|
|
816
|
+
lambda_: float = 1.0
|
|
817
|
+
beta: float = 1.0
|
|
818
|
+
mixture_weight: float = 0.5
|
|
819
|
+
prior_strength: float = 2.0
|
|
820
|
+
band_count: int = 3
|
|
821
|
+
reference_gain_scale: float = 1.0e-4
|
|
822
|
+
root_prior_probability: float = 0.5
|
|
823
|
+
probability_floor: float = 1.0e-6
|
|
824
|
+
scenario_weights: tuple[float, float, float] = (0.25, 0.5, 0.25)
|
|
825
|
+
# The V70 census measured the frozen cross-campaign score as
|
|
826
|
+
# uninformative (pooled Spearman with positivity about -0.01) while
|
|
827
|
+
# native rank was informative, so frozen evidence is a WEAK log-odds
|
|
828
|
+
# feature: a small default weight, and no effect at all unless the
|
|
829
|
+
# frozen fit's training history covers enough runs.
|
|
830
|
+
frozen_score_weight: float = 0.125
|
|
831
|
+
frozen_score_minimum_training_runs: int = 10
|
|
832
|
+
# When cell posteriors tie exactly (no forecasts, shared evidence
|
|
833
|
+
# cell), break ties by the engine's own native-rank order instead
|
|
834
|
+
# of the arbitrary action hash. Off by default so the r1 ranking
|
|
835
|
+
# and definition hash are preserved bit-for-bit.
|
|
836
|
+
within_cell_rank_tie_break: bool = False
|
|
837
|
+
# ARCHIVE-GEOMETRY TIE-BREAK. The jul28 diagnosis measured that with
|
|
838
|
+
# forecasts absent, every probability input to this score is a per
|
|
839
|
+
# (engine, rank-band) CELL constant: the score took 1.4 to 2.8 distinct
|
|
840
|
+
# values over markets of 37 to 64 members, 46-83% of every market tied
|
|
841
|
+
# with the argmax, and 23 of 28 live seats were therefore chosen by the
|
|
842
|
+
# TIE-BREAK and not by the score. The tie-break in use was native-rank
|
|
843
|
+
# quality, a measured non-feature (AUC 0.552), and the seats it bought
|
|
844
|
+
# were geometrically redundant: the panel converted only 30% of the
|
|
845
|
+
# individual gain it purchased into union gain, against a uniform
|
|
846
|
+
# draw's 68%.
|
|
847
|
+
#
|
|
848
|
+
# This flag replaces that tie-break with archive geometry, in the exact
|
|
849
|
+
# place the decision is actually taken. Among candidates the score
|
|
850
|
+
# cannot separate, prefer, in order:
|
|
851
|
+
#
|
|
852
|
+
# 1. the anchor FARTHEST from the anchors this market already bought
|
|
853
|
+
# (max-min Chebyshev dispersion). Provably submodular: a second
|
|
854
|
+
# seat on an identical anchor has dispersion exactly zero and is
|
|
855
|
+
# taken last, which is the redundancy the census measured; and
|
|
856
|
+
# 2. failing that -- nothing bought yet, or an exact dispersion tie --
|
|
857
|
+
# the anchor NEAREST the current archive front (smallest Chebyshev
|
|
858
|
+
# excess), which is the strongest outcome-blind predictor measured
|
|
859
|
+
# (pooled AUC 0.732 held-out, 0.700 on the replay corpus).
|
|
860
|
+
#
|
|
861
|
+
# Both quantities are pure archive geometry consumed as an ORDER only:
|
|
862
|
+
# no metric name, no unit, no absolute threshold, no workload constant,
|
|
863
|
+
# and invariant to any monotone rescaling of the objective frame. A
|
|
864
|
+
# candidate with no anchor takes the market's median position on both,
|
|
865
|
+
# so an anchorless lane is never systematically evicted or protected.
|
|
866
|
+
# Off by default so the r1 and r2 rankings and definition hashes are
|
|
867
|
+
# preserved bit-for-bit.
|
|
868
|
+
anchor_geometry_tie_break: bool = False
|
|
869
|
+
# DOWNSIDE TAIL RISK. The forecast branch already prices risk as an
|
|
870
|
+
# expected SHORTFALL -- ``sum_s w_s * max(0, central - gain_s)`` -- which
|
|
871
|
+
# is zero when the scenarios agree and never scales with the magnitude on
|
|
872
|
+
# its own. The conversion branch instead priced it as ``(1 - p) * M``,
|
|
873
|
+
# which is not a shortfall: substituting it into
|
|
874
|
+
# ``score = p*M - lambda*tail_risk`` collapses the score to
|
|
875
|
+
# ``M * (2p - 1)`` at ``lambda = 1``, so the magnitude channel is
|
|
876
|
+
# INVERTED for every candidate with ``p < 0.5`` and a cell that has
|
|
877
|
+
# demonstrated larger gains scores lower. The jul28 census measured that
|
|
878
|
+
# exposure at 18.1% of multiplier scored candidates.
|
|
879
|
+
#
|
|
880
|
+
# This flag applies the forecast branch's own definition to the
|
|
881
|
+
# conversion branch's own two-point scenario set -- gain ``M`` with
|
|
882
|
+
# probability ``p``, gain ``0`` with probability ``1 - p``, central
|
|
883
|
+
# outcome the expected gain ``p*M`` -- which evaluates in closed form to
|
|
884
|
+
# ``p * (1 - p) * M``. The score becomes ``p*M*(1 - lambda*(1 - p))``,
|
|
885
|
+
# non-decreasing in both ``p`` and ``M`` at ``lambda <= 1``: risk still
|
|
886
|
+
# penalises an uncertain candidate, but it can no longer reverse the
|
|
887
|
+
# sign of the magnitude channel. No new constant, no threshold, and the
|
|
888
|
+
# forecast branch's shortfall is untouched. Off by default so the r1,
|
|
889
|
+
# r2 and r3 rankings and definition hashes are preserved bit-for-bit.
|
|
890
|
+
downside_shortfall_tail_risk: bool = False
|
|
891
|
+
policy_id: str = CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_ID
|
|
892
|
+
policy_version: int = (
|
|
893
|
+
CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_VERSION
|
|
894
|
+
)
|
|
895
|
+
definition_sha256: str = field(init=False)
|
|
896
|
+
|
|
897
|
+
def __post_init__(self) -> None:
|
|
898
|
+
utility_identity = validate_archive_conditioned_gain_port(
|
|
899
|
+
self.archive_gain_utility
|
|
900
|
+
)
|
|
901
|
+
for name in ("lambda_", "beta"):
|
|
902
|
+
value = getattr(self, name)
|
|
903
|
+
if type(value) is not float or not math.isfinite(value) or value < 0.0:
|
|
904
|
+
raise ValueError(f"{name} must be finite and non-negative")
|
|
905
|
+
for name in (
|
|
906
|
+
"mixture_weight",
|
|
907
|
+
"root_prior_probability",
|
|
908
|
+
"frozen_score_weight",
|
|
909
|
+
):
|
|
910
|
+
value = getattr(self, name)
|
|
911
|
+
if (
|
|
912
|
+
type(value) is not float
|
|
913
|
+
or not math.isfinite(value)
|
|
914
|
+
or not 0.0 <= value <= 1.0
|
|
915
|
+
):
|
|
916
|
+
raise ValueError(f"{name} must lie in [0, 1]")
|
|
917
|
+
if (
|
|
918
|
+
type(self.frozen_score_minimum_training_runs) is not int
|
|
919
|
+
or self.frozen_score_minimum_training_runs < 0
|
|
920
|
+
):
|
|
921
|
+
raise ValueError(
|
|
922
|
+
"frozen_score_minimum_training_runs must be non-negative"
|
|
923
|
+
)
|
|
924
|
+
for name in (
|
|
925
|
+
"within_cell_rank_tie_break",
|
|
926
|
+
"anchor_geometry_tie_break",
|
|
927
|
+
"downside_shortfall_tail_risk",
|
|
928
|
+
):
|
|
929
|
+
if type(getattr(self, name)) is not bool:
|
|
930
|
+
raise TypeError(f"{name} must be exact")
|
|
931
|
+
for name in ("prior_strength", "reference_gain_scale"):
|
|
932
|
+
value = getattr(self, name)
|
|
933
|
+
if type(value) is not float or not math.isfinite(value) or value <= 0.0:
|
|
934
|
+
raise ValueError(f"{name} must be positive")
|
|
935
|
+
if type(self.band_count) is not int or self.band_count <= 0:
|
|
936
|
+
raise ValueError("band_count must be positive")
|
|
937
|
+
if (
|
|
938
|
+
type(self.probability_floor) is not float
|
|
939
|
+
or not math.isfinite(self.probability_floor)
|
|
940
|
+
or not 0.0 < self.probability_floor < 0.5
|
|
941
|
+
):
|
|
942
|
+
raise ValueError("probability_floor must lie in (0, 0.5)")
|
|
943
|
+
if (
|
|
944
|
+
type(self.scenario_weights) is not tuple
|
|
945
|
+
or len(self.scenario_weights) != 3
|
|
946
|
+
or any(
|
|
947
|
+
type(value) is not float
|
|
948
|
+
or not math.isfinite(value)
|
|
949
|
+
or value <= 0.0
|
|
950
|
+
for value in self.scenario_weights
|
|
951
|
+
)
|
|
952
|
+
or math.fsum(self.scenario_weights) != 1.0
|
|
953
|
+
):
|
|
954
|
+
raise ValueError(
|
|
955
|
+
"scenario_weights must be three positive floats "
|
|
956
|
+
"summing to exactly one"
|
|
957
|
+
)
|
|
958
|
+
_require_token(self.policy_id, name="policy_id")
|
|
959
|
+
if (
|
|
960
|
+
self.policy_id
|
|
961
|
+
!= CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_ID
|
|
962
|
+
or self.policy_version
|
|
963
|
+
!= CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_VERSION
|
|
964
|
+
):
|
|
965
|
+
raise ValueError("policy identity is immutable")
|
|
966
|
+
definition = {
|
|
967
|
+
"schema_version": 1,
|
|
968
|
+
"policy_id": self.policy_id,
|
|
969
|
+
"policy_version": self.policy_version,
|
|
970
|
+
"archive_gain_utility": {
|
|
971
|
+
"utility_id": utility_identity[0],
|
|
972
|
+
"utility_version": utility_identity[1],
|
|
973
|
+
"definition_sha256": utility_identity[2],
|
|
974
|
+
},
|
|
975
|
+
"lambda_hex": self.lambda_.hex(),
|
|
976
|
+
"beta_hex": self.beta.hex(),
|
|
977
|
+
"mixture_weight_hex": self.mixture_weight.hex(),
|
|
978
|
+
"prior_strength_hex": self.prior_strength.hex(),
|
|
979
|
+
"band_count": self.band_count,
|
|
980
|
+
"reference_gain_scale_hex": (
|
|
981
|
+
self.reference_gain_scale.hex()
|
|
982
|
+
),
|
|
983
|
+
"root_prior_probability_hex": (
|
|
984
|
+
self.root_prior_probability.hex()
|
|
985
|
+
),
|
|
986
|
+
"probability_floor_hex": (
|
|
987
|
+
self.probability_floor.hex()
|
|
988
|
+
),
|
|
989
|
+
"scenario_weights_hex": [
|
|
990
|
+
value.hex() for value in self.scenario_weights
|
|
991
|
+
],
|
|
992
|
+
"scenario_ids": list(_QUANTILE_SCENARIO_IDS),
|
|
993
|
+
"frozen_score_weight_hex": (
|
|
994
|
+
self.frozen_score_weight.hex()
|
|
995
|
+
),
|
|
996
|
+
"frozen_score_minimum_training_runs": (
|
|
997
|
+
self.frozen_score_minimum_training_runs
|
|
998
|
+
),
|
|
999
|
+
"frozen_score_evidence": (
|
|
1000
|
+
"weak_log_odds_feature_gated_by_training_"
|
|
1001
|
+
"run_floor_probability_only"
|
|
1002
|
+
),
|
|
1003
|
+
"forecast_geometry": (
|
|
1004
|
+
"archive_conditioned_scenario_quantile_"
|
|
1005
|
+
"nondomination_and_positive_gain"
|
|
1006
|
+
),
|
|
1007
|
+
"conversion_evidence": (
|
|
1008
|
+
"prequential_engine_rank_band_hierarchical_"
|
|
1009
|
+
"beta_shrinkage"
|
|
1010
|
+
),
|
|
1011
|
+
"probability_mixture": "log_odds_blend",
|
|
1012
|
+
"magnitude_mixture": "arithmetic_blend",
|
|
1013
|
+
"hard_abstention": False,
|
|
1014
|
+
"terminal_value_of_information": 0.0,
|
|
1015
|
+
"parent_relative_improvement_counts": False,
|
|
1016
|
+
"eligible_candidate_outcomes_observed": False,
|
|
1017
|
+
"workload_objective_model_provider_prompt_branches": (
|
|
1018
|
+
False
|
|
1019
|
+
),
|
|
1020
|
+
}
|
|
1021
|
+
# Emitted only when active so the r1 definition hash is
|
|
1022
|
+
# preserved bit-for-bit.
|
|
1023
|
+
if self.within_cell_rank_tie_break:
|
|
1024
|
+
definition["within_cell_tie_break"] = (
|
|
1025
|
+
"native_rank_quality_then_action_sha256"
|
|
1026
|
+
)
|
|
1027
|
+
if self.anchor_geometry_tie_break:
|
|
1028
|
+
definition["anchor_geometry_tie_break"] = (
|
|
1029
|
+
"max_min_chebyshev_dispersion_from_bought_anchors_then_"
|
|
1030
|
+
"chebyshev_excess_over_current_archive_front"
|
|
1031
|
+
)
|
|
1032
|
+
if self.downside_shortfall_tail_risk:
|
|
1033
|
+
definition["conversion_tail_risk"] = (
|
|
1034
|
+
"expected_shortfall_below_the_conversion_branch_own_"
|
|
1035
|
+
"expected_gain"
|
|
1036
|
+
)
|
|
1037
|
+
object.__setattr__(
|
|
1038
|
+
self,
|
|
1039
|
+
"definition_sha256",
|
|
1040
|
+
_hash(_DEFINITION_DOMAIN, definition),
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
def _clamped(self, probability: float) -> float:
|
|
1044
|
+
return min(
|
|
1045
|
+
max(probability, self.probability_floor),
|
|
1046
|
+
1.0 - self.probability_floor,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
def _conversion_evidence(
|
|
1050
|
+
self,
|
|
1051
|
+
observed_outcomes: tuple[ObservedConversionOutcome, ...],
|
|
1052
|
+
) -> dict[object, _ConversionCellEvidence]:
|
|
1053
|
+
if type(observed_outcomes) is not tuple or any(
|
|
1054
|
+
type(value) is not ObservedConversionOutcome
|
|
1055
|
+
for value in observed_outcomes
|
|
1056
|
+
):
|
|
1057
|
+
raise TypeError(
|
|
1058
|
+
"observed_outcomes must contain exact conversion outcomes"
|
|
1059
|
+
)
|
|
1060
|
+
ordinals = tuple(
|
|
1061
|
+
value.observation_ordinal for value in observed_outcomes
|
|
1062
|
+
)
|
|
1063
|
+
if ordinals != tuple(sorted(set(ordinals))):
|
|
1064
|
+
raise ValueError(
|
|
1065
|
+
"observed outcomes must use unique ascending ordinals"
|
|
1066
|
+
)
|
|
1067
|
+
counts: dict[object, list[float]] = {}
|
|
1068
|
+
for value in observed_outcomes:
|
|
1069
|
+
value.__post_init__()
|
|
1070
|
+
band = rank_band_index(
|
|
1071
|
+
value.native_rank,
|
|
1072
|
+
value.lane_size,
|
|
1073
|
+
self.band_count,
|
|
1074
|
+
)
|
|
1075
|
+
for key in (
|
|
1076
|
+
None,
|
|
1077
|
+
value.engine_id,
|
|
1078
|
+
(value.engine_id, band),
|
|
1079
|
+
):
|
|
1080
|
+
row = counts.setdefault(key, [0.0, 0.0, 0.0, 0.0])
|
|
1081
|
+
row[0] += 1.0
|
|
1082
|
+
row[1] += float(value.positive)
|
|
1083
|
+
row[2] += float(value.feasible)
|
|
1084
|
+
row[3] += (
|
|
1085
|
+
value.marginal_archive_gain
|
|
1086
|
+
if value.positive
|
|
1087
|
+
else 0.0
|
|
1088
|
+
)
|
|
1089
|
+
return {
|
|
1090
|
+
key: _ConversionCellEvidence(
|
|
1091
|
+
observation_count=row[0],
|
|
1092
|
+
positive_count=row[1],
|
|
1093
|
+
feasible_count=row[2],
|
|
1094
|
+
positive_gain_sum=row[3],
|
|
1095
|
+
)
|
|
1096
|
+
for key, row in counts.items()
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
def _hierarchical_estimates(
|
|
1100
|
+
self,
|
|
1101
|
+
*,
|
|
1102
|
+
evidence: dict[object, _ConversionCellEvidence],
|
|
1103
|
+
engine_id: str,
|
|
1104
|
+
band: int,
|
|
1105
|
+
) -> dict[str, float]:
|
|
1106
|
+
empty = _ConversionCellEvidence(0.0, 0.0, 0.0, 0.0)
|
|
1107
|
+
chain = (
|
|
1108
|
+
evidence.get(None, empty),
|
|
1109
|
+
evidence.get(engine_id, empty),
|
|
1110
|
+
evidence.get((engine_id, band), empty),
|
|
1111
|
+
)
|
|
1112
|
+
p_positive = self.root_prior_probability
|
|
1113
|
+
p_feasible = self.root_prior_probability
|
|
1114
|
+
magnitude = self.reference_gain_scale
|
|
1115
|
+
for level in chain:
|
|
1116
|
+
p_positive = _beta_posterior_mean(
|
|
1117
|
+
prior_mean=p_positive,
|
|
1118
|
+
prior_strength=self.prior_strength,
|
|
1119
|
+
successes=level.positive_count,
|
|
1120
|
+
failures=level.observation_count - level.positive_count,
|
|
1121
|
+
)
|
|
1122
|
+
p_feasible = _beta_posterior_mean(
|
|
1123
|
+
prior_mean=p_feasible,
|
|
1124
|
+
prior_strength=self.prior_strength,
|
|
1125
|
+
successes=level.feasible_count,
|
|
1126
|
+
failures=level.observation_count - level.feasible_count,
|
|
1127
|
+
)
|
|
1128
|
+
magnitude = _shrunk_magnitude(
|
|
1129
|
+
prior_mean=magnitude,
|
|
1130
|
+
prior_strength=self.prior_strength,
|
|
1131
|
+
positive_gain_sum=level.positive_gain_sum,
|
|
1132
|
+
positive_count=level.positive_count,
|
|
1133
|
+
)
|
|
1134
|
+
cell = chain[2]
|
|
1135
|
+
effective_sample_size = (
|
|
1136
|
+
self.prior_strength + cell.observation_count
|
|
1137
|
+
)
|
|
1138
|
+
return {
|
|
1139
|
+
"p_positive": p_positive,
|
|
1140
|
+
"p_feasible": p_feasible,
|
|
1141
|
+
"magnitude": magnitude,
|
|
1142
|
+
"effective_sample_size": effective_sample_size,
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
def _forecast_geometry(
|
|
1146
|
+
self,
|
|
1147
|
+
*,
|
|
1148
|
+
archive_points: tuple[ObjectivePoint, ...],
|
|
1149
|
+
forecast: PositiveGainForecast,
|
|
1150
|
+
) -> dict[str, float]:
|
|
1151
|
+
gains: dict[str, float] = {}
|
|
1152
|
+
for scenario_id in _QUANTILE_SCENARIO_IDS:
|
|
1153
|
+
gain = self.archive_gain_utility.marginal_archive_gain(
|
|
1154
|
+
archive_points,
|
|
1155
|
+
forecast.point(scenario_id),
|
|
1156
|
+
)
|
|
1157
|
+
if type(gain) is not float or not math.isfinite(gain) or gain < 0.0:
|
|
1158
|
+
raise ValueError(
|
|
1159
|
+
"gain port returned an invalid archive gain"
|
|
1160
|
+
)
|
|
1161
|
+
gains[scenario_id] = gain
|
|
1162
|
+
weights = dict(
|
|
1163
|
+
zip(_QUANTILE_SCENARIO_IDS, self.scenario_weights, strict=True)
|
|
1164
|
+
)
|
|
1165
|
+
nondominated = math.fsum(
|
|
1166
|
+
weights[scenario_id]
|
|
1167
|
+
for scenario_id in _QUANTILE_SCENARIO_IDS
|
|
1168
|
+
if gains[scenario_id] > 0.0
|
|
1169
|
+
)
|
|
1170
|
+
positive_weight = nondominated
|
|
1171
|
+
magnitude = (
|
|
1172
|
+
math.fsum(
|
|
1173
|
+
weights[scenario_id] * gains[scenario_id]
|
|
1174
|
+
for scenario_id in _QUANTILE_SCENARIO_IDS
|
|
1175
|
+
if gains[scenario_id] > 0.0
|
|
1176
|
+
)
|
|
1177
|
+
/ positive_weight
|
|
1178
|
+
if positive_weight > 0.0
|
|
1179
|
+
else 0.0
|
|
1180
|
+
)
|
|
1181
|
+
central = gains[CENTRAL_QUANTILE_SCENARIO_ID]
|
|
1182
|
+
shortfall = math.fsum(
|
|
1183
|
+
weights[scenario_id]
|
|
1184
|
+
* max(0.0, central - gains[scenario_id])
|
|
1185
|
+
for scenario_id in _QUANTILE_SCENARIO_IDS
|
|
1186
|
+
)
|
|
1187
|
+
return {
|
|
1188
|
+
"nondominated_fraction": nondominated,
|
|
1189
|
+
"probability": nondominated,
|
|
1190
|
+
"magnitude": magnitude,
|
|
1191
|
+
"shortfall": shortfall,
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
def _logit(self, probability: float) -> float:
|
|
1195
|
+
clamped = self._clamped(probability)
|
|
1196
|
+
return math.log(clamped / (1.0 - clamped))
|
|
1197
|
+
|
|
1198
|
+
def _anchor_geometry(
|
|
1199
|
+
self,
|
|
1200
|
+
*,
|
|
1201
|
+
candidates: tuple[PositiveGainCandidate, ...],
|
|
1202
|
+
archive_points: tuple[ObjectivePoint, ...],
|
|
1203
|
+
covered_anchors: tuple[ObjectivePoint, ...],
|
|
1204
|
+
) -> dict[str, tuple[float, float]]:
|
|
1205
|
+
"""Per-candidate ``(excess, dispersion)`` in pure archive geometry.
|
|
1206
|
+
|
|
1207
|
+
``excess`` is the anchor's Chebyshev excess over the CURRENT archive
|
|
1208
|
+
front: smaller means the parent sits nearer the front, and it is the
|
|
1209
|
+
strongest outcome-blind predictor of realised positivity measured on
|
|
1210
|
+
this corpus.
|
|
1211
|
+
|
|
1212
|
+
``dispersion`` is the smallest max-norm distance from the anchor to
|
|
1213
|
+
any anchor this market has already bought, and ``inf`` when nothing
|
|
1214
|
+
has been bought. A second seat on an identical anchor therefore has
|
|
1215
|
+
dispersion exactly zero -- the redundancy the census measured -- and
|
|
1216
|
+
buying an anchor can only lower the dispersion of the candidates
|
|
1217
|
+
near it, never of the ones far from it, so preferring larger
|
|
1218
|
+
dispersion is submodular in the bought set by construction.
|
|
1219
|
+
|
|
1220
|
+
A candidate with no anchor is absent from the result and takes the
|
|
1221
|
+
market's median position, so an anchorless lane is never
|
|
1222
|
+
systematically evicted or systematically protected.
|
|
1223
|
+
"""
|
|
1224
|
+
|
|
1225
|
+
metric_ids = tuple(
|
|
1226
|
+
metric_id for metric_id, _value in archive_points[0]
|
|
1227
|
+
)
|
|
1228
|
+
covered = tuple(
|
|
1229
|
+
_point_values(value, metric_ids) for value in covered_anchors
|
|
1230
|
+
)
|
|
1231
|
+
result: dict[str, tuple[float, float]] = {}
|
|
1232
|
+
for candidate in candidates:
|
|
1233
|
+
if candidate.anchor_point is None:
|
|
1234
|
+
continue
|
|
1235
|
+
values = _point_values(candidate.anchor_point, metric_ids)
|
|
1236
|
+
dispersion = (
|
|
1237
|
+
min(
|
|
1238
|
+
max(abs(a - b) for a, b in zip(values, other, strict=True))
|
|
1239
|
+
for other in covered
|
|
1240
|
+
)
|
|
1241
|
+
if covered
|
|
1242
|
+
else math.inf
|
|
1243
|
+
)
|
|
1244
|
+
result[candidate.action_sha256] = (
|
|
1245
|
+
chebyshev_excess(
|
|
1246
|
+
candidate.anchor_point,
|
|
1247
|
+
archive_points,
|
|
1248
|
+
metric_ids,
|
|
1249
|
+
),
|
|
1250
|
+
float(dispersion),
|
|
1251
|
+
)
|
|
1252
|
+
return result
|
|
1253
|
+
|
|
1254
|
+
@staticmethod
|
|
1255
|
+
def _median(values: list[float]) -> float:
|
|
1256
|
+
ordered = sorted(values)
|
|
1257
|
+
return ordered[len(ordered) // 2]
|
|
1258
|
+
|
|
1259
|
+
def score_market(
|
|
1260
|
+
self,
|
|
1261
|
+
*,
|
|
1262
|
+
candidates: tuple[PositiveGainCandidate, ...],
|
|
1263
|
+
archive_points: tuple[ObjectivePoint, ...],
|
|
1264
|
+
observed_outcomes: tuple[ObservedConversionOutcome, ...],
|
|
1265
|
+
future_seats_remaining: int,
|
|
1266
|
+
horizon_total: int,
|
|
1267
|
+
frozen_fit_training_run_count: int = 0,
|
|
1268
|
+
covered_anchors: tuple[ObjectivePoint, ...] = (),
|
|
1269
|
+
) -> CalibratedPositiveGainRanking:
|
|
1270
|
+
"""Score and rank the whole market; never abstain."""
|
|
1271
|
+
|
|
1272
|
+
self.__post_init__()
|
|
1273
|
+
if type(candidates) is not tuple or not candidates:
|
|
1274
|
+
raise ValueError("candidates must be a non-empty exact tuple")
|
|
1275
|
+
for value in candidates:
|
|
1276
|
+
if type(value) is not PositiveGainCandidate:
|
|
1277
|
+
raise TypeError(
|
|
1278
|
+
"candidates must contain exact eligible candidates"
|
|
1279
|
+
)
|
|
1280
|
+
value.__post_init__()
|
|
1281
|
+
action_ids = tuple(value.action_sha256 for value in candidates)
|
|
1282
|
+
if len(action_ids) != len(set(action_ids)):
|
|
1283
|
+
raise ValueError("candidate identities repeat")
|
|
1284
|
+
if type(archive_points) is not tuple or not archive_points:
|
|
1285
|
+
raise ValueError(
|
|
1286
|
+
"archive_points must be a non-empty exact tuple"
|
|
1287
|
+
)
|
|
1288
|
+
for value in archive_points:
|
|
1289
|
+
_require_objective_point(value, name="archive point")
|
|
1290
|
+
if (
|
|
1291
|
+
type(future_seats_remaining) is not int
|
|
1292
|
+
or type(horizon_total) is not int
|
|
1293
|
+
or horizon_total <= 0
|
|
1294
|
+
or not 0 <= future_seats_remaining <= horizon_total
|
|
1295
|
+
):
|
|
1296
|
+
raise ValueError(
|
|
1297
|
+
"future seats must fit the positive horizon total"
|
|
1298
|
+
)
|
|
1299
|
+
if (
|
|
1300
|
+
type(frozen_fit_training_run_count) is not int
|
|
1301
|
+
or frozen_fit_training_run_count < 0
|
|
1302
|
+
):
|
|
1303
|
+
raise ValueError(
|
|
1304
|
+
"frozen_fit_training_run_count must be non-negative"
|
|
1305
|
+
)
|
|
1306
|
+
frozen_history_sufficient = (
|
|
1307
|
+
frozen_fit_training_run_count
|
|
1308
|
+
>= self.frozen_score_minimum_training_runs
|
|
1309
|
+
)
|
|
1310
|
+
evidence = self._conversion_evidence(observed_outcomes)
|
|
1311
|
+
if type(covered_anchors) is not tuple:
|
|
1312
|
+
raise TypeError("covered_anchors must be an exact tuple")
|
|
1313
|
+
for value in covered_anchors:
|
|
1314
|
+
_require_objective_point(value, name="covered anchor")
|
|
1315
|
+
anchor_geometry = (
|
|
1316
|
+
self._anchor_geometry(
|
|
1317
|
+
candidates=candidates,
|
|
1318
|
+
archive_points=archive_points,
|
|
1319
|
+
covered_anchors=covered_anchors,
|
|
1320
|
+
)
|
|
1321
|
+
if self.anchor_geometry_tie_break
|
|
1322
|
+
else {}
|
|
1323
|
+
)
|
|
1324
|
+
horizon_fraction = future_seats_remaining / horizon_total
|
|
1325
|
+
effective_beta = self.beta * horizon_fraction
|
|
1326
|
+
scores: list[CalibratedPositiveGainScore] = []
|
|
1327
|
+
for candidate in sorted(
|
|
1328
|
+
candidates,
|
|
1329
|
+
key=lambda value: value.action_sha256,
|
|
1330
|
+
):
|
|
1331
|
+
band = rank_band_index(
|
|
1332
|
+
candidate.native_rank,
|
|
1333
|
+
candidate.lane_size,
|
|
1334
|
+
self.band_count,
|
|
1335
|
+
)
|
|
1336
|
+
conversion = self._hierarchical_estimates(
|
|
1337
|
+
evidence=evidence,
|
|
1338
|
+
engine_id=candidate.engine_id,
|
|
1339
|
+
band=band,
|
|
1340
|
+
)
|
|
1341
|
+
p_conversion = conversion["p_positive"]
|
|
1342
|
+
conversion_magnitude = conversion["magnitude"]
|
|
1343
|
+
if candidate.forecast is None:
|
|
1344
|
+
geometry: dict[str, float] | None = None
|
|
1345
|
+
raw_forecast_weight = 0.0
|
|
1346
|
+
else:
|
|
1347
|
+
geometry = self._forecast_geometry(
|
|
1348
|
+
archive_points=archive_points,
|
|
1349
|
+
forecast=candidate.forecast,
|
|
1350
|
+
)
|
|
1351
|
+
raw_forecast_weight = (
|
|
1352
|
+
self.mixture_weight
|
|
1353
|
+
* candidate.forecast.reliability
|
|
1354
|
+
)
|
|
1355
|
+
raw_conversion_weight = 1.0 - self.mixture_weight
|
|
1356
|
+
frozen_active = (
|
|
1357
|
+
candidate.frozen_score is not None
|
|
1358
|
+
and frozen_history_sufficient
|
|
1359
|
+
and self.frozen_score_weight > 0.0
|
|
1360
|
+
)
|
|
1361
|
+
raw_frozen_weight = (
|
|
1362
|
+
self.frozen_score_weight if frozen_active else 0.0
|
|
1363
|
+
)
|
|
1364
|
+
anchor = anchor_geometry.get(candidate.action_sha256)
|
|
1365
|
+
probability_total = (
|
|
1366
|
+
raw_forecast_weight
|
|
1367
|
+
+ raw_conversion_weight
|
|
1368
|
+
+ raw_frozen_weight
|
|
1369
|
+
)
|
|
1370
|
+
if (
|
|
1371
|
+
geometry is None and not frozen_active
|
|
1372
|
+
) or probability_total <= 0.0:
|
|
1373
|
+
# Only conversion evidence is active: keep the exact
|
|
1374
|
+
# hierarchical posterior instead of a clamped round trip.
|
|
1375
|
+
p_positive = p_conversion
|
|
1376
|
+
else:
|
|
1377
|
+
blended_logit = (
|
|
1378
|
+
raw_conversion_weight
|
|
1379
|
+
* self._logit(p_conversion)
|
|
1380
|
+
+ (
|
|
1381
|
+
raw_forecast_weight
|
|
1382
|
+
* self._logit(geometry["probability"])
|
|
1383
|
+
if geometry is not None
|
|
1384
|
+
else 0.0
|
|
1385
|
+
)
|
|
1386
|
+
+ (
|
|
1387
|
+
raw_frozen_weight
|
|
1388
|
+
* self._logit(candidate.frozen_score)
|
|
1389
|
+
if frozen_active
|
|
1390
|
+
else 0.0
|
|
1391
|
+
)
|
|
1392
|
+
) / probability_total
|
|
1393
|
+
p_positive = 1.0 / (1.0 + math.exp(-blended_logit))
|
|
1394
|
+
frozen_evidence_weight = (
|
|
1395
|
+
raw_frozen_weight / probability_total
|
|
1396
|
+
if frozen_active
|
|
1397
|
+
else 0.0
|
|
1398
|
+
)
|
|
1399
|
+
# Magnitudes and tail risk come only from forecast geometry
|
|
1400
|
+
# and conversion evidence; a frozen rank prior carries no
|
|
1401
|
+
# gain scale.
|
|
1402
|
+
magnitude_total = raw_forecast_weight + raw_conversion_weight
|
|
1403
|
+
forecast_weight = (
|
|
1404
|
+
raw_forecast_weight / magnitude_total
|
|
1405
|
+
if magnitude_total > 0.0
|
|
1406
|
+
else 0.0
|
|
1407
|
+
)
|
|
1408
|
+
conversion_weight = 1.0 - forecast_weight
|
|
1409
|
+
# The conversion branch's own downside. Both forms are the
|
|
1410
|
+
# weighted mass of scenarios that fall short of a central
|
|
1411
|
+
# outcome; they differ only in which central outcome, and the
|
|
1412
|
+
# shortfall form is the one that cannot invert the magnitude.
|
|
1413
|
+
conversion_shortfall = (
|
|
1414
|
+
p_conversion
|
|
1415
|
+
* (1.0 - p_conversion)
|
|
1416
|
+
* conversion_magnitude
|
|
1417
|
+
if self.downside_shortfall_tail_risk
|
|
1418
|
+
else (1.0 - p_conversion) * conversion_magnitude
|
|
1419
|
+
)
|
|
1420
|
+
if geometry is None:
|
|
1421
|
+
expected_positive_gain = conversion_magnitude
|
|
1422
|
+
tail_risk = conversion_weight * conversion_shortfall
|
|
1423
|
+
else:
|
|
1424
|
+
expected_positive_gain = (
|
|
1425
|
+
forecast_weight * geometry["magnitude"]
|
|
1426
|
+
+ conversion_weight * conversion_magnitude
|
|
1427
|
+
)
|
|
1428
|
+
tail_risk = (
|
|
1429
|
+
forecast_weight * geometry["shortfall"]
|
|
1430
|
+
+ conversion_weight * conversion_shortfall
|
|
1431
|
+
)
|
|
1432
|
+
uncertainty = math.sqrt(
|
|
1433
|
+
p_conversion
|
|
1434
|
+
* (1.0 - p_conversion)
|
|
1435
|
+
/ (conversion["effective_sample_size"] + 1.0)
|
|
1436
|
+
)
|
|
1437
|
+
value_of_information = (
|
|
1438
|
+
uncertainty
|
|
1439
|
+
* max(
|
|
1440
|
+
expected_positive_gain,
|
|
1441
|
+
self.reference_gain_scale,
|
|
1442
|
+
)
|
|
1443
|
+
if future_seats_remaining > 0
|
|
1444
|
+
else 0.0
|
|
1445
|
+
)
|
|
1446
|
+
score = (
|
|
1447
|
+
p_positive * expected_positive_gain
|
|
1448
|
+
- self.lambda_ * tail_risk
|
|
1449
|
+
+ effective_beta * value_of_information
|
|
1450
|
+
)
|
|
1451
|
+
scores.append(
|
|
1452
|
+
CalibratedPositiveGainScore(
|
|
1453
|
+
action_sha256=candidate.action_sha256,
|
|
1454
|
+
p_feasible=conversion["p_feasible"],
|
|
1455
|
+
p_positive_archive_gain=float(p_positive),
|
|
1456
|
+
expected_positive_gain=float(
|
|
1457
|
+
expected_positive_gain
|
|
1458
|
+
),
|
|
1459
|
+
tail_risk=float(tail_risk),
|
|
1460
|
+
value_of_information=float(value_of_information),
|
|
1461
|
+
uncertainty=float(uncertainty),
|
|
1462
|
+
effective_sample_size=float(
|
|
1463
|
+
conversion["effective_sample_size"]
|
|
1464
|
+
),
|
|
1465
|
+
score=float(score),
|
|
1466
|
+
forecast_probability=(
|
|
1467
|
+
None
|
|
1468
|
+
if geometry is None
|
|
1469
|
+
else float(geometry["probability"])
|
|
1470
|
+
),
|
|
1471
|
+
forecast_magnitude=(
|
|
1472
|
+
None
|
|
1473
|
+
if geometry is None
|
|
1474
|
+
else float(geometry["magnitude"])
|
|
1475
|
+
),
|
|
1476
|
+
forecast_nondominated_fraction=(
|
|
1477
|
+
None
|
|
1478
|
+
if geometry is None
|
|
1479
|
+
else float(geometry["nondominated_fraction"])
|
|
1480
|
+
),
|
|
1481
|
+
conversion_probability=float(p_conversion),
|
|
1482
|
+
conversion_magnitude=float(conversion_magnitude),
|
|
1483
|
+
frozen_score_probability=candidate.frozen_score,
|
|
1484
|
+
frozen_evidence_weight=float(
|
|
1485
|
+
frozen_evidence_weight
|
|
1486
|
+
),
|
|
1487
|
+
anchor_excess=(
|
|
1488
|
+
None if anchor is None else float(anchor[0])
|
|
1489
|
+
),
|
|
1490
|
+
anchor_dispersion=(
|
|
1491
|
+
None if anchor is None else float(anchor[1])
|
|
1492
|
+
),
|
|
1493
|
+
)
|
|
1494
|
+
)
|
|
1495
|
+
rank_quality_by_action = {
|
|
1496
|
+
value.action_sha256: (
|
|
1497
|
+
1.0
|
|
1498
|
+
if value.lane_size == 1
|
|
1499
|
+
else 1.0
|
|
1500
|
+
- (value.native_rank - 1)
|
|
1501
|
+
/ float(value.lane_size - 1)
|
|
1502
|
+
)
|
|
1503
|
+
for value in candidates
|
|
1504
|
+
}
|
|
1505
|
+
# Candidates the score cannot separate are separated here. An
|
|
1506
|
+
# anchorless candidate takes the market's median position on both
|
|
1507
|
+
# geometric keys, so no lane is systematically evicted.
|
|
1508
|
+
if self.anchor_geometry_tie_break and anchor_geometry:
|
|
1509
|
+
known = list(anchor_geometry.values())
|
|
1510
|
+
median_excess = self._median([value[0] for value in known])
|
|
1511
|
+
median_dispersion = self._median(
|
|
1512
|
+
[value[1] for value in known]
|
|
1513
|
+
)
|
|
1514
|
+
else:
|
|
1515
|
+
median_excess = 0.0
|
|
1516
|
+
median_dispersion = math.inf
|
|
1517
|
+
|
|
1518
|
+
def geometry_keys(action_sha256: str) -> tuple[float, float]:
|
|
1519
|
+
if not self.anchor_geometry_tie_break:
|
|
1520
|
+
return (0.0, 0.0)
|
|
1521
|
+
excess, dispersion = anchor_geometry.get(
|
|
1522
|
+
action_sha256,
|
|
1523
|
+
(median_excess, median_dispersion),
|
|
1524
|
+
)
|
|
1525
|
+
# Larger dispersion first (novel region), then smaller excess
|
|
1526
|
+
# (parent nearer the front).
|
|
1527
|
+
return (-dispersion, excess)
|
|
1528
|
+
|
|
1529
|
+
ranked = tuple(
|
|
1530
|
+
value.action_sha256
|
|
1531
|
+
for value in sorted(
|
|
1532
|
+
scores,
|
|
1533
|
+
key=lambda value: (
|
|
1534
|
+
-value.score,
|
|
1535
|
+
-value.p_positive_archive_gain,
|
|
1536
|
+
*geometry_keys(value.action_sha256),
|
|
1537
|
+
(
|
|
1538
|
+
-rank_quality_by_action[value.action_sha256]
|
|
1539
|
+
if self.within_cell_rank_tie_break
|
|
1540
|
+
else 0.0
|
|
1541
|
+
),
|
|
1542
|
+
value.action_sha256,
|
|
1543
|
+
),
|
|
1544
|
+
)
|
|
1545
|
+
)
|
|
1546
|
+
return CalibratedPositiveGainRanking(
|
|
1547
|
+
policy_id=self.policy_id,
|
|
1548
|
+
policy_version=self.policy_version,
|
|
1549
|
+
policy_definition_sha256=self.definition_sha256,
|
|
1550
|
+
archive_sha256=_hash(
|
|
1551
|
+
_ARCHIVE_DOMAIN,
|
|
1552
|
+
[
|
|
1553
|
+
_objective_point_record(value)
|
|
1554
|
+
for value in archive_points
|
|
1555
|
+
],
|
|
1556
|
+
),
|
|
1557
|
+
future_seats_remaining=future_seats_remaining,
|
|
1558
|
+
horizon_total=horizon_total,
|
|
1559
|
+
scores=tuple(scores),
|
|
1560
|
+
ranked_action_sha256s=ranked,
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
__all__ = [
|
|
1565
|
+
"CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_ID",
|
|
1566
|
+
"CALIBRATED_POSITIVE_GAIN_OPPORTUNITY_POLICY_VERSION",
|
|
1567
|
+
"CENTRAL_QUANTILE_SCENARIO_ID",
|
|
1568
|
+
"LOWER_QUANTILE_SCENARIO_ID",
|
|
1569
|
+
"UPPER_QUANTILE_SCENARIO_ID",
|
|
1570
|
+
"ArchiveConditionedGainPort",
|
|
1571
|
+
"CalibratedPositiveGainOpportunityPolicy",
|
|
1572
|
+
"CalibratedPositiveGainRanking",
|
|
1573
|
+
"CalibratedPositiveGainScore",
|
|
1574
|
+
"ObjectivePoint",
|
|
1575
|
+
"ObservedConversionOutcome",
|
|
1576
|
+
"PositiveGainCandidate",
|
|
1577
|
+
"PositiveGainForecast",
|
|
1578
|
+
"chebyshev_excess",
|
|
1579
|
+
"non_dominated",
|
|
1580
|
+
"validate_archive_conditioned_gain_port",
|
|
1581
|
+
]
|