Skip to content

Commit 915b64a

Browse files
committed
chore: cleaning documentation and adding filter to spec
1 parent 316e590 commit 915b64a

16 files changed

Lines changed: 252 additions & 64 deletions

File tree

crates/arco-cli/src/benchmark.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ fn to_e2e_summary(
247247
reports: execution_result
248248
.reports
249249
.iter()
250-
.map(|report| report.dsl_name.clone())
250+
.map(|report| report.name.clone())
251251
.collect(),
252252
}
253253
}
@@ -298,6 +298,7 @@ mod tests {
298298
expression: Expr::Number("0".to_string()),
299299
},
300300
active_reports: Vec::new(),
301+
active_variable_reports: Vec::new(),
301302
active_dual_reports: Vec::new(),
302303
}
303304
}

crates/arco-cli/src/driver.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use arco_kdl::ObjectiveSense;
1010
use arco_kdl::pipeline::{PipelineError, compile_file, validate_file};
1111
use miette::Diagnostic;
1212
use serde::Serialize;
13+
use std::collections::BTreeMap;
1314
use std::fmt::Display;
1415
use std::path::{Path, PathBuf};
1516
use std::time::Instant;
@@ -37,6 +38,7 @@ pub struct RunSummary {
3738
pub reports: Vec<ReportSummary>,
3839
#[serde(skip_serializing_if = "Vec::is_empty")]
3940
pub dual_reports: Vec<DualReportSummary>,
41+
#[serde(skip_serializing_if = "Vec::is_empty")]
4042
pub variables: Vec<VariableSummary>,
4143
pub counts: ProblemCounts,
4244
pub timing: TimingSummary,
@@ -52,7 +54,9 @@ pub struct ObjectiveSummary {
5254
#[derive(Debug, Serialize, PartialEq)]
5355
pub struct ReportSummary {
5456
pub name: String,
55-
pub value: f64,
57+
#[serde(skip_serializing_if = "Vec::is_empty")]
58+
pub index: Vec<String>,
59+
pub values: Vec<BTreeMap<String, serde_json::Value>>,
5660
}
5761

5862
#[derive(Debug, Serialize, PartialEq)]
@@ -234,9 +238,10 @@ pub fn run_file_with_options_and_backend(
234238
reports: execution_result
235239
.reports
236240
.into_iter()
237-
.map(|report| ReportSummary {
238-
name: report.dsl_name,
239-
value: report.value,
241+
.map(|r| ReportSummary {
242+
name: r.name,
243+
index: r.index,
244+
values: r.values,
240245
})
241246
.collect(),
242247
dual_reports: execution_result
@@ -340,6 +345,9 @@ fn summarize_variables(
340345
variables: &[crate::execution::MappedVariableResult],
341346
options: &RunOptions,
342347
) -> Vec<VariableSummary> {
348+
if options.filter_variable.is_none() {
349+
return Vec::new();
350+
}
343351
variables
344352
.iter()
345353
.filter(|variable| {

crates/arco-cli/src/execution.rs

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,18 @@ pub struct ExecutionResult {
6666
pub status: SolveStatus,
6767
pub objective_sense: ObjectiveSense,
6868
pub objective: MappedScalarResult,
69-
pub reports: Vec<MappedScalarResult>,
69+
pub reports: Vec<ReportResult>,
7070
pub variables: Vec<MappedVariableResult>,
7171
pub dual_reports: Vec<DualReportResult>,
7272
}
7373

74+
#[derive(Debug, Clone, PartialEq)]
75+
pub struct ReportResult {
76+
pub name: String,
77+
pub index: Vec<String>,
78+
pub values: Vec<BTreeMap<String, serde_json::Value>>,
79+
}
80+
7481
#[derive(Debug, Clone, PartialEq)]
7582
pub struct MappedScalarResult {
7683
pub dsl_name: String,
@@ -603,23 +610,59 @@ pub fn execute_problem_with_options(
603610
.map(|variable| (variable.compiled_name.clone(), variable))
604611
.collect::<BTreeMap<_, _>>();
605612

606-
let reports = problem
607-
.reports
608-
.iter()
609-
.map(|report| {
610-
let value = report_values.get(&report.name).copied().ok_or_else(|| {
611-
ExecutionError::MissingReportValue {
612-
backend: backend.to_string(),
613-
compiled_name: report.name.clone(),
614-
}
615-
})?;
616-
Ok(MappedScalarResult {
617-
dsl_name: report.name.clone(),
613+
// Build unified reports: expression reports (scalar) and variable reports
614+
let mut reports = Vec::new();
615+
616+
for report in &problem.reports {
617+
let value = report_values.get(&report.name).copied().ok_or_else(|| {
618+
ExecutionError::MissingReportValue {
619+
backend: backend.to_string(),
618620
compiled_name: report.name.clone(),
619-
value,
620-
})
621-
})
622-
.collect::<Result<Vec<_>, _>>()?;
621+
}
622+
})?;
623+
let mut record = BTreeMap::new();
624+
record.insert("value".to_string(), serde_json::Value::from(value));
625+
reports.push(ReportResult {
626+
name: report.name.clone(),
627+
index: Vec::new(),
628+
values: vec![record],
629+
});
630+
}
631+
632+
for vr in &problem.variable_reports {
633+
let family_key = format!("{}[{}]", vr.control_name, vr.indices.join(","));
634+
if let Some(family) = variable_values.get(&family_key) {
635+
let values = family
636+
.values
637+
.iter()
638+
.filter_map(|v| {
639+
let (raw, typed) = extract_index_parts(&vr.control_name, &v.compiled_name);
640+
if let Some(ref filter) = vr.filter {
641+
let bindings: BTreeMap<&str, &str> = vr
642+
.indices
643+
.iter()
644+
.zip(raw.iter())
645+
.map(|(k, v)| (k.as_str(), v.as_str()))
646+
.collect();
647+
if !eval_filter(filter, &bindings) {
648+
return None;
649+
}
650+
}
651+
let mut record = BTreeMap::new();
652+
for (idx_name, idx_val) in vr.indices.iter().zip(typed) {
653+
record.insert(idx_name.clone(), idx_val);
654+
}
655+
record.insert("value".to_string(), serde_json::Value::from(v.value));
656+
Some(record)
657+
})
658+
.collect();
659+
reports.push(ReportResult {
660+
name: vr.control_name.clone(),
661+
index: vr.indices.clone(),
662+
values,
663+
});
664+
}
665+
}
623666

624667
let variables = problem
625668
.variables
@@ -900,6 +943,77 @@ fn map_solver_status(status: ArcoSolverStatus) -> SolveStatus {
900943
}
901944
}
902945

946+
/// Split a variable instance name into raw string parts and typed JSON values.
947+
/// E.g. `("pc", "pc[1,Li-Ion]")` → `(["1", "Li-Ion"], [Number(1), String("Li-Ion")])`.
948+
fn extract_index_parts(
949+
family_name: &str,
950+
instance_name: &str,
951+
) -> (Vec<String>, Vec<serde_json::Value>) {
952+
let inner = instance_name
953+
.strip_prefix(family_name)
954+
.and_then(|s| s.strip_prefix('['))
955+
.and_then(|s| s.strip_suffix(']'))
956+
.unwrap_or("");
957+
if inner.is_empty() {
958+
return (Vec::new(), Vec::new());
959+
}
960+
let parts: Vec<&str> = inner.split(',').collect();
961+
let strings = parts.iter().map(|s| s.to_string()).collect();
962+
let typed = parts
963+
.iter()
964+
.map(|part| {
965+
if let Ok(n) = part.parse::<i64>() {
966+
serde_json::Value::Number(n.into())
967+
} else if let Some(n) = part
968+
.parse::<f64>()
969+
.ok()
970+
.and_then(serde_json::Number::from_f64)
971+
{
972+
serde_json::Value::Number(n)
973+
} else {
974+
serde_json::Value::String(part.to_string())
975+
}
976+
})
977+
.collect();
978+
(strings, typed)
979+
}
980+
981+
/// Evaluate a simple filter expression against index bindings.
982+
/// Evaluate a comparison filter against index bindings. Unsupported expression
983+
/// types fail closed (exclude the row) to avoid silent pass-through.
984+
fn eval_filter(expr: &arco_kdl::algebra::Expr, bindings: &BTreeMap<&str, &str>) -> bool {
985+
use arco_kdl::algebra::{ComparisonOp, Expr};
986+
match expr {
987+
Expr::Comparison { op, left, right } => {
988+
let lhs = match left.as_ref() {
989+
Expr::Identifier(name) => bindings.get(name.as_str()).copied(),
990+
Expr::String(s) => Some(s.as_str()),
991+
Expr::Number(n) => Some(n.as_str()),
992+
_ => None,
993+
};
994+
let rhs = match right.as_ref() {
995+
Expr::Identifier(name) => bindings.get(name.as_str()).copied(),
996+
Expr::String(s) => Some(s.as_str()),
997+
Expr::Number(n) => Some(n.as_str()),
998+
_ => None,
999+
};
1000+
match (lhs, rhs) {
1001+
(Some(l), Some(r)) => match op {
1002+
ComparisonOp::Equal | ComparisonOp::DoubleEqual => l == r,
1003+
ComparisonOp::NotEqual => l != r,
1004+
ComparisonOp::Less => l < r,
1005+
ComparisonOp::LessEqual => l <= r,
1006+
ComparisonOp::Greater => l > r,
1007+
ComparisonOp::GreaterEqual => l >= r,
1008+
},
1009+
_ => false,
1010+
}
1011+
}
1012+
// Unsupported filter expressions fail closed
1013+
_ => false,
1014+
}
1015+
}
1016+
9031017
#[cfg(test)]
9041018
mod tests {
9051019
use crate::execution::{MockArcoAdapter, execute_problem_with_options};
@@ -923,6 +1037,7 @@ mod tests {
9231037
expression: "0".to_string(),
9241038
},
9251039
reports: Vec::new(),
1040+
variable_reports: Vec::new(),
9261041
dual_reports: Vec::new(),
9271042
traceability: Vec::new(),
9281043
algebra: AlgebraicProblem {

crates/arco-kdl/src/compile/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct CompiledProblem {
2626
pub constraints: Vec<CompiledConstraint>,
2727
pub objective: CompiledObjective,
2828
pub reports: Vec<CompiledReport>,
29+
pub variable_reports: Vec<CompiledVariableReport>,
2930
pub dual_reports: Vec<CompiledDualReport>,
3031
pub traceability: Vec<TraceabilityRecord>,
3132
pub algebra: AlgebraicProblem,
@@ -63,6 +64,13 @@ pub struct CompiledReport {
6364
pub formula: String,
6465
}
6566

67+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68+
pub struct CompiledVariableReport {
69+
pub control_name: String,
70+
pub indices: Vec<String>,
71+
pub filter: Option<crate::algebra::Expr>,
72+
}
73+
6674
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6775
pub struct CompiledDualReport {
6876
pub constraint_name: String,
@@ -227,6 +235,16 @@ pub fn compile_program(
227235
.map(compile_report)
228236
.collect::<Vec<_>>();
229237

238+
let variable_reports = program
239+
.active_variable_reports
240+
.iter()
241+
.map(|vr| CompiledVariableReport {
242+
control_name: vr.control_name.clone(),
243+
indices: vr.indices.clone(),
244+
filter: vr.filter.clone(),
245+
})
246+
.collect::<Vec<_>>();
247+
230248
let dual_reports = program
231249
.active_dual_reports
232250
.iter()
@@ -258,6 +276,7 @@ pub fn compile_program(
258276
constraints,
259277
objective,
260278
reports,
279+
variable_reports,
261280
dual_reports,
262281
traceability,
263282
algebra,

crates/arco-kdl/src/semantic/resolution.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,17 @@ pub(crate) fn resolve_model_scenario_reports(
1212
scenario: &ScenarioDecl,
1313
active_constraints: &[ResolvedConstraint],
1414
entrypoint: &Path,
15-
) -> Result<(Vec<ResolvedReport>, Vec<ResolvedDualReport>), SemanticError> {
15+
) -> Result<
16+
(
17+
Vec<ResolvedReport>,
18+
Vec<ResolvedDualReport>,
19+
Vec<crate::semantic::types::ResolvedVariableReport>,
20+
),
21+
SemanticError,
22+
> {
1623
let mut reports = Vec::new();
1724
let mut dual_reports = Vec::new();
25+
let mut variable_reports = Vec::new();
1826

1927
for report_decl in &scenario.reports {
2028
match report_decl.kind {
@@ -28,19 +36,33 @@ pub(crate) fn resolve_model_scenario_reports(
2836
continue;
2937
}
3038

31-
let expression = model
39+
if let Some(expression) = model
3240
.expressions
3341
.iter()
3442
.find(|expression| expression.name == report_decl.target)
35-
.ok_or_else(|| SemanticError::MissingDeclaration {
36-
kind: "expression",
37-
name: report_decl.target.clone(),
38-
path: entrypoint.to_path_buf(),
39-
})?;
40-
reports.push(ResolvedReport {
41-
name: expression.name.clone(),
42-
formula_text: expression.formula.clone(),
43-
formula: expression.parsed_formula.clone(),
43+
{
44+
reports.push(ResolvedReport {
45+
name: expression.name.clone(),
46+
formula_text: expression.formula.clone(),
47+
formula: expression.parsed_formula.clone(),
48+
});
49+
continue;
50+
}
51+
52+
if let Some(control) = model.controls.iter().find(|c| c.name == report_decl.target)
53+
{
54+
variable_reports.push(crate::semantic::types::ResolvedVariableReport {
55+
control_name: control.name.clone(),
56+
indices: control.indices.iter().map(|i| i.name.clone()).collect(),
57+
filter: report_decl.parsed_filter_expression.clone(),
58+
});
59+
continue;
60+
}
61+
62+
return Err(SemanticError::MissingDeclaration {
63+
kind: "expression or control",
64+
name: report_decl.target.clone(),
65+
path: entrypoint.to_path_buf(),
4466
});
4567
}
4668
ReportKind::Dual => {
@@ -62,7 +84,7 @@ pub(crate) fn resolve_model_scenario_reports(
6284
}
6385
}
6486

65-
Ok((reports, dual_reports))
87+
Ok((reports, dual_reports, variable_reports))
6688
}
6789

6890
pub(crate) fn resolve_active_model_expressions(

crates/arco-kdl/src/semantic/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ pub struct SemanticProgram {
7070
pub active_expressions: Vec<ResolvedExpression>,
7171
pub active_objective: ResolvedObjective,
7272
pub active_reports: Vec<ResolvedReport>,
73+
pub active_variable_reports: Vec<ResolvedVariableReport>,
7374
pub active_dual_reports: Vec<ResolvedDualReport>,
7475
}
7576

@@ -137,6 +138,13 @@ pub struct ResolvedReport {
137138
pub formula: Expr,
138139
}
139140

141+
#[derive(Debug, Clone, PartialEq, Eq)]
142+
pub struct ResolvedVariableReport {
143+
pub control_name: String,
144+
pub indices: Vec<String>,
145+
pub filter: Option<Expr>,
146+
}
147+
140148
#[derive(Debug, Clone, PartialEq, Eq)]
141149
pub struct ResolvedDualReport {
142150
pub constraint_name: String,

0 commit comments

Comments
 (0)