Configuration and schema¶
Loading and resolution¶
p2p.config.Config.load() reads one YAML manifest, recursively resolves
extends, applies defaults, resolves known paths, expands structured grids,
and freezes the result. Mapping overrides merge recursively; sequences and
scalars replace their inherited value.
Paths are resolved relative to the final declaring manifest. A run writes
config.resolved.yaml with the absolute resolved values and a stable SHA256
configuration digest.
Top-level sections¶
runRun identity and deterministic seed. Common fields include
name,seed,repeat,wave, and labels used by result records.dataSurvey CSV/schema, cached basis, target marginals, predictions, or fitted artifacts. The required members depend on the verb.
generationStrategy (
active,vanilla, or injected), population size, adaptive-generation controls, endowment-model settings, and optional attribute bank.backendA shared backend profile or role-specific
response,endowment, andattribute_learnermappings.type: deepseekuses the provider-specific V4 adapter. It readsDEEPSEEK_API_KEYand defaults to the official API endpoint. Itskwargsacceptthinking: enabled|disabled, optionalreasoning_effort: high|max,temperature,max_tokens, and an optionalbase_urloverride. Reasoning effort is valid only when thinking is enabled; temperature is deliberately omitted from thinking requests.config/backends/deepseek.yamlis a non-thinking profile for forced-choice survey answers.type: openaiacceptstransport: chat_completions|responses(defaultchat_completions; never switched silently),store(defaultfalse: both APIs may store responses by default, and P2P never retrieves them),temperature, andmax_tokens.max_tokensis P2P’s provider-neutral output limit: it is sent asmax_completion_tokensto Chat Completions andmax_output_tokensto Responses, and both bound visible plus reasoning tokens. With Responses, the system prompt is sent asinstructionsand the question asinput.type: simulatedanswers P2P’s Stage-1 prompts by deterministic rules from a declarativeworld(attributes whose values prefer answer options); it is not a language model. It lets the whole pipeline run offline for demonstrations and tests (config/backends/simulated_demo.yaml); usage is zero. Itskwargsareworld,seedandnoise.type: geminiuses thegoogle-genaiSDK and acceptstemperatureandmax_tokens(sent asmax_output_tokens).model_namehas no default: Gemini models are retired on a published schedule, so every configuration names its model.A refusal, a blocked prompt, or a reply that ends without any text raises
NoAnswerTextErrornaming the provider’s stated reason (refusal text,finish_reason,block_reason, or incomplete status). Partial text from a reply cut off at the token limit is returned unchanged.modelRegression method and hyperparameters: constrained
lassoorelasticnet(optionally block-weighted), orsimple_average. See “Stage-2 model options” below.splitQuestion labels used for train/validation and test plus
use_proxy_only.metricsandevaluationRequested metric names (
mse,distribution_mse,one_minus_jsd,emd,mcpa; all scored on the test split) and explicit prediction/invalid-distribution policies.aggregateLocked run list, asserted labels, grouping, expected completeness, hierarchy, summary statistics, and export formats.
simulation,ablate, andpanelSweep declarations and dotted or nested overrides.
clusterOffline clustering algorithm and number of clusters.
Verb requirements¶
Verb |
Required contract |
|---|---|
|
Survey, generation strategy, and required backend roles. |
|
Generation contract plus target, split, model, and metrics. |
|
Survey, cached basis, target, split, model, and metrics. |
|
Survey, target, split, metrics, and either predictions or fitted input. |
|
Parsed |
|
|
|
Corresponding sweep mapping. |
|
|
Minimal pipeline example¶
run:
name: example_pipeline
seed: 101
data:
survey:
source: ../../tests/fixtures/data/survey.csv
schema: ../../tests/fixtures/data/survey_schema.yaml
target: ../../tests/fixtures/commands/pipeline/target.json
generation:
mode: vanilla
target_n: 2
batch_size: 2
backend:
response:
type: dummy
model_name: dummy
kwargs: {answer_text: Option A}
endowment:
type: dummy
model_name: dummy
kwargs:
answer_text: >-
[{"eid": "a", "endow_text": "Synthetic A"},
{"eid": "b", "endow_text": "Synthetic B"}]
model:
method: lasso
alpha: [0.01]
validation: {strategy: cv, cv_folds: 2}
split:
train_val: [train, valid]
test: test
use_proxy_only: true
metrics: [mse, one_minus_jsd, emd, mcpa]
evaluation:
mse_prediction_policy: raw
distribution_prediction_policy: clip_and_normalize
invalid_distribution: error
Structured grids¶
Literal lists and safe structured grids replace evaluated NumPy expressions:
model:
alpha:
logspace: [-4, 0, 10]
l1_ratio:
linspace: [0.1, 0.9, 5]
Arbitrary expressions are rejected.
Stage-2 model options¶
model.method is lasso, elasticnet or simple_average. For the two
fitted methods, the options below are read; an unknown option is refused with a
spelling suggestion. Ridge is elastic net with l1_ratio: [0.0]; because its
many small weights are all meaningful, give it a small zero_threshold (the
Studio writes 1e-10).
Option |
Default |
Meaning |
|---|---|---|
|
|
Penalty grid. When omitted, P2P’s default grid is used and the run’s
|
|
|
Elastic net only: the lasso share of the penalty (1 is pure lasso). |
|
|
|
|
|
Number of folds ( |
|
|
Fold assignment ( |
|
|
Strength of the soft per-question mass penalty, which pulls each
question’s predicted mass towards |
|
|
Adds a basis column that answers every option of a k-option question
with 1/k, weighted like an agent ( |
|
|
Adds an unpenalised constant on every option row; the mass penalty sees the k × intercept it adds to a question. |
|
|
Weights each question equally in the fit, whatever its number of options. |
|
|
Coefficients smaller in magnitude are treated as zero. |
|
|
Hard forms of the constraints. Signed weights (anti-type agents) are
intended, so |
The six combinations of the mass penalty (on or off) with the reference
respondent, the intercept or neither are the Stage-2 configurations Studio offers
by name. max_iter and post_selection_refit are accepted so older
manifests stay readable, but they have no effect and are recorded as
ignored_model_options.
The solver is a command-line choice, not a manifest option:
p2p regress --solver OSQP (default CLARABEL). If a solve fails, P2P falls
back in the order Clarabel → OSQP → SCS and records the solver it used.
Reference distribution¶
data.reference_distribution names a JSON file in the target format, for
example an earlier survey wave’s marginals. It enters every prediction with
weight one:
data:
target: targets_w6.json
reference_distribution: targets_w7.json # prediction = reference + agents
The agents then explain each target’s deviation from the reference rather than
its level, which is the drift refit across survey waves. The file is checked like
the targets (every survey question present, each distribution non-negative and
summing to one), and each question’s reference mass over its modelled option rows
must be one. The mass penalty keeps its meaning: each question’s full prediction
is pulled towards group_total, so when every agent answers every question the
agents’ weights sum to zero. soft_group_sum: false imposes this exactly.
Predictions are full distributions, so every metric is computed as usual, and the
run records the file and its digest (reference_distribution_digest in the
diagnostics). Because the reference’s weight is fixed at one, it cannot be combined
with intercept, reference_respondent, sum_to_one, nonneg,
method: simple_average or evaluation.distribution_route: answer_file;
such a manifest is refused.
Evaluation policies¶
Raw MSE always scores the raw predictions (mse_prediction_policy: raw).
The distributional metrics (rescaled MSE, 1 − JSD, EMD, MCPA) score each
question’s prediction after distribution_prediction_policy has turned it into
a distribution:
clip_and_normalize(default)Set negative values to zero, then rescale each question to sum to one.
project_to_simplexReplace each question’s prediction by the closest probability distribution (Euclidean projection).
signed_sum_normalizeDivide by the signed sum (first by 100 if that sum exceeds 1.5), keeping negative values, and score with SciPy’s routines, which renormalise their inputs.
legacy_signed_normalizeis its earlier spelling and is still accepted.validateScore predictions unchanged; one that is not already a distribution is invalid.
invalid_distribution decides what happens to a question whose prediction is
still not a distribution: error (default) stops the evaluation; allow
leaves it out of the distributional metrics and records how many were left out.
distribution_route chooses where the distributional metrics’ predictions
come from: model (default, the model’s own predictions, as MSE uses) or
answer_file (the agents’ answer file, re-aggregated by agent name with the
fitted weights;
legacy_answer_file is its earlier spelling and is still accepted).
Every metric record names the policy and route it was computed under, and the earlier spellings are recorded exactly as written, so existing manifests keep their digests.
No-regression baseline¶
method: simple_average weights every agent of the cleaned proxy panel equally
instead of fitting weights. It uses the same loader, cleaning and evaluation as a
fitted model, so the comparison isolates the regression step:
model:
method: simple_average
evaluation:
distribution_prediction_policy: validate
invalid_distribution: error
It takes no other model options; a penalty grid, validation settings, the reference respondent or an intercept are refused rather than ignored. The fit also refuses a design in which any agent lacks exactly one answer on a question, so every question is averaged over the same agents.
Grouped targets¶
Existing flat target files remain {qid: {label: probability}}. A grouped
target is an explicit schema:
{
"schema_version": 1,
"default_subgroup": "overall",
"subgroups": {
"overall": {"Q1": {"yes": 0.5, "no": 0.5}},
"party.democrat": {"Q1": {"yes": 0.7, "no": 0.3}}
}
}
Select a named slice without changing the survey, basis, split, model, or metric kernels:
data:
target:
path: targets_by_subgroup.json
subgroup: party.democrat
With multiple groups, selection must be explicit or the file must declare
default_subgroup. The loader validates the selected distributions
without repair and records both the complete source digest and selected-slice
digest.
Scientific simulations¶
simulation.executor: scientific enables the maintained offline executor.
It requires the normal cached-basis data, split, and model
sections. A typical proxy-fraction sweep is:
simulation:
executor: scientific
kind: proxy
invokes_generation: false
target_source: configured
ground_truth:
source: existing
fraction: 1.0
sweep:
target: simulate
repeats: 3
overrides:
- simulation: {fraction: 0.25}
- simulation: {fraction: 0.5}
- simulation: {fraction: 1.0}
Kinds are endowment, response, proxy, trainvalid,
entropy_modes, and target_entropy. Fixed simulated targets use
target_source: ground_truth plus a ground-truth assignment with
source: existing, random, or modes. entropy_modes requires
included_mode_count and n_proxies. The new entropy-target estimand is
opt-in:
simulation:
executor: scientific
kind: target_entropy
sample_size: 30
target_mean_entropy: 0.6
max_evaluations: 5000
It uses the same mean normalized categorical entropy that active generation tracks, but is never called by active generation.
Attribute banks¶
An active-generation bank may be a packaged resource or manifest-relative file:
generation:
mode: active
attribute_bank:
source: builtin:default
The loader returns immutable data. Generation deep-copies it into run-local state before adding survey- or question-specific attributes.
Backend profiles and secrets¶
Reusable backend mappings can live under config/backends/ and be
referenced by path. Keep credentials out of YAML. Gemini can read
GEMINI_API_KEY; the OpenAI client follows its standard environment
configuration. Ollama uses a local base_url and needs no API key.
Budget pricing¶
Provider prices are intentionally separate from central scientific
configuration because they change independently. p2p budget --pricing
accepts:
models:
provider-model-name:
input_per_million_usd: 1.0
output_per_million_usd: 4.0
cached_input_per_million_usd: 0.25
All values are nonnegative USD per one million tokens. Copy
config/pricing.example.yaml and replace its illustrative zeros with the
provider’s current published rates. The resolved source is recorded beside
each forecast. P2P does not treat effective rates from earlier runs as
current prices.
Earlier-format configurations¶
p2p.config_conversion.convert_earlier_config() converts regression,
pipeline, ablation, panel and simulation YAML written in the earlier
configuration format into the central contract. It is a one-off conversion
helper, not a promise that every earlier key is accepted. Review the result
before a paid or large local run. See Earlier formats.