z39_solver/domains/
schedule.rs1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ScheduleRequest {
10 pub tasks: Vec<TaskSpec>,
11 pub slot_start: i32,
13 pub slot_end: i32,
14 #[serde(default)]
15 pub constraints: Vec<ScheduleConstraint>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct TaskSpec {
20 pub name: String,
21 pub duration: i32,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(tag = "type")]
26pub enum ScheduleConstraint {
27 #[serde(rename = "before")]
28 Before { a: String, b: String },
29 #[serde(rename = "no_overlap")]
30 NoOverlap { tasks: Vec<String> },
31 #[serde(rename = "start_after")]
32 StartAfter { task: String, time: i32 },
33 #[serde(rename = "finish_before")]
34 FinishBefore { task: String, time: i32 },
35 #[serde(rename = "sequence")]
36 Sequence { tasks: Vec<String> },
37}
38
39pub fn encode_schedule(req: &ScheduleRequest) -> String {
40 let mut s = String::new();
41 s.push_str("(set-logic QF_LIA)\n");
42
43 for t in &req.tasks {
44 let v = sanitize_name(&t.name);
45 s.push_str(&format!("(declare-const {v}_start Int)\n"));
46 s.push_str(&format!("(declare-const {v}_end Int)\n"));
47 }
48
49 for t in &req.tasks {
50 let v = sanitize_name(&t.name);
51 s.push_str(&format!("(assert (>= {v}_start {}))\n", req.slot_start));
52 s.push_str(&format!("(assert (<= {v}_end {}))\n", req.slot_end));
53 s.push_str(&format!("(assert (= {v}_end (+ {v}_start {})))\n", t.duration));
54 }
55
56 for i in 0..req.tasks.len() {
58 for j in (i+1)..req.tasks.len() {
59 let a = sanitize_name(&req.tasks[i].name);
60 let b = sanitize_name(&req.tasks[j].name);
61 s.push_str(&format!("(assert (or (<= {a}_end {b}_start) (<= {b}_end {a}_start)))\n"));
62 }
63 }
64
65 for c in &req.constraints {
66 match c {
67 ScheduleConstraint::Before { a, b } => {
68 let av = sanitize_name(a);
69 let bv = sanitize_name(b);
70 s.push_str(&format!("(assert (<= {av}_end {bv}_start))\n"));
71 }
72 ScheduleConstraint::NoOverlap { tasks } => {
73 for i in 0..tasks.len() {
74 for j in (i+1)..tasks.len() {
75 let a = sanitize_name(&tasks[i]);
76 let b = sanitize_name(&tasks[j]);
77 s.push_str(&format!("(assert (or (<= {a}_end {b}_start) (<= {b}_end {a}_start)))\n"));
78 }
79 }
80 }
81 ScheduleConstraint::StartAfter { task, time } => {
82 let v = sanitize_name(task);
83 s.push_str(&format!("(assert (>= {v}_start {time}))\n"));
84 }
85 ScheduleConstraint::FinishBefore { task, time } => {
86 let v = sanitize_name(task);
87 s.push_str(&format!("(assert (<= {v}_end {time}))\n"));
88 }
89 ScheduleConstraint::Sequence { tasks } => {
90 for i in 0..tasks.len()-1 {
91 let a = sanitize_name(&tasks[i]);
92 let b = sanitize_name(&tasks[i+1]);
93 let dur = req.tasks.iter()
94 .find(|t| t.name == tasks[i])
95 .map(|t| t.duration)
96 .unwrap_or(0);
97 s.push_str(&format!("(assert (= {b}_start (+ {a}_start {dur})))\n"));
98 }
99 }
100 }
101 }
102
103 s.push_str("(check-sat)\n");
104 s.push_str("(get-model)\n");
105 s
106}
107
108pub fn parse_schedule(model: &str, tasks: &[TaskSpec]) -> String {
109 let mut schedule = Vec::new();
110 for t in tasks {
111 let v = sanitize_name(&t.name);
112 let start = find_define_fun_val(model, &format!("{v}_start"));
113 let end = find_define_fun_val(model, &format!("{v}_end"));
114 if let (Some(s), Some(e)) = (start, end) {
115 schedule.push(format!("{} {}-{}", t.name, format_time(s), format_time(e)));
116 }
117 }
118 schedule.join("\n")
119}
120
121fn find_define_fun_val(model: &str, var_name: &str) -> Option<i32> {
122 let lines: Vec<&str> = model.lines().collect();
128 for i in 0..lines.len() {
129 let line = lines[i].trim();
130 if line.starts_with("(define-fun") {
131 let parts: Vec<&str> = line.split_whitespace().collect();
132 if parts.len() >= 4 && parts[1] == var_name {
134 if parts.len() >= 5 {
136 let val = parts[parts.len() - 1].trim_end_matches(')');
137 if let Ok(v) = val.parse::<i32>() { return Some(v); }
138 }
139 if i + 1 < lines.len() {
141 let next = lines[i + 1].trim();
142 let val = next.trim_end_matches(')');
143 if let Ok(v) = val.parse::<i32>() { return Some(v); }
144 }
145 }
146 }
147 }
148 None
149}
150
151fn sanitize_name(name: &str) -> String {
152 name.to_lowercase().replace(' ', "_").replace('-', "_")
153}
154
155fn format_time(minutes: i32) -> String {
156 let h = minutes / 60;
157 let m = minutes % 60;
158 format!("{:02}:{:02}", h, m)
159}
160
161pub async fn run(
162 z3_bin: &std::path::PathBuf,
163 payload: &str,
164 timeout_secs: u64,
165) -> Result<String, serde_json::Error> {
166 let req: ScheduleRequest = serde_json::from_str(payload)?;
167 let smt = encode_schedule(&req);
168 let result = crate::solver::solve(z3_bin, &smt, timeout_secs).await;
169 if result.is_sat() {
170 let schedule = parse_schedule(&result.raw_output, &req.tasks);
171 Ok(format!("feasible\n{schedule}"))
172 } else if result.is_unsat() {
173 Ok("infeasible — constraints conflict, no valid schedule exists".to_string())
174 } else {
175 Ok(result.to_compact())
176 }
177}