Skip to main content

z39_solver/domains/
schedule.rs

1/// Schedule domain: translate scheduling problems into SMT-LIB2 for Z3
2///
3/// The agent describes tasks, time slots, and constraints.
4/// We encode them as integer variables with ordering/overlap constraints.
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ScheduleRequest {
10    pub tasks: Vec<TaskSpec>,
11    /// Time slot: available start and end (minutes from midnight)
12    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    // No-overlap between ALL tasks by default
57    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    // Z3 model format (multi-line):
123    //   (define-fun var_name () Int
124    //     VALUE)
125    // or single-line:
126    //   (define-fun var_name () Sort VALUE)
127    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            // parts: [(define-fun, var_name, (), Sort, ...]
133            if parts.len() >= 4 && parts[1] == var_name {
134                // Check if value is on the same line
135                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                // Value on next line
140                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}