VVIQ-16 Imagery Study¶
Reproduces the VVIQ-16 response distribution figure using PsychScanner:
- Task: Vividness of Visual Imagery Questionnaire, 16 items across 4 scenes
- Parser:
DefaultLiteralVivid15(1–5 vividness scale) - Two memory conditions:
SingleTurn(No Memory) vsConvo(Conversation Memory) - 5 simulations per condition (
nsim=5)
The output figure shows how response distributions differ across memory conditions.
1. Setup¶
In [1]:
Copied!
import os
from dotenv import load_dotenv
# https://www.youtube.com/watch?v=PYuTzLswn_Y
load_dotenv()
API_KEY = os.getenv("GROQ_KEY")
import os
from dotenv import load_dotenv
# https://www.youtube.com/watch?v=PYuTzLswn_Y
load_dotenv()
API_KEY = os.getenv("GROQ_KEY")
In [2]:
Copied!
from pathlib import Path
import matplotlib.pyplot as plt
import polars as pl
from psychscanner import ExpCard, ExpCardInit, ScannerModel, to_csv, concat_csv
# ── backend ──────────────────────────────────────────────────────────────────
# Change these two lines if you don't use Ollama.
# Any model that supports structured output (json_schema) will work.
MODEL_NAME = 'smollm2:360m-instruct-fp16'#"openai/gpt-oss-120b"#
MODEL_FAMILY = 'ollama'#"groq"#
TASK_FILE = Path.cwd() / "tasks" / "vviq16.json"
PROJECT_DIR = Path.cwd() / "_vviq16_study"
PROJECT_DIR.mkdir(exist_ok=True)
print(f"Outputs → {PROJECT_DIR}")
from pathlib import Path
import matplotlib.pyplot as plt
import polars as pl
from psychscanner import ExpCard, ExpCardInit, ScannerModel, to_csv, concat_csv
# ── backend ──────────────────────────────────────────────────────────────────
# Change these two lines if you don't use Ollama.
# Any model that supports structured output (json_schema) will work.
MODEL_NAME = 'smollm2:360m-instruct-fp16'#"openai/gpt-oss-120b"#
MODEL_FAMILY = 'ollama'#"groq"#
TASK_FILE = Path.cwd() / "tasks" / "vviq16.json"
PROJECT_DIR = Path.cwd() / "_vviq16_study"
PROJECT_DIR.mkdir(exist_ok=True)
print(f"Outputs → {PROJECT_DIR}")
/opt/anaconda3/envs/psyscan/lib/python3.11/site-packages/langgraph/checkpoint/base/__init__.py:17: LangChainPendingDeprecationWarning: The default value of `allowed_objects` will change in a future version. Pass an explicit value (e.g., allowed_objects='messages' or allowed_objects='core') to suppress this warning. from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
Outputs → /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study
2. Task overview¶
The VVIQ-16 presents four imagery scenes, each with four aspects to rate:
| Scene | Items | Topic |
|---|---|---|
| S1 | 1–4 | A familiar person |
| S2 | 5–8 | A rising sun |
| S3 | 9–12 | A shop front |
| S4 | 13–16 | A country landscape |
Ratings use the DefaultLiteralVivid15 parser (1 = no image at all, 5 = perfectly vivid).
3. Run experiments¶
In [3]:
Copied!
# ── Condition 1: No Memory (SingleTurn) ──────────────────────────────────────
card_st = ExpCardInit()
card_st.proj_dir = PROJECT_DIR
card_st.projectname = "vviq16study"
card_st.model = MODEL_NAME
card_st.family = MODEL_FAMILY
#card_st.parameters = {'api_key': API_KEY}
card_st.task_file = TASK_FILE
card_st.parser = "1" # read DefaultLiteralVivid15 from task JSON
card_st.memory = "SingleTurn" # no memory between trials
card_st.cogtype = "no"
card_st.nsim = 5
card_st.tunnel_status = "0"
scanner_st = ScannerModel(expcard=ExpCard(card_st))
results_st = scanner_st.run()
print(f"SingleTurn: {len(results_st)} simulations × {len(results_st[0])} trials")
# ── Condition 1: No Memory (SingleTurn) ──────────────────────────────────────
card_st = ExpCardInit()
card_st.proj_dir = PROJECT_DIR
card_st.projectname = "vviq16study"
card_st.model = MODEL_NAME
card_st.family = MODEL_FAMILY
#card_st.parameters = {'api_key': API_KEY}
card_st.task_file = TASK_FILE
card_st.parser = "1" # read DefaultLiteralVivid15 from task JSON
card_st.memory = "SingleTurn" # no memory between trials
card_st.cogtype = "no"
card_st.nsim = 5
card_st.tunnel_status = "0"
scanner_st = ScannerModel(expcard=ExpCard(card_st))
results_st = scanner_st.run()
print(f"SingleTurn: {len(results_st)} simulations × {len(results_st[0])} trials")
----<PROJECT AND DATA ROOT DIRECTORY>---- Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16study/vviq16/ollama_smollm2:360m-instruct-fp16_SingleTurn ----<>---- --<chat model>-- model='smollm2:360m-instruct-fp16'
2026-05-07 09:59:44.443 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 5 RESUME IDX: None ----<>---- task running
16it [00:27, 1.70s/it] 2026-05-07 10:00:11.753 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0 ----<>---- task running
16it [00:20, 1.31s/it] 2026-05-07 10:00:32.748 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 1 ----<>---- task running
16it [00:21, 1.36s/it] 2026-05-07 10:00:54.596 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 2 ----<>---- task running
16it [00:23, 1.44s/it] 2026-05-07 10:01:17.728 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 3 ----<>---- task running
16it [00:22, 1.39s/it] 2026-05-07 10:01:40.040 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 4
2026-05-07 10:01:40.043 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
SingleTurn: 5 simulations × 16 trials
In [4]:
Copied!
# ── Condition 2: Conversation Memory (Convo) ─────────────────────────────────
card_cv = ExpCardInit()
card_cv.proj_dir = PROJECT_DIR
card_cv.projectname = "vviq16study"
card_cv.model = MODEL_NAME
card_cv.family = MODEL_FAMILY
#card_cv.parameters = {'api_key': API_KEY}
card_cv.task_file = TASK_FILE
card_cv.parser = "1"
card_cv.memory = "Convo" # full conversation context retained
card_cv.cogtype = "no"
card_cv.nsim = 5
card_cv.tunnel_status = "0"
scanner_cv = ScannerModel(expcard=ExpCard(card_cv))
results_cv = scanner_cv.run()
print(f"Convo: {len(results_cv)} simulations × {len(results_cv[0])} trials")
# ── Condition 2: Conversation Memory (Convo) ─────────────────────────────────
card_cv = ExpCardInit()
card_cv.proj_dir = PROJECT_DIR
card_cv.projectname = "vviq16study"
card_cv.model = MODEL_NAME
card_cv.family = MODEL_FAMILY
#card_cv.parameters = {'api_key': API_KEY}
card_cv.task_file = TASK_FILE
card_cv.parser = "1"
card_cv.memory = "Convo" # full conversation context retained
card_cv.cogtype = "no"
card_cv.nsim = 5
card_cv.tunnel_status = "0"
scanner_cv = ScannerModel(expcard=ExpCard(card_cv))
results_cv = scanner_cv.run()
print(f"Convo: {len(results_cv)} simulations × {len(results_cv[0])} trials")
----<PROJECT AND DATA ROOT DIRECTORY>---- Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16study/vviq16/ollama_smollm2:360m-instruct-fp16_Convo ----<>---- --<chat model>-- model='smollm2:360m-instruct-fp16'
2026-05-07 10:01:40.354 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 5 RESUME IDX: None ----<>---- task running
16it [00:37, 2.34s/it] 2026-05-07 10:02:17.811 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0 ----<>---- task running
16it [00:34, 2.19s/it] 2026-05-07 10:02:52.830 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 1 ----<>---- task running
16it [00:36, 2.28s/it] 2026-05-07 10:03:29.289 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 2 ----<>---- task running
16it [00:45, 2.84s/it] 2026-05-07 10:04:14.679 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 3 ----<>---- task running
16it [00:39, 2.45s/it] 2026-05-07 10:04:53.913 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 4
2026-05-07 10:04:53.918 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
Convo: 5 simulations × 16 trials
4. Export to CSV¶
In [5]:
Copied!
# Individual CSVs per condition
df_st = to_csv(scanner_st, path=PROJECT_DIR / "vviq16_singleturn.csv")
df_cv = to_csv(scanner_cv, path=PROJECT_DIR / "vviq16_convo.csv")
# Combined CSV for downstream analysis
df_all = to_csv([scanner_st, scanner_cv],
path=PROJECT_DIR / "vviq16_combined.csv",
combined=True)
print(f"\nCombined: {df_all.shape[0]} rows × {df_all.shape[1]} columns")
#df_all.select(["model", "memory", "trcode", "trial_idx", "resp_Vividness"]).head(6)
# Individual CSVs per condition
df_st = to_csv(scanner_st, path=PROJECT_DIR / "vviq16_singleturn.csv")
df_cv = to_csv(scanner_cv, path=PROJECT_DIR / "vviq16_convo.csv")
# Combined CSV for downstream analysis
df_all = to_csv([scanner_st, scanner_cv],
path=PROJECT_DIR / "vviq16_combined.csv",
combined=True)
print(f"\nCombined: {df_all.shape[0]} rows × {df_all.shape[1]} columns")
#df_all.select(["model", "memory", "trcode", "trial_idx", "resp_Vividness"]).head(6)
Saved 80 rows → /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16_singleturn.csv Saved 80 rows → /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16_convo.csv Saved 160 rows from 2 source(s) → /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16_combined.csv Combined: 160 rows × 20 columns
5. Reproduce the figure¶
Each dot is one simulation's vividness rating for a trial. With 5 simulations × 16 trials = 80 data points per panel.
In [6]:
Copied!
import re, numpy as np
import pandas as pd
_VIV_RE = re.compile(r'Vividness[^0-9]*(\d+)')
def load_vividness(csv_path):
df = pd.read_csv(csv_path)
def _parse(s):
m = _VIV_RE.search(str(s))
return int(m.group(1)) if m else None
df['Vividness'] = df['pred_resp_raw'].apply(_parse)
x = (df['trial_idx'] + 1).values.astype(float)
y = df['Vividness'].values.astype(float)
return x, y
rng = np.random.default_rng(42)
x_st, y_st = load_vividness(PROJECT_DIR / 'vviq16_singleturn.csv')
x_cv, y_cv = load_vividness(PROJECT_DIR / 'vviq16_convo.csv')
fig, (ax_st, ax_cv) = plt.subplots(1, 2, figsize=(12, 5), sharey=True)
def _jitter(x, scale=0.10):
return x + rng.uniform(-scale, scale, size=len(x))
ax_st.scatter(_jitter(x_st), y_st, color='steelblue', s=55, alpha=0.80,
linewidths=0.3, edgecolors='white', zorder=3)
ax_st.set_title('No Memory', fontsize=14, fontweight='bold')
ax_st.set_xlabel('Trial Number', fontsize=12)
ax_st.set_ylabel('Rating response (1 to 5)', fontsize=12)
ax_st.set_xlim(0.5, 16.5)
ax_st.set_xticks(range(1, 17))
ax_st.set_xticklabels(range(1, 17), fontsize=8)
ax_st.set_ylim(0.5, 5.5)
ax_st.set_yticks([1, 2, 3, 4, 5])
ax_st.grid(axis='y', linestyle='--', alpha=0.4)
ax_st.spines[['top', 'right']].set_visible(False)
ax_cv.scatter(_jitter(x_cv), y_cv, color='gray', s=55, alpha=0.80,
linewidths=0.3, edgecolors='white', zorder=3)
ax_cv.set_title('Conversation Memory', fontsize=14, fontweight='bold')
ax_cv.set_xlabel('Trial Number', fontsize=12)
ax_cv.set_xlim(0.5, 16.5)
ax_cv.set_xticks(range(1, 17))
ax_cv.set_xticklabels(range(1, 17), fontsize=8)
ax_cv.set_ylim(0.5, 5.5)
ax_cv.set_yticks([1, 2, 3, 4, 5])
ax_cv.grid(axis='y', linestyle='--', alpha=0.4)
ax_cv.spines[['top', 'right']].set_visible(False)
fig.suptitle(
f'VVIQ-16 (Imagination Questionnaire) response distribution\n'
f'Model: {MODEL_NAME} · {card_st.nsim} simulations x 16 trials',
fontsize=13, y=1.03,
)
plt.tight_layout()
fig.savefig(PROJECT_DIR / 'vviq16_figure.png', dpi=150, bbox_inches='tight')
plt.show()
print(f'Figure saved -> {PROJECT_DIR / "vviq16_figure.png"}')
n_missing = int(np.isnan(y_st).sum() + np.isnan(y_cv).sum())
if n_missing:
print(f'Warning: {n_missing} ratings could not be parsed')
else:
print(f'SingleTurn: M={np.nanmean(y_st):.2f}, SD={np.nanstd(y_st):.2f}')
print(f'Convo: M={np.nanmean(y_cv):.2f}, SD={np.nanstd(y_cv):.2f}')
import re, numpy as np
import pandas as pd
_VIV_RE = re.compile(r'Vividness[^0-9]*(\d+)')
def load_vividness(csv_path):
df = pd.read_csv(csv_path)
def _parse(s):
m = _VIV_RE.search(str(s))
return int(m.group(1)) if m else None
df['Vividness'] = df['pred_resp_raw'].apply(_parse)
x = (df['trial_idx'] + 1).values.astype(float)
y = df['Vividness'].values.astype(float)
return x, y
rng = np.random.default_rng(42)
x_st, y_st = load_vividness(PROJECT_DIR / 'vviq16_singleturn.csv')
x_cv, y_cv = load_vividness(PROJECT_DIR / 'vviq16_convo.csv')
fig, (ax_st, ax_cv) = plt.subplots(1, 2, figsize=(12, 5), sharey=True)
def _jitter(x, scale=0.10):
return x + rng.uniform(-scale, scale, size=len(x))
ax_st.scatter(_jitter(x_st), y_st, color='steelblue', s=55, alpha=0.80,
linewidths=0.3, edgecolors='white', zorder=3)
ax_st.set_title('No Memory', fontsize=14, fontweight='bold')
ax_st.set_xlabel('Trial Number', fontsize=12)
ax_st.set_ylabel('Rating response (1 to 5)', fontsize=12)
ax_st.set_xlim(0.5, 16.5)
ax_st.set_xticks(range(1, 17))
ax_st.set_xticklabels(range(1, 17), fontsize=8)
ax_st.set_ylim(0.5, 5.5)
ax_st.set_yticks([1, 2, 3, 4, 5])
ax_st.grid(axis='y', linestyle='--', alpha=0.4)
ax_st.spines[['top', 'right']].set_visible(False)
ax_cv.scatter(_jitter(x_cv), y_cv, color='gray', s=55, alpha=0.80,
linewidths=0.3, edgecolors='white', zorder=3)
ax_cv.set_title('Conversation Memory', fontsize=14, fontweight='bold')
ax_cv.set_xlabel('Trial Number', fontsize=12)
ax_cv.set_xlim(0.5, 16.5)
ax_cv.set_xticks(range(1, 17))
ax_cv.set_xticklabels(range(1, 17), fontsize=8)
ax_cv.set_ylim(0.5, 5.5)
ax_cv.set_yticks([1, 2, 3, 4, 5])
ax_cv.grid(axis='y', linestyle='--', alpha=0.4)
ax_cv.spines[['top', 'right']].set_visible(False)
fig.suptitle(
f'VVIQ-16 (Imagination Questionnaire) response distribution\n'
f'Model: {MODEL_NAME} · {card_st.nsim} simulations x 16 trials',
fontsize=13, y=1.03,
)
plt.tight_layout()
fig.savefig(PROJECT_DIR / 'vviq16_figure.png', dpi=150, bbox_inches='tight')
plt.show()
print(f'Figure saved -> {PROJECT_DIR / "vviq16_figure.png"}')
n_missing = int(np.isnan(y_st).sum() + np.isnan(y_cv).sum())
if n_missing:
print(f'Warning: {n_missing} ratings could not be parsed')
else:
print(f'SingleTurn: M={np.nanmean(y_st):.2f}, SD={np.nanstd(y_st):.2f}')
print(f'Convo: M={np.nanmean(y_cv):.2f}, SD={np.nanstd(y_cv):.2f}')
Figure saved -> /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16_figure.png SingleTurn: M=3.81, SD=0.69 Convo: M=3.26, SD=0.92
6. Reload from disk and re-plot¶
Demonstrates loading the saved CSVs (or the .psyscan archive) without re-running the model.
In [7]:
Copied!
# Load from individual CSVs
df_st_reload = pl.read_csv(PROJECT_DIR / "vviq16_singleturn.csv")
df_cv_reload = pl.read_csv(PROJECT_DIR / "vviq16_convo.csv")
print("Reloaded from CSV:")
print(f" SingleTurn: {df_st_reload.shape}")
print(f" Convo: {df_cv_reload.shape}")
# Reload directly from the .psyscan archive using to_csv
data_dir = PROJECT_DIR / "vviq16study"
df_from_psyscan = to_csv(data_dir)
print(f"\nReloaded from .psyscan archive: {df_from_psyscan.shape}")
# Load from individual CSVs
df_st_reload = pl.read_csv(PROJECT_DIR / "vviq16_singleturn.csv")
df_cv_reload = pl.read_csv(PROJECT_DIR / "vviq16_convo.csv")
print("Reloaded from CSV:")
print(f" SingleTurn: {df_st_reload.shape}")
print(f" Convo: {df_cv_reload.shape}")
# Reload directly from the .psyscan archive using to_csv
data_dir = PROJECT_DIR / "vviq16study"
df_from_psyscan = to_csv(data_dir)
print(f"\nReloaded from .psyscan archive: {df_from_psyscan.shape}")
Reloaded from CSV: SingleTurn: (80, 20) Convo: (80, 20) Saved 160 rows → /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_vviq16_study/vviq16study/vviq16study_vviq16_smollm2-360m-instruct-fp16_Convo_20260507_100455.csv Reloaded from .psyscan archive: (160, 20)
Summary¶
| Step | Code |
|---|---|
| Define task | examples/tasks/vviq16.json — 16 items, parser: DefaultLiteralVivid15 |
| Run No Memory | card.memory = "SingleTurn", nsim=5 |
| Run Convo Memory | card.memory = "Convo", nsim=5 |
| Export CSVs | to_csv(scanner) and to_csv([...], combined=True) |
| Reload | to_csv(data_dir) or pl.read_csv(...) |
The Convo condition carries the full conversation history across the 16 trials, so the model's ratings can be influenced by its own earlier responses — a property not present in SingleTurn.