///|
/// Severity used by the independent result auditor.
pub(all) enum AuditSeverity {
AuditInfo
AuditWarning
AuditError
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn AuditSeverity::label(self : AuditSeverity) -> String {
match self {
AuditInfo => "info"
AuditWarning => "warning"
AuditError => "error"
}
}
///|
/// A stable, machine-readable audit result. Codes are intentionally kept
/// separate from prose so downstream applications do not parse messages.
pub(all) struct AuditFinding {
code : String
severity : AuditSeverity
subject : String
message : String
slot : Int?
expected : Int?
actual : Int?
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn AuditFinding::new(
code : String,
severity : AuditSeverity,
subject : String,
message : String,
slot? : Int,
expected? : Int,
actual? : Int,
) -> AuditFinding {
{ code, severity, subject, message, slot, expected, actual }
}
///|
/// Per-slot reconstruction of supply, demand, storage, and residual energy.
pub(all) struct EnergyBalanceRow {
slot : Int
demand_w : Int
served_demand_w : Int
grid_w : Int
solar_w : Int
battery_charge_w : Int
battery_discharge_w : Int
unserved_w : Int
supply_w : Int
accounted_use_w : Int
residual_w : Int
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn EnergyBalanceRow::is_balanced(
self : EnergyBalanceRow,
tolerance_w? : Int = 2,
) -> Bool {
analytics_abs(self.residual_w) <= tolerance_w
}
///|
pub(all) struct AuditSummary {
passed : Bool
error_count : Int
warning_count : Int
info_count : Int
maximum_balance_residual_w : Int
maximum_grid_excess_w : Int
minimum_battery_state_wh : Int
maximum_battery_state_wh : Int
findings : Array[AuditFinding]
balances : Array[EnergyBalanceRow]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn AuditSummary::has_code(self : AuditSummary, code : String) -> Bool {
for finding in self.findings {
if finding.code == code {
return true
}
}
false
}
///|
pub fn AuditSummary::to_json_string(self : AuditSummary) -> String {
self.to_json().stringify(indent=2)
}
///|
/// A compact comparison between two valid optimizer outputs.
pub(all) struct PlanDelta {
baseline_title : String
candidate_title : String
cost_delta_micro : Int
carbon_delta_g : Int
peak_delta_w : Int
unserved_delta_wh : Int
critical_unserved_delta_wh : Int
resilience_delta_permille : Int
comfort_delta : Int
completed_task_delta : Int
candidate_dominates : Bool
baseline_dominates : Bool
tradeoff_count : Int
summary : Array[String]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn PlanDelta::to_json_string(self : PlanDelta) -> String {
self.to_json().stringify(indent=2)
}
///|
/// Capacity and outage adequacy indicators computed before optimization.
pub(all) struct AdequacyProfile {
total_demand_wh : Int
total_solar_wh : Int
usable_battery_wh : Int
outage_demand_wh : Int
outage_solar_wh : Int
outage_storage_margin_wh : Int
peak_requested_w : Int
weakest_supply_margin_w : Int
weakest_supply_slot : Int
critical_task_energy_wh : Int
flexible_task_energy_wh : Int
risk_codes : Array[String]
} derive(Debug, Eq, ToJson, FromJson)
///|
pub fn AdequacyProfile::to_json_string(self : AdequacyProfile) -> String {
self.to_json().stringify(indent=2)
}
///|
fn analytics_abs(value : Int) -> Int {
if value < 0 {
-value
} else {
value
}
}
///|
fn analytics_min(a : Int, b : Int) -> Int {
if a < b {
a
} else {
b
}
}
///|
fn analytics_max(a : Int, b : Int) -> Int {
if a > b {
a
} else {
b
}
}
///|
fn array_value(values : Array[Int], index : Int) -> Int {
if index >= 0 && index < values.length() {
values[index]
} else {
0
}
}
///|
fn battery_power_at(result : PlanResult, slot : Int) -> Int {
for step in result.battery_steps {
if step.slot == slot {
return step.power_w
}
}
0
}
///|
fn find_task(input : PlanningInput, task_id : String) -> LoadTask? {
for task in input.tasks {
if task.id == task_id {
return Some(task)
}
}
None
}
///|
fn append_length_finding(
findings : Array[AuditFinding],
name : String,
actual : Int,
expected : Int,
) -> Unit {
if actual != expected {
findings.push(
AuditFinding::new(
"AUDIT-SHAPE-001",
AuditError,
name,
"Result series length does not match the planning horizon",
expected~,
actual~,
),
)
}
}
///|
fn audit_series_shapes(
result : PlanResult,
findings : Array[AuditFinding],
) -> Unit {
append_length_finding(
findings,
"load_w",
result.load_w.length(),
result.horizon_slots,
)
append_length_finding(
findings,
"grid_w",
result.grid_w.length(),
result.horizon_slots,
)
append_length_finding(
findings,
"solar_used_w",
result.solar_used_w.length(),
result.horizon_slots,
)
append_length_finding(
findings,
"unserved_w",
result.unserved_w.length(),
result.horizon_slots,
)
let expected_battery_states = result.horizon_slots + 1
append_length_finding(
findings,
"battery_state_wh",
result.battery_state_wh.length(),
expected_battery_states,
)
}
///|
pub fn reconstruct_energy_balance(
result : PlanResult,
) -> Array[EnergyBalanceRow] {
let rows : Array[EnergyBalanceRow] = []
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let demand = array_value(result.load_w, slot)
let unserved = array_value(result.unserved_w, slot)
let grid = array_value(result.grid_w, slot)
let solar = array_value(result.solar_used_w, slot)
let battery_power = battery_power_at(result, slot)
let charge = analytics_max(0, battery_power)
let discharge = analytics_max(0, -battery_power)
let served = analytics_max(0, demand - unserved)
let supply = grid + solar + discharge
let accounted_use = served + charge
rows.push({
slot,
demand_w: demand,
served_demand_w: served,
grid_w: grid,
solar_w: solar,
battery_charge_w: charge,
battery_discharge_w: discharge,
unserved_w: unserved,
supply_w: supply,
accounted_use_w: accounted_use,
residual_w: supply - accounted_use,
})
}
rows
}
///|
fn audit_nonnegative_series(
result : PlanResult,
findings : Array[AuditFinding],
) -> Unit {
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let values = [
("load_w", array_value(result.load_w, slot)),
("grid_w", array_value(result.grid_w, slot)),
("solar_used_w", array_value(result.solar_used_w, slot)),
("unserved_w", array_value(result.unserved_w, slot)),
]
for pair in values {
let (name, value) = pair
if value < 0 {
findings.push(
AuditFinding::new(
"AUDIT-RANGE-001",
AuditError,
name,
"Power series contains a negative value",
slot~,
expected=0,
actual=value,
),
)
}
}
let unserved = array_value(result.unserved_w, slot)
let load = array_value(result.load_w, slot)
if unserved > load {
findings.push(
AuditFinding::new(
"AUDIT-RANGE-002",
AuditError,
"unserved_w",
"Unserved power is greater than requested load",
slot~,
expected=load,
actual=unserved,
),
)
}
}
}
///|
fn audit_grid_constraints(
input : PlanningInput,
result : PlanResult,
findings : Array[AuditFinding],
) -> Int {
let mut maximum_excess = 0
for slot = 0; slot < result.horizon_slots; slot = slot + 1 {
let grid = array_value(result.grid_w, slot)
let limit = input.grid_limit_at(slot)
let excess = analytics_max(0, grid - limit)
maximum_excess = analytics_max(maximum_excess, excess)
if excess > 0 {
findings.push(
AuditFinding::new(
"AUDIT-GRID-001",
AuditError,
"grid_w",
"Grid import exceeds the effective grid or outage limit",
slot~,
expected=limit,
actual=grid,
),
)
}
if input.has_outage_at(slot) && grid != 0 {
findings.push(
AuditFinding::new(
"AUDIT-OUTAGE-001",
AuditError,
"grid_w",
"Grid power must be zero during a full outage",
slot~,
expected=0,
actual=grid,
),
)
}
}
maximum_excess
}
///|
fn audit_battery_constraints(
input : PlanningInput,
result : PlanResult,
findings : Array[AuditFinding],
) -> (Int, Int) {
let mut minimum_state = 0
let mut maximum_state = 0
match input.battery {
None =>
for step in result.battery_steps {
if step.power_w != 0 {
findings.push(
AuditFinding::new(
"AUDIT-BATTERY-001",
AuditError,
"battery_steps",
"Plan dispatches storage although the input has no battery",
slot=step.slot,
expected=0,
actual=step.power_w,
),
)
}
}
Some(spec) => {
minimum_state = spec.maximum_wh
maximum_state = spec.minimum_wh
for index = 0; index < result.battery_state_wh.length(); index = index + 1 {
let state = result.battery_state_wh[index]
minimum_state = analytics_min(minimum_state, state)
maximum_state = analytics_max(maximum_state, state)
if state < spec.minimum_wh || state > spec.maximum_wh {
findings.push(
AuditFinding::new(
"AUDIT-BATTERY-002",
AuditError,
"battery_state_wh",
"Battery state is outside its configured safety interval",
slot=analytics_min(index, result.horizon_slots - 1),
expected=spec.maximum_wh,
actual=state,
),
)
}
}
for step in result.battery_steps {
if step.power_w > spec.maximum_charge_w {
findings.push(
AuditFinding::new(
"AUDIT-BATTERY-003",
AuditError,
"battery_steps",
"Charge power exceeds the battery limit",
slot=step.slot,
expected=spec.maximum_charge_w,
actual=step.power_w,
),
)
}
if -step.power_w > spec.maximum_discharge_w {
findings.push(
AuditFinding::new(
"AUDIT-BATTERY-004",
AuditError,
"battery_steps",
"Discharge power exceeds the battery limit",
slot=step.slot,
expected=spec.maximum_discharge_w,
actual=-step.power_w,
),
)
}
}
}
}
(minimum_state, maximum_state)
}
///|
fn audit_schedule(
input : PlanningInput,
result : PlanResult,
findings : Array[AuditFinding],
) -> Unit {
for entry in result.schedule {
match find_task(input, entry.task_id) {
None =>
findings.push(
AuditFinding::new(
"AUDIT-TASK-001",
AuditError,
entry.task_id,
"Schedule references a task that is not present in the input",
),
)
Some(task) => {
if entry.start_slot < task.earliest_start ||
entry.end_slot > task.latest_end ||
entry.start_slot >= entry.end_slot {
findings.push(
AuditFinding::new(
"AUDIT-TASK-002",
AuditError,
entry.task_id,
"Scheduled interval is outside the task window",
slot=entry.start_slot,
),
)
}
if entry.power_w != task.power_w {
findings.push(
AuditFinding::new(
"AUDIT-TASK-003",
AuditError,
entry.task_id,
"Scheduled power differs from the task contract",
slot=entry.start_slot,
expected=task.power_w,
actual=entry.power_w,
),
)
}
}
}
}
}
///|
fn audit_balances(
balances : Array[EnergyBalanceRow],
findings : Array[AuditFinding],
tolerance_w : Int,
) -> Int {
let mut maximum_residual = 0
for row in balances {
maximum_residual = analytics_max(
maximum_residual,
analytics_abs(row.residual_w),
)
if !row.is_balanced(tolerance_w~) {
findings.push(
AuditFinding::new(
"AUDIT-BALANCE-001",
AuditWarning,
"energy-balance",
"Supply and accounted use differ; the residual can represent export or rounding",
slot=row.slot,
expected=row.accounted_use_w,
actual=row.supply_w,
),
)
}
}
maximum_residual
}
///|
/// Independently audit a plan against its original input. The auditor does not
/// call the optimizer and therefore also catches regression defects in solve().
pub fn audit_plan(
input : PlanningInput,
result : PlanResult,
balance_tolerance_w? : Int = 2,
) -> AuditSummary {
let findings : Array[AuditFinding] = []
audit_series_shapes(result, findings)
audit_nonnegative_series(result, findings)
let maximum_grid_excess = audit_grid_constraints(input, result, findings)
let (minimum_state, maximum_state) = audit_battery_constraints(
input, result, findings,
)
audit_schedule(input, result, findings)
let balances = reconstruct_energy_balance(result)
let maximum_residual = audit_balances(balances, findings, balance_tolerance_w)
let mut errors = 0
let mut warnings = 0
let mut info = 0
for finding in findings {
match finding.severity {
AuditError => errors = errors + 1
AuditWarning => warnings = warnings + 1
AuditInfo => info = info + 1
}
}
{
passed: errors == 0,
error_count: errors,
warning_count: warnings,
info_count: info,
maximum_balance_residual_w: maximum_residual,
maximum_grid_excess_w: maximum_grid_excess,
minimum_battery_state_wh: minimum_state,
maximum_battery_state_wh: maximum_state,
findings,
balances,
}
}
///|
fn improvement_text(name : String, delta : Int, unit : String) -> String {
if delta < 0 {
name + " improves by " + (-delta).to_string() + " " + unit
} else if delta > 0 {
name + " worsens by " + delta.to_string() + " " + unit
} else {
name + " is unchanged"
}
}
///|
fn no_worse_for_minimize(delta : Int) -> Bool {
delta <= 0
}
///|
fn no_worse_for_maximize(delta : Int) -> Bool {
delta >= 0
}
///|
/// Compare plans using decision-facing deltas rather than their weighted score,
/// because scores from different objective policies are not directly comparable.
pub fn compare_plans(
baseline : PlanResult,
candidate : PlanResult,
) -> PlanDelta {
let cost_delta = candidate.metrics.net_cost_micro() -
baseline.metrics.net_cost_micro()
let carbon_delta = candidate.metrics.carbon_g - baseline.metrics.carbon_g
let peak_delta = candidate.metrics.peak_grid_w - baseline.metrics.peak_grid_w
let unserved_delta = candidate.metrics.unserved_energy_wh -
baseline.metrics.unserved_energy_wh
let critical_delta = candidate.metrics.critical_unserved_wh -
baseline.metrics.critical_unserved_wh
let resilience_delta = candidate.metrics.resilience_permille -
baseline.metrics.resilience_permille
let comfort_delta = candidate.metrics.comfort_penalty -
baseline.metrics.comfort_penalty
let completed_delta = candidate.metrics.completed_tasks -
baseline.metrics.completed_tasks
let candidate_no_worse = no_worse_for_minimize(cost_delta) &&
no_worse_for_minimize(carbon_delta) &&
no_worse_for_minimize(peak_delta) &&
no_worse_for_minimize(unserved_delta) &&
no_worse_for_minimize(critical_delta) &&
no_worse_for_minimize(comfort_delta) &&
no_worse_for_maximize(resilience_delta) &&
no_worse_for_maximize(completed_delta)
let baseline_no_worse = cost_delta >= 0 &&
carbon_delta >= 0 &&
peak_delta >= 0 &&
unserved_delta >= 0 &&
critical_delta >= 0 &&
comfort_delta >= 0 &&
resilience_delta <= 0 &&
completed_delta <= 0
let candidate_strict = cost_delta < 0 ||
carbon_delta < 0 ||
peak_delta < 0 ||
unserved_delta < 0 ||
critical_delta < 0 ||
comfort_delta < 0 ||
resilience_delta > 0 ||
completed_delta > 0
let baseline_strict = cost_delta > 0 ||
carbon_delta > 0 ||
peak_delta > 0 ||
unserved_delta > 0 ||
critical_delta > 0 ||
comfort_delta > 0 ||
resilience_delta < 0 ||
completed_delta < 0
let summary = [
improvement_text("net cost", cost_delta, "micro"),
improvement_text("carbon", carbon_delta, "g"),
improvement_text("peak grid power", peak_delta, "W"),
improvement_text("unserved energy", unserved_delta, "Wh"),
improvement_text("critical unserved energy", critical_delta, "Wh"),
]
let mut improvements = 0
let mut regressions = 0
for
delta in [
cost_delta, carbon_delta, peak_delta, unserved_delta, critical_delta, comfort_delta,
] {
if delta < 0 {
improvements = improvements + 1
} else if delta > 0 {
regressions = regressions + 1
}
}
if resilience_delta > 0 {
improvements = improvements + 1
}
if resilience_delta < 0 {
regressions = regressions + 1
}
if completed_delta > 0 {
improvements = improvements + 1
}
if completed_delta < 0 {
regressions = regressions + 1
}
{
baseline_title: baseline.title,
candidate_title: candidate.title,
cost_delta_micro: cost_delta,
carbon_delta_g: carbon_delta,
peak_delta_w: peak_delta,
unserved_delta_wh: unserved_delta,
critical_unserved_delta_wh: critical_delta,
resilience_delta_permille: resilience_delta,
comfort_delta,
completed_task_delta: completed_delta,
candidate_dominates: candidate_no_worse && candidate_strict,
baseline_dominates: baseline_no_worse && baseline_strict,
tradeoff_count: analytics_min(improvements, regressions),
summary,
}
}
///|
fn task_overlaps_outage(input : PlanningInput, task : LoadTask) -> Bool {
for outage in input.outages {
if task.earliest_start < outage.end_slot &&
task.latest_end > outage.start_slot {
return true
}
}
false
}
///|
/// Estimate resource adequacy before scheduling. This deliberately reports a
/// conservative envelope rather than claiming that aggregate energy guarantees
/// a feasible schedule.
pub fn assess_adequacy(input : PlanningInput) -> AdequacyProfile {
let mut critical_energy = 0
let mut flexible_energy = 0
let mut peak_requested = 0
for task in input.tasks {
let energy = task.energy_wh(input.slot_minutes)
if task.priority == Critical {
critical_energy = critical_energy + energy
}
if task.mode.is_flexible() {
flexible_energy = flexible_energy + energy
}
peak_requested = peak_requested + task.power_w
}
peak_requested = peak_requested + input.base_load_w.maximum()
let mut outage_demand = 0
let mut outage_solar = 0
for slot = 0; slot < input.horizon_slots; slot = slot + 1 {
if input.has_outage_at(slot) {
outage_demand = outage_demand +
input.base_load_w.at(slot) * input.slot_minutes / 60
outage_solar = outage_solar +
input.solar_w.at(slot) * input.slot_minutes / 60
for task in input.tasks {
if task.priority == Critical && task_overlaps_outage(input, task) {
outage_demand = outage_demand + task.power_w * input.slot_minutes / 60
}
}
}
}
let usable_battery = match input.battery {
None => 0
Some(battery) => analytics_max(0, battery.initial_wh - battery.reserve_wh)
}
let outage_margin = outage_solar + usable_battery - outage_demand
let mut weakest_margin = 2147483647
let mut weakest_slot = 0
for slot = 0; slot < input.horizon_slots; slot = slot + 1 {
let battery_support = match input.battery {
Some(battery) => battery.maximum_discharge_w
None => 0
}
let supply = input.grid_limit_at(slot) +
input.solar_w.at(slot) +
battery_support
let demand = input.base_load_w.at(slot)
let margin = supply - demand
if margin < weakest_margin {
weakest_margin = margin
weakest_slot = slot
}
}
if input.horizon_slots == 0 {
weakest_margin = 0
}
let risks : Array[String] = []
if outage_margin < 0 {
risks.push("ADEQUACY-OUTAGE-ENERGY")
}
if weakest_margin < 0 {
risks.push("ADEQUACY-SLOT-POWER")
}
if input.total_solar_energy_wh() >
input.total_task_energy_wh() +
input.base_load_w.sum() * input.slot_minutes / 60 {
risks.push("ADEQUACY-SOLAR-SURPLUS")
}
if flexible_energy == 0 {
risks.push("ADEQUACY-NO-FLEXIBILITY")
}
{
total_demand_wh: input.total_task_energy_wh() +
input.base_load_w.sum() * input.slot_minutes / 60,
total_solar_wh: input.total_solar_energy_wh(),
usable_battery_wh: usable_battery,
outage_demand_wh: outage_demand,
outage_solar_wh: outage_solar,
outage_storage_margin_wh: outage_margin,
peak_requested_w: peak_requested,
weakest_supply_margin_w: weakest_margin,
weakest_supply_slot: weakest_slot,
critical_task_energy_wh: critical_energy,
flexible_task_energy_wh: flexible_energy,
risk_codes: risks,
}
}
///|
pub fn audit_markdown(summary : AuditSummary) -> String {
let buffer = StringBuilder()
let status = if summary.passed { "PASS" } else { "FAIL" }
buffer <+ "# Plan audit\n\n"
buffer <+ "- Status: **\{status}**\n"
buffer <+ "- Errors: \{summary.error_count}\n"
buffer <+ "- Warnings: \{summary.warning_count}\n"
buffer <+ "- Maximum grid excess: \{summary.maximum_grid_excess_w} W\n"
buffer <+
"- Maximum balance residual: \{summary.maximum_balance_residual_w} W\n\n"
buffer <+ "| Severity | Code | Subject | Slot | Message |\n"
buffer <+ "|---|---|---|---:|---|\n"
for finding in summary.findings {
let slot = match finding.slot {
Some(value) => value.to_string()
None => "-"
}
buffer <+
"| \{finding.severity.label()} | \{finding.code} | \{finding.subject} | \{slot} | \{finding.message} |\n"
}
buffer.to_string()
}