Skip to content

Commit 36d8cf1

Browse files
committed
Add method extensions and paper experiment updates
1 parent 4c35da3 commit 36d8cf1

1,613 files changed

Lines changed: 522449 additions & 52 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/core/config_yaml.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414
# Edit any of these values before creating a new session.
1515
# This YAML is reloaded fresh for each setup page visit or reset action.
1616
#
17-
# sampler: random_local | exploit_orthogonal | uncertainty_guided | axis_sweep | incumbent_mix
18-
# updater: winner_average | winner_copy | linear_preference
17+
# sampler: random_local | exploit_orthogonal | uncertainty_guided | axis_sweep | incumbent_mix | diversity_shell | line_search | plateau_escape | annealed_shell | spherical_cover
18+
# updater: winner_average | winner_copy | linear_preference | score_weighted_preference | contrastive_preference | softmax_preference | borda_preference | bradley_terry_preference
1919
# feedback_mode: scalar_rating | pairwise | top_k | winner_only | approve_reject
2020
# seed_policy: fixed-per-round | fixed-per-candidate | fixed-per-candidate-role
2121
# steering_mode: currently low_dimensional
2222
# steering_dimension: low-dimensional steering vector size, for example 3 or 5
2323
# candidate_count: visible candidates per round
2424
# image_size: WIDTHxHEIGHT, for example 512x512
2525
# trust_radius: steering search radius around the current state
26+
# stagnation_patience: rounds of identical selected image before challenger search widens
27+
# stagnation_trust_radius_scale: multiplier applied to trust_radius during stagnation escape
2628
# anchor_strength: strength of the steering offset applied to prompt embeddings
2729
# guidance_scale: classifier-free guidance strength, for example 7.5
2830
# num_inference_steps: diffusion denoising steps, for example 15 or 30

app/core/schema.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,22 @@ class SamplerType(str, Enum):
6565
uncertainty_guided = "uncertainty_guided"
6666
axis_sweep = "axis_sweep"
6767
incumbent_mix = "incumbent_mix"
68+
diversity_shell = "diversity_shell"
69+
line_search = "line_search"
70+
plateau_escape = "plateau_escape"
71+
annealed_shell = "annealed_shell"
72+
spherical_cover = "spherical_cover"
6873

6974

7075
class UpdaterType(str, Enum):
7176
winner_average = "winner_average"
7277
winner_copy = "winner_copy"
7378
linear_preference = "linear_preference"
79+
score_weighted_preference = "score_weighted_preference"
80+
contrastive_preference = "contrastive_preference"
81+
softmax_preference = "softmax_preference"
82+
borda_preference = "borda_preference"
83+
bradley_terry_preference = "bradley_terry_preference"
7484

7585

7686
class SteeringMode(str, Enum):
@@ -89,6 +99,8 @@ class StrategyConfig(BaseModel):
8999
candidate_count: int = Field(default=5, ge=1, le=12)
90100
image_size: str = "512x512"
91101
trust_radius: float = Field(default=0.55, gt=0.0, le=1.0)
102+
stagnation_patience: int = Field(default=0, ge=0, le=10)
103+
stagnation_trust_radius_scale: float = Field(default=1.0, ge=1.0, le=3.0)
92104
anchor_strength: float = Field(default=0.7, ge=0.0, le=2.0)
93105
guidance_scale: float = Field(default=7.5, gt=0.0, le=20.0)
94106
num_inference_steps: int = Field(default=15, ge=1, le=100)

