use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::adapter::ModelInputSpec;
use crate::builtin_models::{
BuiltinDataModel, BUILTIN_DATA_MODELS, REPRESENTATION_FEATURE_BLOCK_SET,
REPRESENTATION_GRAY_IMAGE, REPRESENTATION_MC_IMAGE, REPRESENTATION_MULTISPECTRAL_IMAGE,
REPRESENTATION_RGB_IMAGE, REPRESENTATION_SAMPLE_METADATA, REPRESENTATION_SIGNAL_1D,
REPRESENTATION_SIGNAL_WITH_PROCESSINGS, REPRESENTATION_TARGET_CATEGORICAL,
REPRESENTATION_TARGET_CATEGORICAL_MATRIX, REPRESENTATION_TARGET_NUMERIC,
REPRESENTATION_TARGET_NUMERIC_MATRIX,
};
use crate::error::{DataError, Result};
use crate::model::RepresentationSpec;
pub const REPRESENTATION_REGISTRY_SCHEMA_VERSION: u32 = 1;
pub const REPRESENTATION_REGISTRY_ID: &str = "dag-ml-data.representation_registry.v1";
pub const REPRESENTATION_REGISTRY_SOURCE: &str = "crates/dag-ml-data-core/src/builtin_models.rs";
pub const MVP_PROFILE_SPECTRA_IMAGE: &str = "spectra_image";
const MVP_SPECTRA_IMAGE_EMITTED: &[&str] = &[
REPRESENTATION_SIGNAL_1D,
REPRESENTATION_SIGNAL_WITH_PROCESSINGS,
REPRESENTATION_FEATURE_BLOCK_SET,
REPRESENTATION_TARGET_NUMERIC,
REPRESENTATION_TARGET_CATEGORICAL,
REPRESENTATION_TARGET_NUMERIC_MATRIX,
REPRESENTATION_TARGET_CATEGORICAL_MATRIX,
REPRESENTATION_SAMPLE_METADATA,
];
const MVP_SPECTRA_IMAGE_PENDING: &[&str] = &[
REPRESENTATION_GRAY_IMAGE,
REPRESENTATION_RGB_IMAGE,
REPRESENTATION_MC_IMAGE,
REPRESENTATION_MULTISPECTRAL_IMAGE,
];
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MvpEmissionStatus {
Emitted,
LandedPendingEmit,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MvpStatus {
pub profile: String,
pub emission: MvpEmissionStatus,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RegisteredRepresentation {
pub representation_id: String,
pub builtin_key: String,
pub modality: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mvp: Option<MvpStatus>,
pub representation: RepresentationSpec,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RepresentationRegistry {
pub schema_version: u32,
pub registry_id: String,
pub source: String,
pub representations: Vec<RegisteredRepresentation>,
}
pub fn representation_registry() -> RepresentationRegistry {
RepresentationRegistry {
schema_version: REPRESENTATION_REGISTRY_SCHEMA_VERSION,
registry_id: REPRESENTATION_REGISTRY_ID.to_string(),
source: REPRESENTATION_REGISTRY_SOURCE.to_string(),
representations: BUILTIN_DATA_MODELS
.iter()
.copied()
.map(registered_representation)
.collect(),
}
}
impl RepresentationRegistry {
pub fn validate(&self) -> Result<()> {
if self.schema_version != REPRESENTATION_REGISTRY_SCHEMA_VERSION {
return Err(DataError::Validation(format!(
"representation registry uses unsupported schema_version {}, expected {}",
self.schema_version, REPRESENTATION_REGISTRY_SCHEMA_VERSION
)));
}
if self.registry_id != REPRESENTATION_REGISTRY_ID {
return Err(DataError::Validation(format!(
"representation registry id `{}` does not match `{}`",
self.registry_id, REPRESENTATION_REGISTRY_ID
)));
}
if self.representations.is_empty() {
return Err(DataError::Validation(
"representation registry contains no representations".to_string(),
));
}
let mut ids = BTreeSet::new();
for entry in &self.representations {
if entry.representation_id.trim().is_empty() {
return Err(DataError::Validation(
"representation registry contains an empty representation_id".to_string(),
));
}
if !ids.insert(entry.representation_id.as_str()) {
return Err(DataError::Validation(format!(
"representation registry contains duplicate representation `{}`",
entry.representation_id
)));
}
if entry.representation_id != entry.representation.id.as_str() {
return Err(DataError::Validation(format!(
"representation registry entry `{}` does not match embedded representation id `{}`",
entry.representation_id, entry.representation.id
)));
}
entry.representation.validate()?;
}
Ok(())
}
}
pub fn validate_model_input_spec_against_registry(
model_input: &ModelInputSpec,
registry: &RepresentationRegistry,
) -> Result<()> {
registry.validate()?;
model_input.validate()?;
let by_id = registry
.representations
.iter()
.map(|entry| (entry.representation_id.as_str(), entry))
.collect::<BTreeMap<_, _>>();
for port in &model_input.ports {
for representation_id in &port.accepted_representations {
let representation_key = representation_id.as_str();
let Some(entry) = by_id.get(representation_key) else {
return Err(DataError::Validation(format!(
"model input port `{}` accepts unknown representation `{representation_key}`",
port.name
)));
};
if !port
.accepted_types
.iter()
.any(|type_id| type_id == &entry.representation.type_id)
{
return Err(DataError::Validation(format!(
"model input port `{}` representation `{}` has type `{}` not listed in accepted_types",
port.name, representation_key, entry.representation.type_id
)));
}
if let Some(rank) = port.rank {
if entry.representation.rank != Some(rank) {
return Err(DataError::Validation(format!(
"model input port `{}` requires rank {rank} but representation `{}` has {:?}",
port.name, representation_key, entry.representation.rank
)));
}
}
}
}
Ok(())
}
fn registered_representation(model: BuiltinDataModel) -> RegisteredRepresentation {
let representation = model.representation();
let representation_id = representation.id.as_str().to_string();
let mvp = mvp_status(&representation_id);
RegisteredRepresentation {
representation_id,
builtin_key: model.key().to_string(),
modality: model.modality().to_string(),
mvp,
representation,
}
}
fn mvp_status(representation_id: &str) -> Option<MvpStatus> {
if MVP_SPECTRA_IMAGE_EMITTED.contains(&representation_id) {
Some(MvpStatus {
profile: MVP_PROFILE_SPECTRA_IMAGE.to_string(),
emission: MvpEmissionStatus::Emitted,
})
} else if MVP_SPECTRA_IMAGE_PENDING.contains(&representation_id) {
Some(MvpStatus {
profile: MVP_PROFILE_SPECTRA_IMAGE.to_string(),
emission: MvpEmissionStatus::LandedPendingEmit,
})
} else {
None
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::adapter::InputPortSpec;
use crate::ids::{RepresentationId, TypeId};
use super::*;
const PUBLISHED: &str = include_str!("../../../docs/contracts/representation_registry.v1.json");
#[test]
fn published_registry_matches_builtin_models() {
let generated = serde_json::to_value(representation_registry()).unwrap();
let published: serde_json::Value = serde_json::from_str(PUBLISHED).unwrap();
assert_eq!(
published, generated,
"docs/contracts/representation_registry.v1.json drifted from builtin_models.rs; \
regenerate with `cargo run -p dag-ml-data-cli -- representation-registry`"
);
}
#[test]
fn registry_header_is_stable() {
let registry = representation_registry();
assert_eq!(
registry.schema_version,
REPRESENTATION_REGISTRY_SCHEMA_VERSION
);
assert_eq!(registry.registry_id, REPRESENTATION_REGISTRY_ID);
assert_eq!(registry.source, REPRESENTATION_REGISTRY_SOURCE);
}
#[test]
fn registry_covers_every_builtin_with_unique_ids() {
let registry = representation_registry();
assert_eq!(registry.representations.len(), BUILTIN_DATA_MODELS.len());
let mut ids = BTreeSet::new();
let mut keys = BTreeSet::new();
for entry in ®istry.representations {
assert!(
ids.insert(entry.representation_id.clone()),
"duplicate representation id {}",
entry.representation_id
);
assert!(
keys.insert(entry.builtin_key.clone()),
"duplicate builtin key {}",
entry.builtin_key
);
assert_eq!(entry.representation_id, entry.representation.id.as_str());
entry.representation.validate().unwrap();
}
}
#[test]
fn spectra_image_mvp_profile_is_consistent() {
let registry = representation_registry();
let frozen: BTreeSet<&str> = registry
.representations
.iter()
.map(|entry| entry.representation_id.as_str())
.collect();
let emitted: BTreeSet<&str> = registry
.representations
.iter()
.filter(|entry| {
matches!(&entry.mvp, Some(status) if status.emission == MvpEmissionStatus::Emitted)
})
.map(|entry| entry.representation_id.as_str())
.collect();
let pending: BTreeSet<&str> = registry
.representations
.iter()
.filter(|entry| {
matches!(
&entry.mvp,
Some(status) if status.emission == MvpEmissionStatus::LandedPendingEmit
)
})
.map(|entry| entry.representation_id.as_str())
.collect();
assert_eq!(emitted.len(), 8, "spectra MVP must publish 8 emitted IDs");
assert_eq!(pending.len(), 4, "image MVP must publish 4 pending IDs");
assert!(emitted.is_disjoint(&pending), "MVP groups must be disjoint");
for id in emitted.iter().chain(pending.iter()) {
assert!(
frozen.contains(id),
"MVP id {id} is not a frozen representation"
);
}
assert_eq!(
pending,
BTreeSet::from([
REPRESENTATION_GRAY_IMAGE,
REPRESENTATION_RGB_IMAGE,
REPRESENTATION_MC_IMAGE,
REPRESENTATION_MULTISPECTRAL_IMAGE,
])
);
for entry in ®istry.representations {
if let Some(status) = &entry.mvp {
assert_eq!(status.profile, MVP_PROFILE_SPECTRA_IMAGE);
}
}
}
#[test]
fn registry_round_trips_through_json() {
let registry = representation_registry();
let json = serde_json::to_string(®istry).unwrap();
let decoded: RepresentationRegistry = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, registry);
}
#[test]
fn model_input_spec_registry_validation_accepts_published_tabular_requirement() {
let spec = crate::tabular_numeric_model_input_spec();
validate_model_input_spec_against_registry(&spec, &representation_registry()).unwrap();
}
#[test]
fn model_input_spec_registry_validation_rejects_unknown_representation() {
let spec = ModelInputSpec {
ports: vec![InputPortSpec {
name: "x".to_string(),
accepted_representations: vec![RepresentationId::new("columnar_f64").unwrap()],
accepted_types: vec![TypeId::new("table").unwrap()],
rank: Some(2),
multi_source: false,
optional: false,
}],
default_fusion: None,
};
let error = validate_model_input_spec_against_registry(&spec, &representation_registry())
.unwrap_err();
assert!(
error.to_string().contains("unknown representation"),
"unexpected error: {error}"
);
}
#[test]
fn model_input_spec_registry_validation_rejects_type_drift() {
let mut spec = crate::tabular_numeric_model_input_spec();
spec.ports[0].accepted_types = vec![TypeId::new("f64").unwrap()];
let error = validate_model_input_spec_against_registry(&spec, &representation_registry())
.unwrap_err();
assert!(
error.to_string().contains("not listed in accepted_types"),
"unexpected error: {error}"
);
}
}