use serde::Serialize;
pub const SYNTHESIZABILITY_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssessmentStatus {
InvalidTarget,
EvaluationError,
NoRouteFoundWithinBudget,
Indeterminate,
RoutesFoundButRejected,
RouteSupportedWithValidationGaps,
RouteSupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StockTerminationStatus {
AllLeavesVerifiedInConfiguredStock,
OneOrMoreLeavesNotInStock,
StockNotSupplied,
StockSuppliedButEmpty,
StockIdentityUnavailable,
StockCheckNotPerformed,
StockCheckError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StockInputStatus {
NotSupplied,
SuppliedButEmpty,
AllEntriesInvalid,
PartiallyUsable,
FullyUsable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ForwardValidationStatus {
AllEvaluatedStepsValid,
OneOrMoreStepsInvalid,
PartiallyEvaluated,
NotEvaluated,
ValidatorError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ElementAccountingStatus {
Accounted,
UnaccountedTargetElement,
NotEvaluable,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct EvidenceCoverage {
pub exact_substrate_evidence_steps: usize,
pub template_level_evidence_steps: usize,
pub steps_without_evidence: usize,
pub steps_with_reported_yield: usize,
pub steps_with_conditions: usize,
pub steps_with_warnings: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HardFailure {
RouteStructureUnparseable,
StockTerminalMismatch { leaf: String },
UnaccountedTargetElement { step_index: usize },
ForwardValidationFailed { step_index: usize },
RouteGraphInconsistent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ValidationGap {
ForwardValidationNotRun,
NoEvidence,
NoExactSubstrateEvidence,
StepNotEvaluable { step_index: usize },
StockNotSupplied,
StockProvenanceHashMissing,
ReagentOmissionAccountingGap {
step_index: usize,
template_id: String,
},
UnaccountedTargetElementNotEnforced { step_index: usize },
BestEffortRouteOnly,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RouteAssessment {
pub route_id: String,
pub route_depth: u32,
pub route_cost: f64,
pub stock_termination_status: StockTerminationStatus,
pub target_element_accounting_status: ElementAccountingStatus,
pub forward_validation_status: ForwardValidationStatus,
pub evidence_coverage: EvidenceCoverage,
pub hard_failures: Vec<HardFailure>,
pub validation_gaps: Vec<ValidationGap>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ForwardValidationPolicy {
Ignore,
RequireAllValid,
RequireNoInvalid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidencePolicy {
Ignore,
RequireAnyEvidence,
RequireExactSubstrate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AccountingFailurePolicy {
HardFailure,
ValidationGap,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SynthesizabilityConfig {
pub require_verified_stock_terminal: bool,
pub require_target_element_accounting: bool,
pub forward_validation_policy: ForwardValidationPolicy,
pub evidence_policy: EvidencePolicy,
pub reagent_omission_template_allowlist: Vec<String>,
pub accounting_failure_policy: AccountingFailurePolicy,
pub max_route_diagnostics: usize,
pub include_all_route_diagnostics: bool,
}
const DEFAULT_MAX_ROUTE_DIAGNOSTICS: usize = 10;
impl SynthesizabilityConfig {
fn default_reagent_omission_allowlist() -> Vec<String> {
vec![
"rule:boc_deprotection_retro".to_string(),
"rule:cbz_deprotection_retro".to_string(),
]
}
pub fn conservative() -> Self {
Self {
require_verified_stock_terminal: true,
require_target_element_accounting: true,
forward_validation_policy: ForwardValidationPolicy::Ignore,
evidence_policy: EvidencePolicy::Ignore,
reagent_omission_template_allowlist: Self::default_reagent_omission_allowlist(),
accounting_failure_policy: AccountingFailurePolicy::HardFailure,
max_route_diagnostics: DEFAULT_MAX_ROUTE_DIAGNOSTICS,
include_all_route_diagnostics: false,
}
}
pub fn diagnostic() -> Self {
Self {
accounting_failure_policy: AccountingFailurePolicy::ValidationGap,
..Self::conservative()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SynthesizabilityConfigSummary {
pub require_verified_stock_terminal: bool,
pub require_target_element_accounting: bool,
pub forward_validation_policy: ForwardValidationPolicy,
pub evidence_policy: EvidencePolicy,
pub reagent_omission_template_allowlist: Vec<String>,
pub accounting_failure_policy: AccountingFailurePolicy,
pub max_route_diagnostics: usize,
pub include_all_route_diagnostics: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct AssessmentProvenance {
pub renkin_version: String,
pub assessment_schema_version: u32,
pub canonical_target: String,
pub rules_count: usize,
pub rules_hash: String,
pub stock_count: usize,
pub stock_hash: String,
pub stock_input_status: StockInputStatus,
pub stock_source: Option<String>,
pub template_metadata_hash: Option<String>,
pub search_config_summary: Option<String>,
pub assessment_config_hash: String,
pub git_commit: Option<String>,
pub embedded_fallback_used: Option<bool>,
pub reproducibility_hash: String,
pub reproducibility_exclusions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SynthesizabilityAssessment {
pub schema_version: u32,
pub target: String,
pub canonical_target: Option<String>,
pub status: AssessmentStatus,
pub selected_route: Option<RouteAssessment>,
pub route_assessments: Vec<RouteAssessment>,
pub routes_supplied_count: usize,
pub routes_assessed_count: usize,
pub route_assessments_returned_count: usize,
pub route_assessments_truncated: bool,
pub provenance: AssessmentProvenance,
pub config_used: SynthesizabilityConfigSummary,
pub warnings: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conservative_config_has_documented_defaults() {
let cfg = SynthesizabilityConfig::conservative();
assert!(cfg.require_verified_stock_terminal);
assert!(cfg.require_target_element_accounting);
assert_eq!(
cfg.forward_validation_policy,
ForwardValidationPolicy::Ignore
);
assert_eq!(cfg.evidence_policy, EvidencePolicy::Ignore);
assert_eq!(
cfg.reagent_omission_template_allowlist,
vec![
"rule:boc_deprotection_retro".to_string(),
"rule:cbz_deprotection_retro".to_string(),
]
);
assert!(
!cfg.reagent_omission_template_allowlist
.iter()
.any(|t| t == "rule:aryl_amine_retro"),
"aryl_amine_retro must not be on the default allowlist -- see issue #73"
);
}
#[test]
fn diagnostic_config_differs_from_conservative_only_in_accounting_failure_policy() {
let conservative = SynthesizabilityConfig::conservative();
let diagnostic = SynthesizabilityConfig::diagnostic();
assert_eq!(
conservative.accounting_failure_policy,
AccountingFailurePolicy::HardFailure
);
assert_eq!(
diagnostic.accounting_failure_policy,
AccountingFailurePolicy::ValidationGap
);
assert_eq!(
SynthesizabilityConfig {
accounting_failure_policy: AccountingFailurePolicy::HardFailure,
..diagnostic
},
conservative
);
}
#[test]
fn statuses_implement_debug_and_partial_eq() {
assert_eq!(
AssessmentStatus::RouteSupported,
AssessmentStatus::RouteSupported
);
assert_ne!(
AssessmentStatus::RouteSupported,
AssessmentStatus::InvalidTarget
);
assert_eq!(
StockTerminationStatus::AllLeavesVerifiedInConfiguredStock,
StockTerminationStatus::AllLeavesVerifiedInConfiguredStock
);
assert_eq!(
ForwardValidationStatus::NotEvaluated,
ForwardValidationStatus::NotEvaluated
);
assert_eq!(
ElementAccountingStatus::Accounted,
ElementAccountingStatus::Accounted
);
assert_eq!(
HardFailure::RouteGraphInconsistent,
HardFailure::RouteGraphInconsistent
);
assert_ne!(
HardFailure::UnaccountedTargetElement { step_index: 0 },
HardFailure::UnaccountedTargetElement { step_index: 1 }
);
assert_eq!(
ValidationGap::BestEffortRouteOnly,
ValidationGap::BestEffortRouteOnly
);
let _ = format!(
"{:?}",
HardFailure::StockTerminalMismatch {
leaf: "CCO".to_string()
}
);
}
fn sample_route_assessment() -> RouteAssessment {
RouteAssessment {
route_id: "sha256:deadbeef".to_string(),
route_depth: 2,
route_cost: 1.5,
stock_termination_status: StockTerminationStatus::AllLeavesVerifiedInConfiguredStock,
target_element_accounting_status: ElementAccountingStatus::Accounted,
forward_validation_status: ForwardValidationStatus::NotEvaluated,
evidence_coverage: EvidenceCoverage::default(),
hard_failures: vec![],
validation_gaps: vec![ValidationGap::ForwardValidationNotRun],
warnings: vec![],
}
}
#[test]
fn synthesizability_assessment_smoke_test_serializes() {
let route = sample_route_assessment();
let assessment = SynthesizabilityAssessment {
schema_version: SYNTHESIZABILITY_SCHEMA_VERSION,
target: "c1ccccc1".to_string(),
canonical_target: Some("c1ccccc1".to_string()),
status: AssessmentStatus::RouteSupportedWithValidationGaps,
selected_route: Some(route.clone()),
route_assessments: vec![route],
routes_supplied_count: 1,
routes_assessed_count: 1,
route_assessments_returned_count: 1,
route_assessments_truncated: false,
provenance: AssessmentProvenance {
renkin_version: "0.0.0-test".to_string(),
assessment_schema_version: SYNTHESIZABILITY_SCHEMA_VERSION,
canonical_target: "c1ccccc1".to_string(),
rules_count: 1,
rules_hash: "sha256:rules".to_string(),
stock_count: 1,
stock_hash: "sha256:stock".to_string(),
stock_input_status: StockInputStatus::FullyUsable,
stock_source: None,
template_metadata_hash: None,
search_config_summary: None,
assessment_config_hash: "sha256:config".to_string(),
git_commit: None,
embedded_fallback_used: None,
reproducibility_hash: "sha256:repro".to_string(),
reproducibility_exclusions: vec!["wall_clock_timestamp".to_string()],
},
config_used: SynthesizabilityConfigSummary {
require_verified_stock_terminal: true,
require_target_element_accounting: true,
forward_validation_policy: ForwardValidationPolicy::Ignore,
evidence_policy: EvidencePolicy::Ignore,
reagent_omission_template_allowlist: vec![
"rule:boc_deprotection_retro".to_string(),
],
accounting_failure_policy: AccountingFailurePolicy::HardFailure,
max_route_diagnostics: 10,
include_all_route_diagnostics: false,
},
warnings: vec![],
};
let json = serde_json::to_string(&assessment).expect("must serialize without panicking");
assert!(json.contains("\"route_supported_with_validation_gaps\""));
assert!(json.contains("\"stock_source\":null"));
assert!(json.contains("\"git_commit\":null"));
}
}