app/engine/orchestrator.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,23 @@
2626
from app.core.tracing import TraceRecorder
2727
from app.feedback.normalization import normalize_feedback
2828
from app.samplers.axis_sweep import AxisSweepSampler
29+
from app.samplers.annealed_shell import AnnealedShellSampler
2930
from app.samplers.base import clamp_vector
31+
from app.samplers.diversity_shell import DiversityShellSampler
3032
from app.samplers.exploit_orthogonal import ExploitOrthogonalSampler
3133
from app.samplers.incumbent_mix import IncumbentMixSampler
34+
from app.samplers.line_search import LineSearchSampler
35+
from app.samplers.plateau_escape import PlateauEscapeSampler
3236
from app.samplers.random_local import RandomLocalSampler
37+
from app.samplers.spherical_cover import SphericalCoverSampler
3338
from app.samplers.uncertainty import UncertaintyGuidedSampler
3439
from app.storage.repository import JsonRepository
40+
from app.updaters.contrastive_pref import ContrastivePreferenceUpdater
41+
from app.updaters.borda_pref import BordaPreferenceUpdater
42+
from app.updaters.bradley_terry_pref import BradleyTerryPreferenceUpdater
3543
from app.updaters.linear_pref import LinearPreferenceUpdater
44+
from app.updaters.softmax_pref import SoftmaxPreferenceUpdater
45+
from app.updaters.score_weighted import ScoreWeightedPreferenceUpdater
3646
from app.updaters.winner_average import WinnerAverageUpdater
3747
from app.updaters.winner_copy import WinnerCopyUpdater
3848
from app.engine.generation import GenerationEngine, build_generation_engine
@@ -56,11 +66,21 @@ def __init__(
5666
"uncertainty_guided": UncertaintyGuidedSampler(),
5767
"axis_sweep": AxisSweepSampler(),
5868
"incumbent_mix": IncumbentMixSampler(),
69+
"diversity_shell": DiversityShellSampler(),
70+
"line_search": LineSearchSampler(),
71+
"plateau_escape": PlateauEscapeSampler(),
72+
"annealed_shell": AnnealedShellSampler(),
73+
"spherical_cover": SphericalCoverSampler(),
5974
}
6075
self.updaters = {
6176
"winner_copy": WinnerCopyUpdater(),
6277
"winner_average": WinnerAverageUpdater(),
6378
"linear_preference": LinearPreferenceUpdater(),
79+
"score_weighted_preference": ScoreWeightedPreferenceUpdater(),
80+
"contrastive_preference": ContrastivePreferenceUpdater(),
81+
"softmax_preference": SoftmaxPreferenceUpdater(),
82+
"borda_preference": BordaPreferenceUpdater(),
83+
"bradley_terry_preference": BradleyTerryPreferenceUpdater(),
6484
}
6585

