Skip to content

Commit 39ebde1

Browse files
committed
Move condition weight resolution into TaskSettings
1 parent 8c8062f commit 39ebde1

17 files changed

Lines changed: 137 additions & 91 deletions

File tree

ChangLog.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# psyflow change log
22

3+
## 0.1.20 (2026-03-02)
4+
5+
### Summary
6+
- Moved condition-weight resolution into `TaskSettings`:
7+
- added `TaskSettings.resolve_condition_weights()` in `psyflow/TaskSettings.py`.
8+
- Removed `psyflow.utils.trials.resolve_condition_weights` and all related exports.
9+
- Updated runtime/template standards to use `settings.resolve_condition_weights()`:
10+
- cookiecutter `main.py` and config comments now point to the `TaskSettings` method.
11+
- Updated contracts and task-build skill assets/checklists to standardize this policy.
12+
- Updated condition-weight unit tests to exercise the `TaskSettings` method directly.
13+
14+
### Validation
15+
- `python -m unittest tests.test_condition_weights` passed.
16+
- `python -m unittest tests.test_validate` passed.
17+
- `python -m psyflow.validate "psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}"` passed with `FAIL=0`.
18+
319
## 0.1.19 (2026-03-02)
420

521
### Summary

psyflow/TaskSettings.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass, field
22
from typing import List, Optional, Any, Dict
33
from math import ceil
4+
import math
45
import random
56
import hashlib
67
from datetime import datetime
@@ -40,6 +41,7 @@ class TaskSettings:
4041

4142
# --- Trial logic ---
4243
conditions: List[str] = field(default_factory=list)
44+
condition_weights: Any = None
4345
block_seed: Optional[List[int]] = None
4446

4547
# --- Seeding strategy ---
@@ -81,6 +83,65 @@ def set_block_seed(self, seed_base: Optional[int]):
8183
rng = random.Random(seed_base)
8284
self.block_seed = [rng.randint(0, 99999) for _ in range(self.total_blocks)]
8385

