Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ print(fixed_item_calibration.best)
- Rubric-centered schemas, deterministic bounded item-blueprint compilation,
and canonical provider-neutral generation contracts. See
[Rubric-Centered Item Generation](docs/rubric_item_generation.md).
- Provider-neutral contextual-orchestrator LLM-as-a-Judge integration with
strict structured parsing. A judge result becomes an IRT row only through
LLMJudgeResult.to_irt_row() with at least two criteria, followed by
validate_irt_response_matrix() for a multi-item dichotomous or explicitly
categorized polytomous matrix. Equal-width score projection is experimental;
category-count and prompt-perturbation calibration are required. See
[ADR 0005](https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/89bd5bf73319dd21f2be1f094eb2639bb8ead8f3/docs/planning/adrs/0005-irt-response-matrix-contract.md) and
[ADR 0006](https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/89bd5bf73319dd21f2be1f094eb2639bb8ead8f3/docs/planning/adrs/0006-polytomous-llm-judge-bias-calibration.md) and
[ADR 0008](https://github.com/ContextualWisdomLab/contextual-orchestrator/blob/2b65d5c0f3d6bd64a9e05818f1f9286e98c334c1/docs/planning/adrs/0008-fast-judge-review-hardening.md).
- Standalone HTML reports for saved fit or dimensionality diagnostics.
- Automated benchmark evidence reports from release-acceptance timing.
- Release evidence index reports that tie dist artifact hashes, acceptance,
Expand Down
20 changes: 20 additions & 0 deletions python/fast_mlsirm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
rotate_factor_loadings as rotate_factor_loadings,
rotation_criterion_value_gradient as rotation_criterion_value_gradient,
)
from .llm_judge import (
ContextualOrchestratorJudge as ContextualOrchestratorJudge,
JudgeCriterion as JudgeCriterion,
JudgeFormatError as JudgeFormatError,
LLMJudgeResult as LLMJudgeResult,
MAX_JUDGE_CATEGORIES as MAX_JUDGE_CATEGORIES,
)
from .irt_contract import (
IRTItemType as IRTItemType,
MIN_IRT_ITEMS as MIN_IRT_ITEMS,
validate_irt_response_matrix as validate_irt_response_matrix,
)

# Resolve distribution metadata at the package boundary on every reload. The
# compatibility module may remain cached, so its copied value is not sufficient
Expand All @@ -44,6 +56,14 @@
"available_rotation_criteria",
"rotate_factor_loadings",
"rotation_criterion_value_gradient",
"ContextualOrchestratorJudge",
"JudgeCriterion",
"JudgeFormatError",
"LLMJudgeResult",
"MAX_JUDGE_CATEGORIES",
"IRTItemType",
"MIN_IRT_ITEMS",
"validate_irt_response_matrix",
]

del _PackageNotFoundError, _distribution_version, _public_name
79 changes: 79 additions & 0 deletions python/fast_mlsirm/irt_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Explicit response-shape checks for cross-component IRT inputs."""

from __future__ import annotations

from collections.abc import Iterable
from typing import Literal

import numpy as np

from .config import MAX_POLYTOMOUS_CATEGORIES

IRTItemType = Literal["dichotomous", "polytomous"]
MIN_IRT_ITEMS = 2


def validate_irt_response_matrix(
responses: Iterable[Iterable[float]] | np.ndarray,
item_type: IRTItemType,
*,
n_categories: int | None = None,
) -> np.ndarray:
"""Validate and return a persons-by-items matrix for an IRT experiment.

This is the integration boundary for response data produced by another
component, such as an LLM judge. It deliberately requires at least two
item columns. Low-level one-item numerical primitives remain available for
diagnostics and unit tests, but a one-item result is not an IRT experiment
contract.

NaN denotes a missing response. Dichotomous observations are 0/1;
polytomous observations are integer category indices in
0..n_categories-1.
"""
if item_type not in {"dichotomous", "polytomous"}:
raise ValueError("item_type must be 'dichotomous' or 'polytomous'")
if item_type == "dichotomous" and n_categories is not None:
raise ValueError("n_categories is only valid for polytomous responses")
if item_type == "polytomous" and (
not isinstance(n_categories, int)
or isinstance(n_categories, bool)
or not 2 <= n_categories <= MAX_POLYTOMOUS_CATEGORIES
):
raise ValueError(
"polytomous responses require n_categories in "
f"2..{MAX_POLYTOMOUS_CATEGORIES}"
)

try:
matrix = np.asarray(responses, dtype=np.float64)
except (TypeError, ValueError) as exc:
raise ValueError("responses must be numeric") from exc
if matrix.ndim != 2:
raise ValueError("responses must be a 2-D persons x items matrix")
n_persons, n_items = matrix.shape
if n_persons < 1:
raise ValueError("responses must contain at least one person")
if n_items < MIN_IRT_ITEMS:
raise ValueError(
"IRT responses must contain at least two item columns; "
"a scalar or one-item result is not an IRT experiment"
)

missing = np.isnan(matrix)
if np.any(~missing & ~np.isfinite(matrix)):
raise ValueError("observed responses must be finite or NaN")
observed = matrix[~missing]
if observed.size and np.any(observed != np.floor(observed)):
raise ValueError("observed responses must be integer category values")
if item_type == "dichotomous":
if observed.size and np.any((observed < 0) | (observed > 1)):
raise ValueError("dichotomous responses must be 0, 1, or NaN")
elif observed.size and np.any((observed < 0) | (observed >= int(n_categories))):
raise ValueError(
f"polytomous responses must be in 0..{int(n_categories) - 1} or NaN"
)
return matrix


__all__ = ["MIN_IRT_ITEMS", "IRTItemType", "validate_irt_response_matrix"]
Loading
Loading