6686
@staticmethod
@@ -164,8 +184,14 @@ def generate_round(
164184
carried_forward = self._build_carried_forward_candidate(session)
165185
baseline_candidate = self._build_baseline_prompt_candidate(session)
166186
sampler_seed = self._seed_token(session.id, round_index, "sampler")
187+
sampler_session, stagnation_streak, boosted_radius = self._sampler_session_for_round(session)
167188
self._report_progress(progress_callback, 36, f"Sampling {session.config.candidate_count} candidate directions")
168-
proposed_candidates = sampler.propose(session, sampler_seed)
189+
proposed_candidates = sampler.propose(sampler_session, sampler_seed)
190+
if boosted_radius is not None:
191+
for candidate in proposed_candidates:
192+
candidate.generation_params["stagnation_escape_active"] = True
193+
candidate.generation_params["stagnation_streak"] = stagnation_streak
194+
candidate.generation_params["stagnation_boosted_trust_radius"] = round(boosted_radius, 4)
169195
proposed_candidates = self._widen_first_round_candidates(session, proposed_candidates)
170196
candidates = self._compose_round_candidates(
171197
pinned_candidate=carried_forward or baseline_candidate,
@@ -573,6 +599,51 @@ def _assign_candidate_seeds(self, session: Session, round_index: int, candidates
573599
candidate.generation_params["seed_group"] = seed_group
574600
candidate.generation_params["round_seed"] = round_seed
575601

602+
def _sampler_session_for_round(self, session: Session) -> tuple[Session, int, float | None]:
603+
"""Return the sampling view of the session, optionally widened after repeated no-change wins."""
604+
605+
patience = int(session.config.stagnation_patience)
606+
if patience <= 0 or session.current_round <= 0:
607+
return session, 0, None
608+
609+
streak = self._trailing_selected_image_streak(session)
610+
if streak < patience:
611+
return session, streak, None
612+
613+
boosted_radius = min(1.0, session.config.trust_radius * session.config.stagnation_trust_radius_scale)
614+
if boosted_radius <= session.config.trust_radius + 1e-9:
615+
return session, streak, None
616+
617+
boosted_config = session.config.model_copy(update={"trust_radius": boosted_radius})
618+
boosted_session = session.model_copy(deep=True)
619+
boosted_session.config = boosted_config
620+
return boosted_session, streak, boosted_radius
621+
622+
def _trailing_selected_image_streak(self, session: Session) -> int:
623+
"""Count trailing completed rounds that ended with the same selected image artifact."""
624+
625+
rounds = self.repository.list_rounds_for_session(session.id)
626+
streak = 0
627+
image_key: str | None = None
628+
for round_obj in reversed(rounds):
629+
if not round_obj.update_summary:
630+
break
631+
winner_id = round_obj.update_summary.get("winner_candidate_id")
632+
if not winner_id:
633+
break
634+
winner = next((candidate for candidate in round_obj.candidates if candidate.id == winner_id), None)
635+
if winner is None:
636+
break
637+
current_key = winner.image_path or repr([round(value, 6) for value in winner.z])
638+
if image_key is None:
639+
image_key = current_key
640+
streak = 1
641+
continue
642+
if current_key != image_key:
643+
break
644+
streak += 1
645+
return streak
646+
576647
@staticmethod
577648
def _seed_token(*parts: object) -> int:
578649
"""Create one stable positive seed from arbitrary deterministic inputs."""

app/samplers/annealed_shell.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
import math
4+
5+
from app.core.schema import Candidate, Session
6+
from app.samplers.base import clamp_vector, make_rng
7+
8+
9+
class AnnealedShellSampler:
10+
"""Sampler that shrinks from broad shell exploration toward local refinement over rounds."""
11+
12+
name = "annealed_shell"
13+
14+
def propose(self, session: Session, seed: int) -> list[Candidate]:
15+
"""Generate shell probes whose radius anneals with session progress."""
16+
17+
rng = make_rng(seed + 613)
18+
dimensions = max(1, len(session.current_z))
19+
progress = min(max(session.current_round, 0), 8) / 8.0
20+
shell_fraction = 0.95 - (0.42 * progress)
21+
shell_radius = min(max(session.config.trust_radius * shell_fraction, 0.2), session.config.trust_radius)
22+
jitter_scale = 0.04 - (0.018 * progress)
23+
candidates: list[Candidate] = []
24+
25+
for index in range(session.config.candidate_count):
26+
direction = self._spread_direction(index, dimensions)
27+
jitter = [rng.uniform(-jitter_scale, jitter_scale) for _ in range(dimensions)]
28+
z = clamp_vector(
29+
[
30+
current + (axis * shell_radius) + noise
31+
for current, axis, noise in zip(session.current_z, direction, jitter, strict=False)
32+
],
33+
session.config.trust_radius,
34+
)
35+
candidates.append(
36+
Candidate(
37+
round_id="",
38+
candidate_index=index,
39+
z=z,
40+
sampler_role="annealed_probe" if index % 2 == 0 else "annealed_counterprobe",
41+
predicted_score=sum(z) - (0.008 * index),
42+
predicted_uncertainty=max(0.05, 0.2 - (0.08 * progress) + (0.01 * index)),
43+
seed=seed,
44+
generation_params={
45+
"image_size": session.config.image_size,
46+
"annealed_progress": round(progress, 4),
47+
"shell_radius": round(shell_radius, 4),
48+
"jitter_scale": round(jitter_scale, 4),
49+
"spread_direction": [round(value, 4) for value in direction],
50+
},
51+
)
52+
)
53+
return candidates
54+
55+
@staticmethod
56+
def _spread_direction(index: int, dimensions: int) -> list[float]:
57+
vector = [0.0 for _ in range(dimensions)]
58+
primary_axis = index % dimensions
59+
secondary_axis = (index + 1) % dimensions
60+
tertiary_axis = (index + 2) % dimensions
61+
62+
vector[primary_axis] = 1.0 if index % 2 == 0 else -1.0
63+
if dimensions > 1:
64+
vector[secondary_axis] += 0.58 if index % 3 != 1 else -0.58
65+
if dimensions > 2:
66+
vector[tertiary_axis] += 0.26 if index % 4 < 2 else -0.26
67+
if dimensions > 3:
68+
vector[(index + 3) % dimensions] += 0.15 if index % 2 == 0 else -0.15
69+
70+
norm = math.sqrt(sum(value * value for value in vector))
71+
if norm == 0.0:
72+
vector[0] = 1.0
73+
return vector
74+
return [value / norm for value in vector]

app/samplers/diversity_shell.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
import math
4+
5+
from app.core.schema import Candidate, Session
6+
from app.samplers.base import clamp_vector, make_rng
7+
8+
9+
class DiversityShellSampler:
10+
"""Sampler that spreads challengers across a high-radius shell."""
11+
12+
name = "diversity_shell"
13+
14+
def propose(self, session: Session, seed: int) -> list[Candidate]:
15+
"""Generate deliberately separated shell probes around the current state."""
16+
17+
rng = make_rng(seed + 401)
18+
dimensions = max(1, len(session.current_z))
19+
shell_radius = min(max(session.config.trust_radius * 0.92, 0.28), session.config.trust_radius)
20+
candidates: list[Candidate] = []
21+
22+
for index in range(session.config.candidate_count):
23+
direction = self._spread_direction(index, dimensions)
24+
jitter = [rng.uniform(-0.035, 0.035) for _ in range(dimensions)]
25+
z = clamp_vector(
26+
[
27+
current + (axis * shell_radius) + noise
28+
for current, axis, noise in zip(session.current_z, direction, jitter, strict=False)
29+
],
30+
session.config.trust_radius,
31+
)
32+
candidates.append(
33+
Candidate(
34+
round_id="",
35+
candidate_index=index,
36+
z=z,
37+
sampler_role="shell_probe" if index % 2 == 0 else "shell_counterprobe",
38+
predicted_score=sum(z) - (0.01 * index),
39+
predicted_uncertainty=0.18 + (0.03 * index),
40+
seed=seed,
41+
generation_params={
42+
"image_size": session.config.image_size,
43+
"shell_radius": round(shell_radius, 4),
44+
"spread_direction": [round(value, 4) for value in direction],
45+
},
46+
)
47+
)
48+
return candidates
49+
50+
@staticmethod
51+
def _spread_direction(index: int, dimensions: int) -> list[float]:
52+
"""Return a deterministic spread direction for one shell position."""
53+
54+
vector = [0.0 for _ in range(dimensions)]
55+
primary_axis = index % dimensions
56+
secondary_axis = (index + 1) % dimensions
57+
tertiary_axis = (index + 2) % dimensions
58+
59+
primary_sign = 1.0 if index % 2 == 0 else -1.0
60+
secondary_sign = -1.0 if index % 4 in {1, 2} else 1.0
61+
tertiary_sign = -1.0 if index % 3 == 2 else 1.0
62+
63+
vector[primary_axis] = 1.0 * primary_sign
64+
if dimensions > 1:
65+
vector[secondary_axis] += 0.62 * secondary_sign
66+
if dimensions > 2:
67+
vector[tertiary_axis] += 0.28 * tertiary_sign
68+
if dimensions > 3:
69+
extra_axis = (index + 3) % dimensions
70+
vector[extra_axis] += 0.18 if index % 2 == 0 else -0.18
71+
72+
length = math.sqrt(sum(value * value for value in vector))
73+
if length == 0.0:
74+
vector[0] = 1.0
75+
return vector
76+
return [value / length for value in vector]

0 commit comments

Comments
 (0)