Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,35 @@ source_relevance:

**Nuisance parameters** can be marked `nuisance: true` when needed by the forward model but not part of the QSP model (e.g., a proliferation rate that helps constrain an activation rate). They carry their own inline prior, are sampled during MCMC, but are excluded from the output priors.

There's also a **CalibrationTarget** schema for clinical/in vivo observables (biopsies, blood draws) that require full model simulation — these feed into Stage 2.
There's also a **CalibrationTarget** schema for clinical/in vivo observables
(biopsies, blood draws) that require full model simulation — these feed into
Stage 2. A literature target's `observable.readout` records what was measured and
the closed-category facts of its measurement process:

```yaml
readout:
quantity_kind: fraction
assay_modality: mihc
measurement_context:
acquisition_method: resection
specimen_compartment: primary_tumor
specimen_material: tissue_section
preservation: ffpe
sampling_unit: region_of_interest
sampling_region: non_lymphoid_tumor
detection_basis: marker_defined_cells
analyte_pool: cellular
normalization_mode: cell_fraction_of_all_nucleated_cells
numerator_species: [V_T.CD8]
denominator_species: [V_T.CD8, V_T.Th, V_T.Treg]
```

Maple stores this factual vocabulary without choosing a statistical correction;
downstream analyses explicitly select which fields, reference levels, and prior
weights define a discrepancy model. Mechanistic assertions omit `readout`
because no experimental measurement was made. See the
[CalibrationTarget extraction guide](src/maple/prompts/calibration_target_prompt.md)
for the complete enum vocabulary and authoring rules.

---

Expand Down
20 changes: 20 additions & 0 deletions src/maple/core/calibration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@

# Enums
from maple.core.calibration.enums import (
AcquisitionMethod,
AnalytePool,
AssayModality,
Compartment,
DetectionBasis,
NormalizationMode,
Preservation,
QuantityKind,
REQUIRES_REFERENCE,
SamplingRegion,
SamplingUnit,
SpecimenCompartment,
SpecimenMaterial,
ExtractionMethod,
Indication,
IndicationMatch,
Expand Down Expand Up @@ -70,6 +79,7 @@
)

