Update app.py
Browse files
app.py
CHANGED
|
@@ -283,52 +283,145 @@ def _ensure_min_events(sequence, event_check_func, min_proportion, generator_fun
|
|
| 283 |
return current_sequence, False
|
| 284 |
else: return current_sequence, True
|
| 285 |
|
| 286 |
-
# Attention Sequence Generator (
|
| 287 |
def generate_attention_sequence(params):
|
| 288 |
-
n = params['trials']
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
total_p = p_ax + p_ao + p_x + p_s + p_o
|
| 293 |
if abs(total_p - 1.0) > 1e-6:
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
def _generate_single_pass():
|
| 297 |
-
nonlocal final_sequence
|
|
|
|
|
|
|
| 298 |
for i in range(n):
|
| 299 |
-
chosen_stim = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
if r < (cdf := cdf + p_ax):
|
| 301 |
-
if
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
elif r < (cdf := cdf + p_ao):
|
|
|
|
| 308 |
distractor_after_a = random.choice(similar_distractors + other_distractors)
|
| 309 |
if last_stim != cue:
|
| 310 |
-
if cue != last_stim:
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
generated_sequence = _generate_single_pass()
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
final_ax_count = sum(1 for i, item in enumerate(final_sequence) if check_ax_pairs(final_sequence, i, item))
|
| 330 |
min_ax_needed = int(n * ATTN_MIN_PAIR_PROPORTION) if p_ax > 0 else 0
|
| 331 |
-
if final_ax_count < min_ax_needed:
|
|
|
|
|
|
|
|
|
|
| 332 |
ex = {'target': target, 'cue': cue, 'similar_distractor_key': params['similar_distractor_key']}
|
| 333 |
return final_sequence, ex
|
| 334 |
|
|
|
|
| 283 |
return current_sequence, False
|
| 284 |
else: return current_sequence, True
|
| 285 |
|
| 286 |
+
# --- Modified Attention Sequence Generator (v8.2 - FIX SyntaxError) ---
|
| 287 |
def generate_attention_sequence(params):
|
| 288 |
+
n = params['trials']
|
| 289 |
+
cue = params['cue']
|
| 290 |
+
target = params['target']
|
| 291 |
+
similar_distractors = params['similar_distractors']
|
| 292 |
+
other_distractors = ATTN_OTHER_DISTRACTORS
|
| 293 |
+
|
| 294 |
+
# Get probabilities from params (calculated in get_difficulty_params)
|
| 295 |
+
p_ax = params['prob_A_then_X']
|
| 296 |
+
p_ao = params['prob_A_then_Other'] # Note: A->Other can include Similar or Other distractors
|
| 297 |
+
p_x = params['prob_X_alone']
|
| 298 |
+
p_s = params['prob_Similar_alone']
|
| 299 |
+
p_o = params['prob_Other_distractor']
|
| 300 |
+
|
| 301 |
+
# Normalize just in case they don't perfectly sum to 1 due to floating point
|
| 302 |
total_p = p_ax + p_ao + p_x + p_s + p_o
|
| 303 |
if abs(total_p - 1.0) > 1e-6:
|
| 304 |
+
# print(f"WARN: Attention probabilities sum to {total_p:.4f}, normalizing.")
|
| 305 |
+
if total_p > 1e-9:
|
| 306 |
+
p_ax /= total_p; p_ao /= total_p; p_x /= total_p; p_s /= total_p; p_o /= total_p
|
| 307 |
+
|
| 308 |
+
final_sequence = [] # Define upfront for access within _generate_single_pass
|
| 309 |
+
|
| 310 |
def _generate_single_pass():
|
| 311 |
+
nonlocal final_sequence # Allow modification
|
| 312 |
+
sq = []
|
| 313 |
+
last_stim = None
|
| 314 |
for i in range(n):
|
| 315 |
+
chosen_stim = None
|
| 316 |
+
# Core logic based on probabilities of specific conditions happening *at this trial*
|
| 317 |
+
r = random.random()
|
| 318 |
+
cdf = 0.0
|
| 319 |
+
|
| 320 |
if r < (cdf := cdf + p_ax):
|
| 321 |
+
# Force an A->X pair if possible, otherwise just X
|
| 322 |
+
if last_stim != cue: # Need to insert A first
|
| 323 |
+
# Check if inserting A makes sense (not A->A)
|
| 324 |
+
if cue != last_stim:
|
| 325 |
+
sq.append(cue)
|
| 326 |
+
last_stim = cue
|
| 327 |
+
else: # Cannot insert A (would be A->A), so choose X, S, or O based on their probabilities
|
| 328 |
+
# --- START FIX for SyntaxError ---
|
| 329 |
+
chosen_stim = None
|
| 330 |
+
prob_pool_xso = p_x + p_s + p_o # Probabilities for X_alone, S_alone, O_distractor
|
| 331 |
+
if prob_pool_xso < 1e-9:
|
| 332 |
+
chosen_stim = target # Fallback if all probs are near zero
|
| 333 |
+
else:
|
| 334 |
+
rand_choice = random.random() * prob_pool_xso # Random value scaled to the pool sum
|
| 335 |
+
if rand_choice < p_x:
|
| 336 |
+
chosen_stim = target
|
| 337 |
+
elif rand_choice < p_x + p_s:
|
| 338 |
+
chosen_stim = random.choice(similar_distractors)
|
| 339 |
+
else: # Implicitly rand_choice < p_x + p_s + p_o
|
| 340 |
+
chosen_stim = random.choice(other_distractors)
|
| 341 |
+
# --- END FIX for SyntaxError ---
|
| 342 |
+
|
| 343 |
+
if chosen_stim is None: # We added 'A' or it was already 'A'
|
| 344 |
+
chosen_stim = target
|
| 345 |
elif r < (cdf := cdf + p_ao):
|
| 346 |
+
# Force an A -> Other pair (can be Similar or Other)
|
| 347 |
distractor_after_a = random.choice(similar_distractors + other_distractors)
|
| 348 |
if last_stim != cue:
|
| 349 |
+
if cue != last_stim:
|
| 350 |
+
sq.append(cue)
|
| 351 |
+
last_stim = cue
|
| 352 |
+
else: # Cannot insert A, choose a different non-A stimulus
|
| 353 |
+
pool = [target] + similar_distractors + other_distractors
|
| 354 |
+
chosen_stim = random.choice([s for s in pool if s != last_stim])
|
| 355 |
+
if not chosen_stim: chosen_stim = random.choice(pool) # Fallback
|
| 356 |
+
|
| 357 |
+
if chosen_stim is None:
|
| 358 |
+
chosen_stim = distractor_after_a
|
| 359 |
+
# Avoid A -> A
|
| 360 |
+
if chosen_stim == cue:
|
| 361 |
+
pool = [target] + similar_distractors + other_distractors
|
| 362 |
+
chosen_stim = random.choice([s for s in pool if s != cue])
|
| 363 |
+
if not chosen_stim: chosen_stim = random.choice(pool) # Fallback
|
| 364 |
+
elif r < (cdf := cdf + p_x):
|
| 365 |
+
chosen_stim = target
|
| 366 |
+
elif r < (cdf := cdf + p_s):
|
| 367 |
+
chosen_stim = random.choice(similar_distractors)
|
| 368 |
+
else: # (implicitly r < cdf + p_o)
|
| 369 |
+
chosen_stim = random.choice(other_distractors)
|
| 370 |
+
|
| 371 |
+
# Final checks to avoid immediate repeats of non-cue/non-target
|
| 372 |
+
if chosen_stim == last_stim and chosen_stim not in [cue, target]:
|
| 373 |
+
pool = similar_distractors + other_distractors
|
| 374 |
+
available = [d for d in pool if d != last_stim]
|
| 375 |
+
chosen_stim = random.choice(available) if available else chosen_stim # Keep if no alternative
|
| 376 |
+
|
| 377 |
+
# If we inserted 'A', we might need to adjust sequence length
|
| 378 |
+
if len(sq) < n:
|
| 379 |
+
sq.append(chosen_stim)
|
| 380 |
+
last_stim = chosen_stim
|
| 381 |
+
|
| 382 |
+
# Trim if we added extra 'A's making it too long
|
| 383 |
+
final_sequence = sq[:n]
|
| 384 |
+
return final_sequence # Return for the helper function
|
| 385 |
+
|
| 386 |
+
# Define check functions for _ensure_min_events
|
| 387 |
+
def check_ax_pairs(seq, index, item):
|
| 388 |
+
# Is item at index 'target' and preceded by 'cue'?
|
| 389 |
+
return index > 0 and item == target and seq[index - 1] == cue
|
| 390 |
+
|
| 391 |
+
def check_similar_alone(seq, index, item):
|
| 392 |
+
# Is item a similar distractor and NOT preceded by 'cue'?
|
| 393 |
+
return item in similar_distractors and (index == 0 or seq[index - 1] != cue)
|
| 394 |
+
|
| 395 |
+
# Generate initial sequence
|
| 396 |
generated_sequence = _generate_single_pass()
|
| 397 |
+
|
| 398 |
+
# Ensure minimum A->X pairs
|
| 399 |
+
sequence_ax_ok, success_ax = _ensure_min_events(
|
| 400 |
+
generated_sequence,
|
| 401 |
+
check_ax_pairs,
|
| 402 |
+
ATTN_MIN_PAIR_PROPORTION if p_ax > 0 else 0,
|
| 403 |
+
_generate_single_pass # Regeneration function
|
| 404 |
+
)
|
| 405 |
+
if not success_ax:
|
| 406 |
+
print(f"WARN: Failed to ensure minimum A->X pairs ({ATTN_MIN_PAIR_PROPORTION*100:.1f}%).")
|
| 407 |
+
|
| 408 |
+
# Ensure minimum Similar Alone trials *using the potentially regenerated sequence*
|
| 409 |
+
final_sequence, success_sa = _ensure_min_events(
|
| 410 |
+
sequence_ax_ok, # Start with the sequence that passed (or failed) the A->X check
|
| 411 |
+
check_similar_alone,
|
| 412 |
+
ATTN_MIN_SIMILAR_ALONE_PROPORTION if p_s > 0 else 0,
|
| 413 |
+
_generate_single_pass # Regeneration function (might undo A->X guarantee, but necessary)
|
| 414 |
+
)
|
| 415 |
+
if not success_sa:
|
| 416 |
+
print(f"WARN: Failed to ensure minimum Similar Alone pairs ({ATTN_MIN_SIMILAR_ALONE_PROPORTION*100:.1f}%).")
|
| 417 |
+
|
| 418 |
+
# Final check (optional): Re-verify A->X count after ensuring Similar Alone, as regeneration might affect it
|
| 419 |
final_ax_count = sum(1 for i, item in enumerate(final_sequence) if check_ax_pairs(final_sequence, i, item))
|
| 420 |
min_ax_needed = int(n * ATTN_MIN_PAIR_PROPORTION) if p_ax > 0 else 0
|
| 421 |
+
if final_ax_count < min_ax_needed:
|
| 422 |
+
print(f"WARN: Final Attn sequence low on A->X pairs ({final_ax_count}/{min_ax_needed}) after S-Alone check.")
|
| 423 |
+
|
| 424 |
+
# Expected response info remains simple, logic is in process_response
|
| 425 |
ex = {'target': target, 'cue': cue, 'similar_distractor_key': params['similar_distractor_key']}
|
| 426 |
return final_sequence, ex
|
| 427 |
|