86+
def resolve_condition_weights(self) -> list[float] | None:
87+
"""Resolve and validate optional condition weights.
88+
89+
Returns
90+
-------
91+
list[float] | None
92+
A weight vector aligned to ``self.conditions`` when
93+
``self.condition_weights`` is configured; otherwise ``None`` to
94+
indicate even/default generation.
95+
"""
96+
raw = getattr(self, "condition_weights", None)
97+
if raw is None:
98+
return None
99+
100+
if not isinstance(self.conditions, list):
101+
raise TypeError("conditions must be a list when condition_weights is provided.")
102+
103+
labels = [str(c) for c in self.conditions]
104+
if not labels:
105+
raise ValueError("conditions must be non-empty when condition_weights is provided.")
106+
107+
values: list[Any]
108+
if isinstance(raw, dict):
109+
keyed = {str(k): v for k, v in raw.items()}
110+
missing = [label for label in labels if label not in keyed]
111+
extra = [key for key in keyed if key not in labels]
112+
if missing:
113+
raise ValueError(f"condition_weights missing entries for condition(s): {missing}")
114+
if extra:
115+
raise ValueError(f"condition_weights contains unknown condition key(s): {extra}")
116+
values = [keyed[label] for label in labels]
117+
elif isinstance(raw, (list, tuple)):
118+
if len(raw) != len(labels):
119+
raise ValueError(
120+
"condition_weights length mismatch: expected "
121+
f"{len(labels)} for conditions {labels}, got {len(raw)}"
122+
)
123+
values = list(raw)
124+
else:
125+
raise TypeError("condition_weights must be null, list/tuple, or mapping keyed by condition label.")
126+
127+
weights: list[float] = []
128+
for i, value in enumerate(values):
129+
try:
130+
w = float(value)
131+
except Exception as exc:
132+
raise TypeError(
133+
f"condition_weights[{i}] could not be parsed as number: {value!r}"
134+
) from exc
135+
if not math.isfinite(w):
136+
raise ValueError(f"condition_weights[{i}] must be finite, got {w!r}")
137+
if w <= 0:
138+
raise ValueError(f"condition_weights[{i}] must be > 0, got {w!r}")
139+
weights.append(w)
140+
141+
if sum(weights) <= 0:
142+
raise ValueError(f"condition_weights sum must be > 0, got {weights}")
143+
return weights
144+
84145
def add_subinfo(self, subinfo: Dict[str, Any]):
85146
"""
86147
Add subject-specific information and set seed/file names accordingly.

psyflow/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"next_trial_id": ("psyflow.utils.trials", "next_trial_id"),
5454
"reset_trial_counter": ("psyflow.utils.trials", "reset_trial_counter"),
5555
"resolve_deadline": ("psyflow.utils.trials", "resolve_deadline"),
56-
"resolve_condition_weights": ("psyflow.utils.trials", "resolve_condition_weights"),
5756
"resolve_trial_id": ("psyflow.utils.trials", "resolve_trial_id"),
5857
}
5958

@@ -92,7 +91,6 @@
9291
next_trial_id as next_trial_id,
9392
reset_trial_counter as reset_trial_counter,
9493
resolve_deadline as resolve_deadline,
95-
resolve_condition_weights as resolve_condition_weights,
9694
resolve_trial_id as resolve_trial_id,
9795
)
9896

psyflow/contracts/v0.1.0/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ These contracts define practical standards for building auditable psyflow/TAPS t
88
- Task metadata (`taskbeacon.yaml`)
99
- Config structure and explicit value/type constraints
1010
- mandatory/optional/recommended keys and value specs
11-
- optional `task.condition_weights` validation (mapping/list aligned to `task.conditions`)
11+
- optional `task.condition_weights` validation (mapping/list aligned to `task.conditions`, resolved via `TaskSettings.resolve_condition_weights()`)
1212
- stimulus type standards and asset-backed path conventions
1313
- smoke-profile rules for `config_qa.yaml` and sim configs (shorter than base but condition-covering)
1414
- Runtime entrypoint pattern (`main.py`)

psyflow/contracts/v0.1.0/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ notes:
135135
- controller is optional.
136136
- if controller is configured, keep implementation under src/utils.py.
137137
- if task.condition_weights is defined, weighted condition generation must be explicit and aligned to task.conditions.
138+
- task runtime should resolve/validate weights through TaskSettings.resolve_condition_weights().
138139
- if task.condition_weights is omitted (or null), default/even condition generation is assumed unless a custom generator is documented.
139140
- stimuli entries must include a valid type.
140141
- asset-backed stimuli (image/movie/sound) should load from assets/ paths.

psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ task:
4040
total_trials: 24
4141
trial_per_block: 12
4242
conditions: [baseline, variant]
43-
# Optional weighted condition generation; leave null for even generation.
43+
# Optional weighted condition generation consumed by TaskSettings.resolve_condition_weights().
44+
# Leave null for even generation.
4445
# Use either a mapping by condition label or a list aligned to task.conditions.
4546
condition_weights: null
4647
key_list: [space]

psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config_qa.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ task:
4141
total_trials: 8
4242
trial_per_block: 8
4343
conditions: [baseline, variant]
44-
# Optional weighted condition generation; leave null for even generation.
44+
# Optional weighted condition generation consumed by TaskSettings.resolve_condition_weights().
45+
# Leave null for even generation.
4546
# Use either a mapping by condition label or a list aligned to task.conditions.
4647
condition_weights: null
4748
key_list: [space]

psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config_sampler_sim.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ task:
4141
total_trials: 8
4242
trial_per_block: 8
4343
conditions: [baseline, variant]
44-
# Optional weighted condition generation; leave null for even generation.
44+
# Optional weighted condition generation consumed by TaskSettings.resolve_condition_weights().
45+
# Leave null for even generation.
4546
# Use either a mapping by condition label or a list aligned to task.conditions.
4647
condition_weights: null
4748
key_list: [space]

psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/config/config_scripted_sim.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ task:
4141
total_trials: 8
4242
trial_per_block: 8
4343
conditions: [baseline, variant]
44-
# Optional weighted condition generation; leave null for even generation.
44+
# Optional weighted condition generation consumed by TaskSettings.resolve_condition_weights().
45+
# Leave null for even generation.
4546
# Use either a mapping by condition label or a list aligned to task.conditions.
4647
condition_weights: null
4748
key_list: [space]

psyflow/templates/cookiecutter-psyflow/{{cookiecutter.project_name}}/main.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
initialize_triggers,
1919
load_config,
2020
parse_task_run_options,
21-
resolve_condition_weights,
2221
runtime_context,
2322
)
2423

@@ -92,10 +91,7 @@ def run(options: TaskRunOptions):
9291
instruction.wait_and_continue()
9392

9493
all_data = []
95-
condition_weights = resolve_condition_weights(
96-
getattr(settings, "condition_weights", None),
97-
list(getattr(settings, "conditions", [])),
98-
)
94+
condition_weights = settings.resolve_condition_weights()
9995
for block_i in range(settings.total_blocks):
10096
if options.mode not in ("qa", "sim"):
10197
count_down(win, 3, color="black")

0 commit comments

Comments
 (0)