11from __future__ import annotations
22
33from copy import deepcopy
4+ import hashlib
45import math
56
67from app .core .config import settings
1415 RenderStatus ,
1516 Round ,
1617 RoundResponse ,
18+ SeedPolicy ,
1719 Session ,
1820 SessionCreate ,
1921 SessionStatus ,
2224from app .core .logging import logger
2325from app .core .tracing import TraceRecorder
2426from app .feedback .normalization import normalize_feedback
27+ from app .samplers .axis_sweep import AxisSweepSampler
2528from app .samplers .base import clamp_vector
2629from app .samplers .exploit_orthogonal import ExploitOrthogonalSampler
30+ from app .samplers .incumbent_mix import IncumbentMixSampler
2731from app .samplers .random_local import RandomLocalSampler
2832from app .samplers .uncertainty import UncertaintyGuidedSampler
2933from app .storage .repository import JsonRepository
@@ -49,6 +53,8 @@ def __init__(
4953 "random_local" : RandomLocalSampler (),
5054 "exploit_orthogonal" : ExploitOrthogonalSampler (),
5155 "uncertainty_guided" : UncertaintyGuidedSampler (),
56+ "axis_sweep" : AxisSweepSampler (),
57+ "incumbent_mix" : IncumbentMixSampler (),
5258 }
5359 self .updaters = {
5460 "winner_copy" : WinnerCopyUpdater (),
@@ -123,7 +129,6 @@ def generate_round(self, session_id: str) -> RoundResponse:
123129 session = self ._require_session (session_id )
124130 if session .status == SessionStatus .awaiting_feedback :
125131 raise RuntimeError ("Cannot generate a new round while feedback for the current round is still pending" )
126- seed = 1000 + session .current_round
127132 sampler = self .samplers [session .config .sampler ]
128133 round_index = session .current_round + 1
129134 round_obj = Round (
@@ -140,21 +145,22 @@ def generate_round(self, session_id: str) -> RoundResponse:
140145 )
141146 carried_forward = self ._build_carried_forward_candidate (session )
142147 baseline_candidate = self ._build_baseline_prompt_candidate (session )
143- proposed_candidates = sampler .propose (session , seed )
148+ sampler_seed = self ._seed_token (session .id , round_index , "sampler" )
149+ proposed_candidates = sampler .propose (session , sampler_seed )
144150 proposed_candidates = self ._widen_first_round_candidates (session , proposed_candidates )
145151 candidates = self ._compose_round_candidates (
146152 pinned_candidate = carried_forward or baseline_candidate ,
147153 proposed_candidates = proposed_candidates ,
148154 candidate_count = session .config .candidate_count ,
149155 )
156+ self ._assign_candidate_seeds (session , round_index , candidates )
150157 # Render each candidate independently so future versions can tolerate
151158 # partial round failures without changing the orchestration contract.
152159 for candidate in candidates :
153160 candidate .round_id = round_obj .id
154161 if candidate .generation_params .get ("carried_forward" ) and candidate .image_path :
155162 candidate .render_status = RenderStatus .succeeded
156163 continue
157- candidate .seed = seed
158164 candidate = self .generator .render_candidate (session , candidate )
159165 candidate .render_status = RenderStatus .succeeded
160166 round_obj .candidates = candidates
@@ -281,6 +287,16 @@ def _validate_feedback_against_round(self, round_obj: Round, feedback) -> None:
281287 if unknown_ranked :
282288 raise ValueError (f"Feedback ranking references unknown candidates: { ', ' .join (unknown_ranked )} " )
283289
290+ approved = feedback .normalized_payload .get ("approved_candidate_ids" , [])
291+ unknown_approved = [candidate_id for candidate_id in approved if candidate_id not in candidate_ids ]
292+ if unknown_approved :
293+ raise ValueError (f"Feedback approvals reference unknown candidates: { ', ' .join (unknown_approved )} " )
294+
295+ rejected = feedback .normalized_payload .get ("rejected_candidate_ids" , [])
296+ unknown_rejected = [candidate_id for candidate_id in rejected if candidate_id not in candidate_ids ]
297+ if unknown_rejected :
298+ raise ValueError (f"Feedback rejections reference unknown candidates: { ', ' .join (unknown_rejected )} " )
299+
284300 @staticmethod
285301 def _candidate_trace_payload (candidate ) -> dict :
286302 """Return a compact trace payload for one proposed image candidate."""
@@ -294,6 +310,8 @@ def _candidate_trace_payload(candidate) -> dict:
294310 "z" : candidate .z ,
295311 "predicted_score" : candidate .predicted_score ,
296312 "predicted_uncertainty" : candidate .predicted_uncertainty ,
313+ "seed_policy" : candidate .generation_params .get ("seed_policy" ),
314+ "seed_group" : candidate .generation_params .get ("seed_group" ),
297315 }
298316
299317 def _build_carried_forward_candidate (self , session : Session ) -> Candidate | None :
@@ -409,3 +427,40 @@ def _compose_round_candidates(
409427 for index , candidate in enumerate (selected ):
410428 candidate .candidate_index = index
411429 return selected
430+
431+ def _assign_candidate_seeds (self , session : Session , round_index : int , candidates : list [Candidate ]) -> None :
432+ """Assign deterministic candidate seeds according to the configured policy."""
433+
434+ policy = session .config .seed_policy
435+ round_seed = self ._seed_token (session .id , round_index , "round" )
436+ for candidate in candidates :
437+ if candidate .generation_params .get ("carried_forward" ):
438+ candidate .generation_params ["seed_policy" ] = policy .value
439+ candidate .generation_params ["seed_group" ] = "carried_forward"
440+ candidate .generation_params ["seed_preserved" ] = True
441+ continue
442+
443+ if policy == SeedPolicy .fixed_per_round :
444+ candidate .seed = round_seed
445+ seed_group = "round_shared"
446+ elif policy == SeedPolicy .fixed_per_candidate :
447+ candidate .seed = self ._seed_token (session .id , round_index , "candidate" , str (candidate .candidate_index ))
448+ seed_group = f"candidate:{ candidate .candidate_index } "
449+ elif policy == SeedPolicy .fixed_per_candidate_role :
450+ role = candidate .sampler_role or "candidate"
451+ candidate .seed = self ._seed_token (session .id , round_index , "role" , role )
452+ seed_group = f"role:{ role } "
453+ else :
454+ raise ValueError (f"Unsupported seed policy: { policy } " )
455+
456+ candidate .generation_params ["seed_policy" ] = policy .value
457+ candidate .generation_params ["seed_group" ] = seed_group
458+ candidate .generation_params ["round_seed" ] = round_seed
459+
460+ @staticmethod
461+ def _seed_token (* parts : object ) -> int :
462+ """Create one stable positive seed from arbitrary deterministic inputs."""
463+
464+ joined = "|" .join (str (part ) for part in parts )
465+ digest = hashlib .blake2b (joined .encode ("utf-8" ), digest_size = 4 ).digest ()
466+ return int .from_bytes (digest , byteorder = "big" , signed = False )
0 commit comments