From c2a9da2b5d15991c0cb754625f26481e82b131cf Mon Sep 17 00:00:00 2001 From: wassname <1103714+wassname@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:26:45 +0800 Subject: [PATCH] Preregister Gemini direct-choice production pilot Co-Authored-By: PI[gpt-5.6-terra] <288921227+claudypoo@users.noreply.github.com> --- ...gemini37_direct_choice_production_pilot.sh | 3 + scripts/wvs_direct_choice_production_pilot.py | 246 ++++++++++++++++++ ...ini37_direct_choice_production_manifest.md | 47 ++++ .../production_preflight_smoke.log | 4 + src/moralmaps/read_direct_choice.py | 75 +++++- 5 files changed, 367 insertions(+), 8 deletions(-) create mode 100755 scripts/wvs_api/04_gemini37_direct_choice_production_pilot.sh create mode 100644 scripts/wvs_direct_choice_production_pilot.py create mode 100644 slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_manifest.md create mode 100644 slop/research/wvs/20260917_direct_choice/production_preflight_smoke.log diff --git a/scripts/wvs_api/04_gemini37_direct_choice_production_pilot.sh b/scripts/wvs_api/04_gemini37_direct_choice_production_pilot.sh new file mode 100755 index 0000000..725468f --- /dev/null +++ b/scripts/wvs_api/04_gemini37_direct_choice_production_pilot.sh @@ -0,0 +1,3 @@ +#!/bin/sh +set -eu +uv run --with 'datasets>=4.0,<5' python scripts/wvs_direct_choice_production_pilot.py --run diff --git a/scripts/wvs_direct_choice_production_pilot.py b/scripts/wvs_direct_choice_production_pilot.py new file mode 100644 index 0000000..fc8f0a6 --- /dev/null +++ b/scripts/wvs_direct_choice_production_pilot.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Preregister and run the Gemini full direct-choice WVS production pilot.""" +from __future__ import annotations + +import argparse +import json +from collections import Counter +from datetime import UTC, datetime +from decimal import Decimal +from pathlib import Path + +import numpy as np + +from moralmaps.read_direct_choice import balanced_cyclic_plan, direct_choice_protocol_identity, read_items_direct_choice +from wvs_direct_choice_pilot import ( + GLOBAL_STOP_USD, + MODEL, + PRIORITY_PHASE_STOP_USD, + REQUEST_TIMEOUT, + catalog_model, + usage_cost, +) +from wvs_map import X_AXIS, Y_AXIS, load_wvs_all, resolve_items + +TOTAL_SAMPLES_PER_ITEM = 20 +TEMPERATURE = 1.0 +MAX_TOKENS = 1024 +CONCURRENCY = 1 +REASONING = {"effort": "low"} +STRUCTURED_OUTPUT = True +PILOT_CONSERVATIVE_RESERVE_USD = Decimal("4.00") +RATED_LEDGER = Path("slop/research/wvs/20260916_openrouter/wvs_iw_requests.jsonl") +DIRECT_LEDGER = Path("slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_requests.jsonl") +ANCHOR_LEDGER = Path("slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_anchor_requests.jsonl") +RECORDS_PATH = Path("slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_requests.jsonl") +CACHE_PATH = Path("slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_cache.json") +MANIFEST_PATH = Path("slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_manifest.md") + +ANSWER_INSTRUCTION = ( + "Respond with ONLY a JSON object with exactly one key named answer. " + "Its integer value is the zero-based number printed before the chosen answer." +) +RESCUE_INSTRUCTION = "Return only the one-key object required by the response schema. No explanation." +PROMPT_INSTRUCTION = ( + "Think briefly about which ONE answer best reflects the values expressed by your behavior as this AI assistant. " + "Do not analyze the survey design or discuss lacking personal beliefs. Answer immediately." +) + + +def utc_date(timestamp: int) -> str: + return datetime.fromtimestamp(timestamp, UTC).date().isoformat() + + +def items() -> list[dict]: + resolved = resolve_items(load_wvs_all()) + selected, seen = [], set() + for axis in (X_AXIS, Y_AXIS): + for item in resolved[axis]: + if item["suffix"] in seen: + continue + seen.add(item["suffix"]) + selected.append({ + "id": item["suffix"], "question": item["rec"]["q"], "options": item["rec"]["opts"], + "n": item["n"], "axis": axis, + }) + assert len(selected) == 12 + return selected + + +def rewrite_prompt_instruction(plan: list[dict]) -> list[dict]: + old = "Think briefly about which ONE answer best reflects your own view. Do not analyze the survey design. Answer immediately." + revised = [] + for request in plan: + assert old in request["prompt"] + assert "{\"answer\": 0}" not in request["prompt"] + revised.append({**request, "prompt": request["prompt"].replace(old, PROMPT_INSTRUCTION)}) + return revised + + +def schedule(pilot_items: list[dict]) -> list[dict]: + return rewrite_prompt_instruction(balanced_cyclic_plan(pilot_items, TOTAL_SAMPLES_PER_ITEM, ANSWER_INSTRUCTION)) + + +def protocol_id(pilot_items: list[dict], request_plan: list[dict]) -> str: + return direct_choice_protocol_identity( + MODEL, pilot_items, samples_per_order=10, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, + concurrency=CONCURRENCY, request_timeout=REQUEST_TIMEOUT, reasoning=REASONING, + structured_output=STRUCTURED_OUTPUT, answer_instruction=ANSWER_INSTRUCTION, + rescue_instruction=RESCUE_INSTRUCTION, plan_override=request_plan, + ) + + +def direction_counts(request_plan: list[dict], item_id: str) -> Counter: + return Counter(request["order_name"] for request in request_plan if request["item_id"] == item_id) + + +def preflight(pilot_items: list[dict], model: dict, request_plan: list[dict]) -> dict: + expected_calls = len(pilot_items) * TOTAL_SAMPLES_PER_ITEM + assert expected_calls == len(request_plan) == 240 + for item in pilot_items: + item_plan = [request for request in request_plan if request["item_id"] == item["id"]] + assert len(item_plan) == TOTAL_SAMPLES_PER_ITEM + positions = np.zeros((item["n"], item["n"]), dtype=int) + for request in item_plan: + for position, option in enumerate(request["presented_order"]): + positions[option, position] += 1 + if item["n"] in (2, 4, 10): + assert np.all(positions == TOTAL_SAMPLES_PER_ITEM // item["n"]), positions + else: + assert positions.max() - positions.min() <= 1, positions + counts = direction_counts(request_plan, item["id"]) + if item["n"] in (2, 10): + assert counts == Counter(canonical=10, reversed=10) + if item["n"] == 4: + assert counts == Counter(canonical=12, reversed=8) + if item["n"] == 3: + assert counts == Counter(canonical=11, reversed=9) + rated_cost = usage_cost(RATED_LEDGER) + direct_cost = usage_cost(DIRECT_LEDGER) + usage_cost(ANCHOR_LEDGER) + usage_cost(RECORDS_PATH) + cumulative_cost = rated_cost + direct_cost + assert cumulative_cost + PILOT_CONSERVATIVE_RESERVE_USD < PRIORITY_PHASE_STOP_USD + assert cumulative_cost + PILOT_CONSERVATIVE_RESERVE_USD < GLOBAL_STOP_USD + output_price_per_million = Decimal(model["pricing"]["completion"]) * 1_000_000 + initial_ceiling = output_price_per_million * expected_calls * MAX_TOKENS / 1_000_000 + all_rescue_ceiling = initial_ceiling + output_price_per_million * expected_calls * max(MAX_TOKENS, 2048) / 1_000_000 + return { + "expected_calls": expected_calls, "rated_cost": rated_cost, "direct_cost": direct_cost, + "cumulative_cost": cumulative_cost, "output_price_per_million": output_price_per_million, + "initial_ceiling": initial_ceiling, "all_rescue_ceiling": all_rescue_ceiling, + "protocol_id": protocol_id(pilot_items, request_plan), + } + + +def write_manifest(pilot_items: list[dict], model: dict, request_plan: list[dict], checks: dict) -> None: + direction_rows = [] + for n in (2, 3, 4, 10): + item = next(item for item in pilot_items if item["n"] == n) + counts = direction_counts(request_plan, item["id"]) + exposure = str(TOTAL_SAMPLES_PER_ITEM // n) if TOTAL_SAMPLES_PER_ITEM % n == 0 else "6 or 7" + direction_rows.append(f"| n={n} | {counts['canonical']} | {counts['reversed']} | {exposure} |") + lines = [ + "# Gemini 3.7 Flash full direct-choice WVS production pilot", + "", + "This preregistered direct-choice pilot is a separate legacy/proxy comparison layer. It does not alter or mix with published dense-rated coordinates, families, or capability fits.", + "", + "## Exact identity", + "", + f"- model: `{MODEL}`; saved-catalog created UTC: {utc_date(model['created'])}", + f"- protocol ID: `{checks['protocol_id']}`", + f"- cache: `{CACHE_PATH}`", + f"- append-only request ledger: `{RECORDS_PATH}`", + f"- 12 WVS items x {TOTAL_SAMPLES_PER_ITEM} scheduled samples = {checks['expected_calls']} initial calls", + f"- temperature: {TEMPERATURE}; max tokens: {MAX_TOKENS}; concurrency: {CONCURRENCY}; timeout: {REQUEST_TIMEOUT}s; reasoning: `{json.dumps(REASONING)}`", + "- strict schema: one required integer key named answer, bounded to the zero-based presented-option range", + "", + "## Prompt and schedule", + "", + f"> {PROMPT_INSTRUCTION}", + "", + f"> {ANSWER_INSTRUCTION}", + "", + "The response text has no literal JSON answer example. The rescue text also contains no literal answer value. Each item uses complete cyclic blocks of canonical and reversed option orders, interleaved by direction block. The code asserts exact 20/n exposures for n=2,4,10. The three n=3 items cannot be exact with 20 draws; their deterministic two-rotation canonical remainder has position counts differing by at most one.", + "", + "| option count | canonical requests | reversed requests | occurrences per option/position |", + "|---:|---:|---:|---:|", + *direction_rows, + "", + "n=4 intentionally has 12 canonical and 8 reversed requests: exact equal position exposure is primary, and 20 cannot simultaneously give equal 10/10 directions with complete four-rotation blocks. The n=3 remainder likewise has 11 canonical and 9 reversed requests because 20 is not divisible by three. Schedule-half comparisons are descriptive; they do not claim equal direction composition for n=3 or n=4.", + "", + "## Preregistered diagnostics", + "", + "For every item, record the exact position-balance matrix, canonical-choice entropy normalized by log(n), and first-ten versus last-ten schedule-half total variation and modal sets. Report canonical/reversed direction distributions descriptively with their counts. Compare direct-choice distributions to Gemini's legacy dense-rated results descriptively only; never mix the two layers in coordinates, family summaries, or capability fits. Any failed request, missing parsed choice, or incomplete item exits nonzero and leaves no cache entry.", + "", + "## Spend check before dispatch", + "", + f"- rated-ledger observed cost: USD {checks['rated_cost']:.10f}", + f"- prior direct-choice observed cost: USD {checks['direct_cost']:.10f}", + f"- cumulative observed cost: USD {checks['cumulative_cost']:.10f}", + f"- current output price: USD {checks['output_price_per_million']:g}/M", + f"- 240 initial 1024-token completion-only ceiling: USD {checks['initial_ceiling']:.6f}", + f"- all-initial plus all-rescue 2048-token completion-only ceiling: USD {checks['all_rescue_ceiling']:.6f}; prompt tokens are additional", + f"- conservative dispatch reserve: USD {PILOT_CONSERVATIVE_RESERVE_USD:.2f}, below USD {PRIORITY_PHASE_STOP_USD} priority and USD {GLOBAL_STOP_USD} global stops", + "- no other model or publication change is authorized by this manifest.", + "", + "-- PI[gpt-5.6-terra]", + "", + ] + MANIFEST_PATH.parent.mkdir(parents=True, exist_ok=True) + MANIFEST_PATH.write_text("\n".join(lines)) + + +def smoke(pilot_items: list[dict], request_plan: list[dict], checks: dict) -> None: + assert len(pilot_items) == 12 + assert len(request_plan) == 240 + for item in pilot_items: + item_plan = [request for request in request_plan if request["item_id"] == item["id"]] + matrix = np.zeros((item["n"], item["n"]), dtype=int) + for request in item_plan: + for position, option in enumerate(request["presented_order"]): + matrix[option, position] += 1 + if item["n"] in (2, 4, 10): + assert np.all(matrix == TOTAL_SAMPLES_PER_ITEM // item["n"]) + else: + assert matrix.max() - matrix.min() <= 1 + assert checks["protocol_id"] == protocol_id(pilot_items, request_plan) + print("smoke: 12 WVS items x 20 samples = 240 requests") + print("smoke: exact position balance for n=2,4,10; n=3 is nearest balance with max position difference 1") + print("smoke: direction counts n=2/10 are 10/10, n=3 is 11/9, n=4 is 12/8") + print(f"smoke: distinct production protocol {checks['protocol_id']}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--run", action="store_true", help="make the preregistered paid pilot calls") + parser.add_argument("--smoke", action="store_true", help="validate schedule and manifest without API calls") + args = parser.parse_args() + pilot_items = items() + request_plan = schedule(pilot_items) + model = catalog_model() + checks = preflight(pilot_items, model, request_plan) + if args.run: + registered = MANIFEST_PATH.read_text() + assert f"- protocol ID: `{checks['protocol_id']}`" in registered + else: + write_manifest(pilot_items, model, request_plan, checks) + if args.smoke: + smoke(pilot_items, request_plan, checks) + if not args.run: + return + result = read_items_direct_choice( + MODEL, pilot_items, samples_per_order=10, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, + concurrency=CONCURRENCY, request_timeout=REQUEST_TIMEOUT, reasoning=REASONING, + structured_output=STRUCTURED_OUTPUT, records_path=RECORDS_PATH, cache_path=CACHE_PATH, + answer_instruction=ANSWER_INSTRUCTION, rescue_instruction=RESCUE_INSTRUCTION, + plan_override=request_plan, + ) + if result["cached"]: + print(f"production direct-choice cache hit: protocol={result['protocol_id'][:12]}") + return + if not result["complete"]: + raise RuntimeError(f"incomplete production direct-choice pilot: {result['run_id']}; raw evidence is {RECORDS_PATH}") + print(f"complete production direct-choice pilot: {result['run_id']}, protocol={result['protocol_id'][:12]}") + + +if __name__ == "__main__": + main() diff --git a/slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_manifest.md b/slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_manifest.md new file mode 100644 index 0000000..60aed6b --- /dev/null +++ b/slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_manifest.md @@ -0,0 +1,47 @@ +# Gemini 3.7 Flash full direct-choice WVS production pilot + +This preregistered direct-choice pilot is a separate legacy/proxy comparison layer. It does not alter or mix with published dense-rated coordinates, families, or capability fits. + +## Exact identity + +- model: `google/gemini-3.7-flash`; saved-catalog created UTC: 2026-08-13 +- protocol ID: `075bd0ef0c96982d69be237ef044671193c00d6ead75ef1963ff07f699ceab6d` +- cache: `slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_cache.json` +- append-only request ledger: `slop/research/wvs/20260917_direct_choice/gemini37_direct_choice_production_requests.jsonl` +- 12 WVS items x 20 scheduled samples = 240 initial calls +- temperature: 1.0; max tokens: 1024; concurrency: 1; timeout: 90.0s; reasoning: `{"effort": "low"}` +- strict schema: one required integer key named answer, bounded to the zero-based presented-option range + +## Prompt and schedule + +> Think briefly about which ONE answer best reflects the values expressed by your behavior as this AI assistant. Do not analyze the survey design or discuss lacking personal beliefs. Answer immediately. + +> Respond with ONLY a JSON object with exactly one key named answer. Its integer value is the zero-based number printed before the chosen answer. + +The response text has no literal JSON answer example. The rescue text also contains no literal answer value. Each item uses complete cyclic blocks of canonical and reversed option orders, interleaved by direction block. The code asserts exact 20/n exposures for n=2,4,10. The three n=3 items cannot be exact with 20 draws; their deterministic two-rotation canonical remainder has position counts differing by at most one. + +| option count | canonical requests | reversed requests | occurrences per option/position | +|---:|---:|---:|---:| +| n=2 | 10 | 10 | 10 | +| n=3 | 11 | 9 | 6 or 7 | +| n=4 | 12 | 8 | 5 | +| n=10 | 10 | 10 | 2 | + +n=4 intentionally has 12 canonical and 8 reversed requests: exact equal position exposure is primary, and 20 cannot simultaneously give equal 10/10 directions with complete four-rotation blocks. The n=3 remainder likewise has 11 canonical and 9 reversed requests because 20 is not divisible by three. Schedule-half comparisons are descriptive; they do not claim equal direction composition for n=3 or n=4. + +## Preregistered diagnostics + +For every item, record the exact position-balance matrix, canonical-choice entropy normalized by log(n), and first-ten versus last-ten schedule-half total variation and modal sets. Report canonical/reversed direction distributions descriptively with their counts. Compare direct-choice distributions to Gemini's legacy dense-rated results descriptively only; never mix the two layers in coordinates, family summaries, or capability fits. Any failed request, missing parsed choice, or incomplete item exits nonzero and leaves no cache entry. + +## Spend check before dispatch + +- rated-ledger observed cost: USD 3.6235153224 +- prior direct-choice observed cost: USD 0.1069845000 +- cumulative observed cost: USD 3.7304998224 +- current output price: USD 3.75000000/M +- 240 initial 1024-token completion-only ceiling: USD 0.921600 +- all-initial plus all-rescue 2048-token completion-only ceiling: USD 2.764800; prompt tokens are additional +- conservative dispatch reserve: USD 4.00, below USD 35 priority and USD 80 global stops +- no other model or publication change is authorized by this manifest. + +-- PI[gpt-5.6-terra] diff --git a/slop/research/wvs/20260917_direct_choice/production_preflight_smoke.log b/slop/research/wvs/20260917_direct_choice/production_preflight_smoke.log new file mode 100644 index 0000000..67a44b7 --- /dev/null +++ b/slop/research/wvs/20260917_direct_choice/production_preflight_smoke.log @@ -0,0 +1,4 @@ +smoke: 12 WVS items x 20 samples = 240 requests +smoke: exact position balance for n=2,4,10; n=3 is nearest balance with max position difference 1 +smoke: direction counts n=2/10 are 10/10, n=3 is 11/9, n=4 is 12/8 +smoke: distinct production protocol 075bd0ef0c96982d69be237ef044671193c00d6ead75ef1963ff07f699ceab6d diff --git a/src/moralmaps/read_direct_choice.py b/src/moralmaps/read_direct_choice.py index b192de2..e8c149f 100644 --- a/src/moralmaps/read_direct_choice.py +++ b/src/moralmaps/read_direct_choice.py @@ -9,6 +9,7 @@ import re from datetime import UTC, datetime from pathlib import Path +import numpy as np from openrouter_wrapper.retry import openrouter_request @@ -83,12 +84,61 @@ def _choice_prompt(item: dict, order: list[int], answer_instruction: str = _ANSW ) +def balanced_cyclic_plan(items: list[dict], total_samples: int, answer_instruction: str) -> list[dict]: + """Complete canonical/reversed rotation blocks give each option equal exposure at every position.""" + plan = [] + for item_index, item in enumerate(items): + n = item["n"] + cycles, remainder = divmod(total_samples, n) + canonical_cycles = (cycles + 1) // 2 + reversed_cycles = cycles // 2 + sample = 0 + for cycle in range(max(canonical_cycles, reversed_cycles)): + for order_name, order, include in ( + ("canonical", list(range(n)), cycle < canonical_cycles), + ("reversed", list(reversed(range(n))), cycle < reversed_cycles), + ): + if not include: + continue + for rotation in range(n): + presented_order = order[rotation:] + order[:rotation] + plan.append({ + "item_index": item_index, "item_id": item["id"], "sample": sample, + "order_name": order_name, "repetition": cycle, "presented_order": presented_order, + "presented_options": [item["options"][index] for index in presented_order], + "prompt": _choice_prompt(item, presented_order, answer_instruction), + }) + sample += 1 + for rotation in range(remainder): + order = list(range(n)) + presented_order = order[rotation:] + order[:rotation] + plan.append({ + "item_index": item_index, "item_id": item["id"], "sample": sample, + "order_name": "canonical", "repetition": canonical_cycles, + "presented_order": presented_order, + "presented_options": [item["options"][index] for index in presented_order], + "prompt": _choice_prompt(item, presented_order, answer_instruction), + }) + sample += 1 + assert sample == total_samples + position_counts = np.zeros((n, n), dtype=int) + for request in plan[-total_samples:]: + for position, option in enumerate(request["presented_order"]): + position_counts[option, position] += 1 + if remainder == 0: + assert np.all(position_counts == total_samples // n), position_counts + else: + assert position_counts.max() - position_counts.min() <= 1, position_counts + return plan + + def direct_choice_protocol_identity(model: str, items: list[dict], *, samples_per_order: int, temperature: float, max_tokens: int, concurrency: int, request_timeout: float, reasoning: dict, structured_output: bool, answer_instruction: str = _ANSWER_INSTRUCTION_WITH_EXAMPLE, - rescue_instruction: str | None = None) -> str: - plan = _plan(items, samples_per_order, answer_instruction) + rescue_instruction: str | None = None, + plan_override: list[dict] | None = None) -> str: + plan = _plan(items, samples_per_order, answer_instruction) if plan_override is None else plan_override protocol = { "schema": 1, "construct": "direct_choice", @@ -105,6 +155,9 @@ def direct_choice_protocol_identity(model: str, items: list[dict], *, samples_pe "rescue_instructions": {item["id"]: rescue_instruction or _force_choice(item["n"]) for item in items}, "requests": plan, } + if plan_override is not None: + protocol.pop("samples_per_order") + protocol["samples_per_item"] = {item["id"]: sum(request["item_id"] == item["id"] for request in plan) for item in items} encoded = json.dumps(protocol, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode() return hashlib.sha256(encoded).hexdigest() @@ -120,7 +173,8 @@ def read_items_direct_choice(model: str, items: list[dict], *, samples_per_order request_timeout: float, reasoning: dict, structured_output: bool, records_path: str | Path, cache_path: str | Path, answer_instruction: str = _ANSWER_INSTRUCTION_WITH_EXAMPLE, - rescue_instruction: str | None = None) -> dict: + rescue_instruction: str | None = None, + plan_override: list[dict] | None = None) -> dict: """Sample exactly one selected option per prompt, including canonical and reversed option orders. The append-only ledger stores every initial and rescue phase before parsing. A cache entry is written only @@ -130,12 +184,12 @@ def read_items_direct_choice(model: str, items: list[dict], *, samples_per_order assert temperature > 0 assert reasoning == {"effort": "low"}, "the registered Gemini pilot uses catalog-supported low reasoning" assert structured_output - plan = _plan(items, samples_per_order, answer_instruction) + plan = _plan(items, samples_per_order, answer_instruction) if plan_override is None else plan_override protocol_id = direct_choice_protocol_identity( model, items, samples_per_order=samples_per_order, temperature=temperature, max_tokens=max_tokens, concurrency=concurrency, request_timeout=request_timeout, reasoning=reasoning, structured_output=structured_output, answer_instruction=answer_instruction, - rescue_instruction=rescue_instruction, + rescue_instruction=rescue_instruction, plan_override=plan_override, ) cache_file = Path(cache_path) cache = json.loads(cache_file.read_text()) if cache_file.exists() else {"schema": 1, "completed": {}} @@ -151,11 +205,16 @@ def read_items_direct_choice(model: str, items: list[dict], *, samples_per_order "max_tokens": max_tokens, "concurrency": concurrency, "request_timeout": request_timeout, "reasoning": reasoning, "structured_output": structured_output, } + if plan_override is not None: + settings.pop("samples_per_order") + settings["samples_per_item"] = {item["id"]: sum(request["item_id"] == item["id"] for request in plan) for item in items} + settings["schedule"] = "balanced_cyclic_rotations" _append_record(records, { "event": "run_started", "run_id": run_id, "protocol_id": protocol_id, "construct": "direct_choice", "settings": settings, "items": items, - "planned_requests": len(plan), "canonical_requests": len(plan) // 2, - "reversed_requests": len(plan) // 2, + "planned_requests": len(plan), + "canonical_requests": sum(request["order_name"] == "canonical" for request in plan), + "reversed_requests": sum(request["order_name"] == "reversed" for request in plan), }) async def run_all() -> list[dict]: @@ -245,8 +304,8 @@ def read_items_direct_choice(model: str, items: list[dict], *, samples_per_order }) item_results = [] - expected_samples = 2 * samples_per_order for item in items: + expected_samples = sum(request["item_id"] == item["id"] for request in plan) samples = by_item[item["id"]] canonical = sum(sample["order_name"] == "canonical" for sample in samples) reversed_order = sum(sample["order_name"] == "reversed" for sample in samples)