use std::collections::BTreeMap;
use std::path::Path;
use chrono::NaiveDate;
use super::super::error::OutputError;
use super::codec::{
deserialize_stage_basis, deserialize_stage_cuts, deserialize_stage_states,
read_sorted_bin_files, serialize_stage_basis, serialize_stage_cuts, serialize_stage_states,
};
use super::records::{
ENTITY_SLOT_DELIVERY_DATE_SENTINEL, FORMAT_VERSION, OwnedPolicyBasisRecord, PolicyBasisRecord,
PolicyCheckpoint, PolicyCheckpointMetadata, StageCutsPayload, StageCutsReadResult,
StageStatesPayload, StageStatesReadResult,
};
const ENTITY_TYPE_HYDRO_TRANSIT_BUCKET: u8 = 3;
fn is_well_formed_delivery_date(delivery_date: i32) -> bool {
if delivery_date == ENTITY_SLOT_DELIVERY_DATE_SENTINEL {
return true;
}
let year = delivery_date / 10_000;
let month = (delivery_date / 100) % 100;
let day = delivery_date % 100;
let (Ok(month), Ok(day)) = (u32::try_from(month), u32::try_from(day)) else {
return false;
};
NaiveDate::from_ymd_opt(year, month, day).is_some()
}
fn check_transit_bucket_monotonicity(pool: &StageCutsReadResult) -> Result<(), OutputError> {
let mut by_entity: BTreeMap<i32, Vec<(u32, i32)>> = BTreeMap::new();
for slot in &pool.entity_manifest {
if slot.entity_type == ENTITY_TYPE_HYDRO_TRANSIT_BUCKET
&& slot.delivery_date != ENTITY_SLOT_DELIVERY_DATE_SENTINEL
{
by_entity
.entry(slot.entity_id)
.or_default()
.push((slot.subindex, slot.delivery_date));
}
}
for dates in by_entity.values_mut() {
dates.sort_by_key(|&(subindex, _)| subindex);
for pair in dates.windows(2) {
let (prev_subindex, prev_date) = pair[0];
let (subindex, date) = pair[1];
if date < prev_date {
let pool_id = pool.stage_id;
return Err(OutputError::serialization(
"policy_checkpoint_dates",
format!(
"pool {pool_id} subindex {subindex} carries delivery_date {date}, \
earlier than subindex {prev_subindex}'s {prev_date}"
),
));
}
}
}
Ok(())
}
fn validate_checkpoint_dates(checkpoint: &PolicyCheckpoint) -> Result<(), OutputError> {
for pool in &checkpoint.stage_cuts {
for slot in &pool.entity_manifest {
if !is_well_formed_delivery_date(slot.delivery_date) {
return Err(OutputError::serialization(
"policy_checkpoint_dates",
format!(
"pool {} subindex {} carries malformed delivery_date {}",
pool.stage_id, slot.subindex, slot.delivery_date
),
));
}
}
check_transit_bucket_monotonicity(pool)?;
}
Ok(())
}
fn bin_file_name(id: u32) -> String {
format!("{id:03}.bin")
}
pub fn write_policy_checkpoint(
path: &Path,
stage_cuts: &[StageCutsPayload<'_>],
stage_bases: &[PolicyBasisRecord<'_>],
metadata: &PolicyCheckpointMetadata,
stage_states: &[StageStatesPayload<'_>],
) -> Result<(), OutputError> {
let cuts_dir = path.join("cuts");
std::fs::create_dir_all(&cuts_dir).map_err(|e| OutputError::io(&cuts_dir, e))?;
let basis_dir = path.join("basis");
std::fs::create_dir_all(&basis_dir).map_err(|e| OutputError::io(&basis_dir, e))?;
for payload in stage_cuts {
let file_path = cuts_dir.join(bin_file_name(payload.stage_id));
let buf = serialize_stage_cuts(
payload.stage_id,
payload.state_dimension,
payload.capacity,
payload.warm_start_count,
payload.cuts,
payload.active_cut_indices,
payload.populated_count,
payload.entity_manifest,
);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
for record in stage_bases {
let file_path = basis_dir.join(bin_file_name(record.stage_id));
let buf = serialize_stage_basis(record);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
if !stage_states.is_empty() {
let states_dir = path.join("states");
std::fs::create_dir_all(&states_dir).map_err(|e| OutputError::io(&states_dir, e))?;
for payload in stage_states {
let file_path = states_dir.join(bin_file_name(payload.stage_id));
let buf = serialize_stage_states(payload);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
}
let json = serde_json::to_string_pretty(metadata)
.map_err(|e| OutputError::serialization("policy_metadata", e.to_string()))?;
let meta_path = path.join("metadata.json");
std::fs::write(&meta_path, json.as_bytes()).map_err(|e| OutputError::io(&meta_path, e))?;
Ok(())
}
pub fn read_policy_checkpoint(path: &Path) -> Result<PolicyCheckpoint, OutputError> {
#[derive(serde::Deserialize)]
struct FormatVersionProbe {
#[serde(default)]
format_version: u32,
}
let meta_path = path.join("metadata.json");
let meta_bytes = std::fs::read(&meta_path).map_err(|e| OutputError::io(&meta_path, e))?;
let probe: FormatVersionProbe = serde_json::from_slice(&meta_bytes)
.map_err(|e| OutputError::serialization("policy_metadata", e.to_string()))?;
if probe.format_version != FORMAT_VERSION {
return Err(OutputError::serialization(
"policy_metadata",
format!(
"unsupported value-function artifact format_version: expected {FORMAT_VERSION}, \
found {} (a pre-marker or other-version artifact is not readable by this build)",
probe.format_version
),
));
}
let metadata: PolicyCheckpointMetadata = serde_json::from_slice(&meta_bytes)
.map_err(|e| OutputError::serialization("policy_metadata", e.to_string()))?;
let cuts_dir = path.join("cuts");
let mut stage_cuts: Vec<StageCutsReadResult> =
read_sorted_bin_files(&cuts_dir, "stage_cuts", deserialize_stage_cuts)?;
stage_cuts.sort_by_key(|r| r.stage_id);
let basis_dir = path.join("basis");
let mut stage_bases: Vec<OwnedPolicyBasisRecord> =
read_sorted_bin_files(&basis_dir, "stage_basis", deserialize_stage_basis)?;
stage_bases.sort_by_key(|r| r.stage_id);
let states_dir = path.join("states");
let stage_states: Vec<StageStatesReadResult> = if states_dir.is_dir() {
let mut ss = read_sorted_bin_files(&states_dir, "stage_states", deserialize_stage_states)?;
ss.sort_by_key(|r| r.stage_id);
ss
} else {
Vec::new()
};
let checkpoint = PolicyCheckpoint {
metadata,
stage_cuts,
stage_bases,
stage_states,
};
validate_checkpoint_dates(&checkpoint)?;
Ok(checkpoint)
}