from maple.core.calibration.readout import (
MeasurementContext,
Readout,
ReadoutReference,
)
Expand Down Expand Up @@ -218,6 +228,15 @@
"ExtractionMethod",
"enum_field_description",
"AssayModality",
"AcquisitionMethod",
"SpecimenCompartment",
"SpecimenMaterial",
"Preservation",
"SamplingUnit",
"SamplingRegion",
"DetectionBasis",
"AnalytePool",
"NormalizationMode",
"QuantityKind",
"REQUIRES_REFERENCE",
# Reported distribution
Expand Down Expand Up @@ -252,6 +271,7 @@
"Observable",
"Readout",
"ReadoutReference",
"MeasurementContext",
"ObservableConstant",
"SupportType",
"Submodel",
Expand Down
6 changes: 6 additions & 0 deletions src/maple/core/calibration/calibration_target_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,12 @@ def validate_literature_target_is_placed(self) -> "CalibrationTarget":
"measurement-discrepancy design from its attributes, and its species "
"composition is the row's identity."
)
elif self.observable.readout.measurement_context is None:
missing.append(
"observable.readout.measurement_context: record the closed-category "
"facts of specimen acquisition, processing, sampling, detection, and "
"normalization. Storing a field does not itself activate a correction."
)
if self.empirical_data.observed_distribution is None:
missing.append(
"empirical_data.observed_distribution: list the statistics the source "
Expand Down
10 changes: 10 additions & 0 deletions src/maple/core/calibration/cross_scenario_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,16 @@ def validate_literature_arms_are_placed(self) -> "CrossScenarioCalibrationTarget
"""
if self.epistemic_basis != "literature":
return self
missing_context = [
i.role for i in self.observable.inputs if i.readout.measurement_context is None
]
if missing_context:
raise ValueError(
"epistemic_basis='literature' but these roles have no "
f"readout.measurement_context: {missing_context}. Each measured arm must "
"record how its specimen was acquired, processed, sampled, detected, and "
"normalized."
)
unplaced = [i.role for i in self.observable.inputs if not i.cohort_id]
if unplaced:
raise ValueError(
Expand Down
121 changes: 117 additions & 4 deletions src/maple/core/calibration/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,8 @@ class ExtractionMethod(str, Enum):
class QuantityKind(str, Enum):
"""What kind of quantity an observable is.

Read by inference to build the measurement-discrepancy design matrix. Two
observables of the same kind and modality share a row of it, and so share a
location and scale correction.
This defines the mathematical readout and its observation scale. It is
factual metadata, not a measurement-correction field.
"""

DENSITY = "density"
Expand Down Expand Up @@ -235,7 +234,11 @@ class QuantityKind(str, Enum):


class AssayModality(str, Enum):
"""How an observable was measured."""
"""How an observable was measured.

A downstream model may explicitly activate this field for shared
corrections; Maple only records the category.
"""

MIHC = "mihc"
"""Multiplex immunohistochemistry with digital image analysis."""
Expand Down Expand Up @@ -271,6 +274,116 @@ class AssayModality(str, Enum):
"""Recorded clinical observation rather than an assay."""


class AcquisitionMethod(str, Enum):
"""How the biological specimen or image was acquired."""

RESECTION = "resection"
NEEDLE_BIOPSY = "needle_biopsy"
BLOOD_DRAW = "blood_draw"
IN_SITU_IMAGING = "in_situ_imaging"
UNKNOWN = "unknown"


class SpecimenCompartment(str, Enum):
"""Anatomical compartment from which the measurement was obtained."""

PRIMARY_TUMOR = "primary_tumor"
PERIPHERAL_BLOOD = "peripheral_blood"
UNKNOWN = "unknown"


class SpecimenMaterial(str, Enum):
"""Physical material presented to the measurement procedure."""

TISSUE_SECTION = "tissue_section"
TISSUE_HOMOGENATE = "tissue_homogenate"
DISSOCIATED_CELLS = "dissociated_cells"
WHOLE_BLOOD = "whole_blood"
IN_SITU_TUMOR = "in_situ_tumor"
UNKNOWN = "unknown"


class Preservation(str, Enum):
"""Specimen-preservation state at measurement."""

FFPE = "ffpe"
FRESH = "fresh"
FROZEN = "frozen"
NOT_APPLICABLE = "not_applicable"
UNKNOWN = "unknown"


class SamplingUnit(str, Enum):
"""Physical unit over which the reported measurement was sampled."""

WHOLE_SECTION = "whole_section"
TISSUE_MICROARRAY_CORE = "tissue_microarray_core"
REGION_OF_INTEREST = "region_of_interest"
BULK_TISSUE_SAMPLE = "bulk_tissue_sample"
WHOLE_BLOOD_SAMPLE = "whole_blood_sample"
IMAGED_LESION = "imaged_lesion"
UNKNOWN = "unknown"


class SamplingRegion(str, Enum):
"""Region within the specimen selected for measurement."""

WHOLE_TUMOR = "whole_tumor"
SELECTED_TUMOR_REGION = "selected_tumor_region"
NON_LYMPHOID_TUMOR = "non_lymphoid_tumor"
LYMPHOID_AGGREGATE = "lymphoid_aggregate"
TUMOR_STROMA = "tumor_stroma"
TUMOR_CLUSTER = "tumor_cluster"
NOT_APPLICABLE = "not_applicable"
UNKNOWN = "unknown"


class DetectionBasis(str, Enum):
"""Operational basis used to identify or quantify the analyte."""

MARKER_DEFINED_CELLS = "marker_defined_cells"
TRANSCRIPTOMIC_CLUSTER = "transcriptomic_cluster"
SOLUBLE_ANALYTE = "soluble_analyte"
LABEL_FREE_MATRIX_SIGNAL = "label_free_matrix_signal"
MORPHOLOGIC_SEGMENTATION = "morphologic_segmentation"
ANATOMIC_IMAGING = "anatomic_imaging"
UNKNOWN = "unknown"


class AnalytePool(str, Enum):
"""Physical analyte pool represented by the reported quantity."""

CELLULAR = "cellular"
SOLUBLE_TISSUE_EXTRACT = "soluble_tissue_extract"
STRUCTURAL_MATRIX = "structural_matrix"
TISSUE_MORPHOLOGY = "tissue_morphology"
ANATOMIC_LESION = "anatomic_lesion"
UNKNOWN = "unknown"


class NormalizationMode(str, Enum):
"""Complete experimental denominator or normalization convention."""

CELL_COUNT_PER_TISSUE_AREA = "cell_count_per_tissue_area"
CELL_COUNT_PER_WHOLE_BLOOD_VOLUME = "cell_count_per_whole_blood_volume"
CELL_COUNT_RATIO_WITHIN_SAMPLING_UNIT = "cell_count_ratio_within_sampling_unit"
CELL_FRACTION_OF_ALL_NUCLEATED_CELLS = "cell_fraction_of_all_nucleated_cells"
CELL_FRACTION_OF_CD4_T_CELLS = "cell_fraction_of_cd4_t_cells"
CELL_FRACTION_OF_CD8_T_CELLS = "cell_fraction_of_cd8_t_cells"
CELL_FRACTION_OF_CAFS = "cell_fraction_of_cafs"
CELL_FRACTION_OF_TOTAL_DENDRITIC_CELLS = "cell_fraction_of_total_dendritic_cells"
FIBROBLAST_AREA_FRACTION_OF_CLASSIFIED_FIBROBLAST_AREA = (
"fibroblast_area_fraction_of_classified_fibroblast_area"
)
MATRIX_SIGNAL_AREA_FRACTION_OF_IMAGE_FIELD_AREA = (
"matrix_signal_area_fraction_of_image_field_area"
)
STROMA_AREA_FRACTION_OF_TOTAL_TISSUE_AREA = "stroma_area_fraction_of_total_tissue_area"
ANALYTE_AMOUNT_PER_TOTAL_PROTEIN_MASS = "analyte_amount_per_total_protein_mass"
ELAPSED_TIME_BETWEEN_SERIAL_IMAGES = "elapsed_time_between_serial_images"
UNKNOWN = "unknown"


# =============================================================================
# SOURCE RELEVANCE ENUMS
# =============================================================================
Expand Down
8 changes: 4 additions & 4 deletions src/maple/core/calibration/observable.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,10 @@ class Observable(BaseModel):

readout: Optional[Readout] = Field(
default=None,
description="What the experiment measured: quantity kind, assay, denominator, and "
"reference. The rest of this model is how to compute it. Required for a literature "
"target; null for a mechanistic one, which asserts a constraint rather than running "
"an assay.",
description="What the experiment measured: quantity kind, assay, structured "
"measurement context, denominator, and reference. The rest of this model is how "
"to compute it. Required for a literature target; null for a mechanistic one, "
"which asserts a constraint rather than running an assay.",
)

readout_time: Optional[float] = Field(
Expand Down
51 changes: 48 additions & 3 deletions src/maple/core/calibration/readout.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,41 @@

from pydantic import BaseModel, ConfigDict, Field, model_validator

from maple.core.calibration.enums import REQUIRES_REFERENCE, AssayModality, QuantityKind
from maple.core.calibration.enums import (
REQUIRES_REFERENCE,
AcquisitionMethod,
AnalytePool,
AssayModality,
DetectionBasis,
NormalizationMode,
Preservation,
QuantityKind,
SamplingRegion,
SamplingUnit,
SpecimenCompartment,
SpecimenMaterial,
)


class MeasurementContext(BaseModel):
"""Factual categorical description of a literature measurement process.

Maple records the complete context without deciding which fields create a
statistical correction. That activation, its references, and its prior
weights belong to the downstream analysis specification.
"""

model_config = ConfigDict(extra="forbid")

acquisition_method: AcquisitionMethod
specimen_compartment: SpecimenCompartment
specimen_material: SpecimenMaterial
preservation: Preservation
sampling_unit: SamplingUnit
sampling_region: SamplingRegion
detection_basis: DetectionBasis
analyte_pool: AnalytePool
normalization_mode: NormalizationMode


class ReadoutReference(BaseModel):
Expand Down Expand Up @@ -39,13 +73,24 @@ def _matches_kind(self) -> "ReadoutReference":


class Readout(BaseModel):
"""What was measured. Inference builds the measurement-discrepancy design from
these attributes, so two readouts agreeing on them share one correction."""
"""What was measured, separately from how the model computes it.

The fields are factual. A downstream analysis explicitly selects which, if
any, define shared measurement corrections.
"""

model_config = ConfigDict(extra="forbid")

quantity_kind: QuantityKind = Field(description="What kind of quantity was reported.")
assay_modality: AssayModality = Field(description="How it was measured.")
measurement_context: Optional[MeasurementContext] = Field(
default=None,
description=(
"Closed-category facts about specimen acquisition, processing, sampling, "
"detection, and normalization. Required for literature targets; absent on "
"mechanistic assertions that do not represent a measurement."
),
)
numerator_species: List[str] = Field(
min_length=1, description="Model species composing the numerator."
)
Expand Down
22 changes: 22 additions & 0 deletions src/maple/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@
from fastmcp import FastMCP

from maple.core.calibration.enums import (
AcquisitionMethod,
AnalytePool,
AssayModality,
DetectionBasis,
ExperimentalSystem,
IndicationMatch,
MeasurementDirectness,
NormalizationMode,
PerturbationType,
Preservation,
QuantityKind,
SamplingRegion,
SamplingUnit,
SourceQuality,
SpecimenCompartment,
SpecimenMaterial,
TemporalResolution,
TMECompatibility,
)
Expand Down Expand Up @@ -126,6 +137,17 @@ def _build_enum_reference() -> str:
_enum_table(MeasurementDirectness, "measurement_directness"),
_enum_table(TemporalResolution, "temporal_resolution"),
_enum_table(ExperimentalSystem, "experimental_system"),
_enum_table(QuantityKind, "quantity_kind"),
_enum_table(AssayModality, "assay_modality"),
_enum_table(AcquisitionMethod, "measurement_context.acquisition_method"),
_enum_table(SpecimenCompartment, "measurement_context.specimen_compartment"),
_enum_table(SpecimenMaterial, "measurement_context.specimen_material"),
_enum_table(Preservation, "measurement_context.preservation"),
_enum_table(SamplingUnit, "measurement_context.sampling_unit"),
_enum_table(SamplingRegion, "measurement_context.sampling_region"),
_enum_table(DetectionBasis, "measurement_context.detection_basis"),
_enum_table(AnalytePool, "measurement_context.analyte_pool"),
_enum_table(NormalizationMode, "measurement_context.normalization_mode"),
]
return "\n\n".join(sections)

Expand Down
Loading
Loading