Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions crates/solverforge-cli/src/commands/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ static BASIC_GENERIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates
static EMPLOYEE_SCHEDULING_TEMPLATE: Dir =
include_dir!("$CARGO_MANIFEST_DIR/templates/basic/employee-scheduling");

static LIST_GENERIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/list/generic");

static VEHICLE_ROUTING_TEMPLATE: Dir =
include_dir!("$CARGO_MANIFEST_DIR/templates/list/vehicle-routing");

Expand All @@ -20,6 +22,7 @@ const AVAILABLE_TEMPLATES: &str = "
--basic=employee-scheduling — assign employees to shifts

List Variable (each entity owns an ordered sequence):
--list — generic list-variable skeleton
--list=vehicle-routing — capacitated vehicle routing (CVRP)";

pub fn run(
Expand Down Expand Up @@ -53,6 +56,15 @@ pub fn run(
skip_readme,
quiet,
),
Template::List => scaffold(
name,
&crate_name,
&LIST_GENERIC_TEMPLATE,
"list",
skip_git,
skip_readme,
quiet,
),
Template::ListVehicleRouting => scaffold(
name,
&crate_name,
Expand Down Expand Up @@ -269,6 +281,15 @@ fn print_template_guidance(project_name: &str, label: &str) {
println!(" solverforge generate constraint all_assigned --unary --hard");
println!(" solverforge server");
}
"list" => {
println!(" solverforge server");
println!();
println!(" This template includes:");
println!(" - 2-phase solver (cheapest insertion + late acceptance)");
println!(" - Balanced load constraint (soft)");
println!(" - Sequence view at http://localhost:7860");
println!(" - REST API with SSE live updates");
}
_ => {
println!(" solverforge server");
}
Expand Down Expand Up @@ -345,6 +366,7 @@ fn generate_readme(project_name: &str, _crate_name: &str, label: &str) -> String
pub enum Template {
Basic,
BasicEmployeeScheduling,
List,
ListVehicleRouting,
}

Expand All @@ -353,10 +375,7 @@ impl Template {
match (basic, list, specialization) {
(true, false, None) => Ok(Template::Basic),
(true, false, Some("employee-scheduling")) => Ok(Template::BasicEmployeeScheduling),
(false, true, None) => Err(CliError::with_hint(
"the --list template requires a specialization",
"Use --list=vehicle-routing".to_string(),
)),
(false, true, None) => Ok(Template::List),
(false, true, Some("vehicle-routing")) => Ok(Template::ListVehicleRouting),
(false, false, None) => Err(CliError::with_hint(
"specify a template flag",
Expand Down
8 changes: 5 additions & 3 deletions crates/solverforge-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use error::CliResult;

const EXAMPLES: &str = "\x1b[1mExamples:\x1b[0m
solverforge new my-scheduler --basic=employee-scheduling
solverforge new my-planner --basic
solverforge new my-sorter --list
solverforge new my-router --list=vehicle-routing
solverforge generate entity shift --planning-variable employee_idx
solverforge generate constraint no_overlap --pair --hard
Expand Down Expand Up @@ -56,14 +58,14 @@ enum Command {
/// Variable class (required, mutually exclusive):
///
/// --basic Standard variable — each entity holds one assigned value
/// --list=... List variable — each entity owns an ordered sequence
/// --list List variable — each entity owns an ordered sequence
///
/// Specializations (append after the flag with =):
///
/// --basic=employee-scheduling
/// --list=vehicle-routing
#[command(
after_help = "Examples:\n solverforge new my-scheduler --basic=employee-scheduling\n solverforge new my-router --list=vehicle-routing\n solverforge new my-planner --basic"
after_help = "Examples:\n solverforge new my-scheduler --basic=employee-scheduling\n solverforge new my-planner --basic\n solverforge new my-sorter --list\n solverforge new my-router --list=vehicle-routing"
)]
New {
/// Project name (directory that will be created)
Expand All @@ -73,7 +75,7 @@ enum Command {
#[arg(long = "basic", value_name = "SPECIALIZATION", num_args = 0..=1, require_equals = true)]
basic: Option<Option<String>>,

/// Scaffold a list-variable project specialization (currently: --list=vehicle-routing)
/// Scaffold a list-variable project (optionally: --list=vehicle-routing)
#[arg(
long = "list",
value_name = "SPECIALIZATION",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ name = "{{crate_name}}"
path = "src/main.rs"

[dependencies]
solverforge = { version = "{{solverforge_version}}", features = ["serde"] }
solverforge = { version = "{{solverforge_version}}", features = ["serde", "console", "verbose-logging"] }
solverforge-ui = "0.2.0"
# Web server
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1", features = ["sync"] }
tower-http = { version = "0.6", features = ["fs", "cors"] }
tower = "0.5"

Expand All @@ -23,7 +25,3 @@ serde_json = "1"
# Utilities
uuid = { version = "1", features = ["v4", "serde"] }
parking_lot = "0.12"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
owo-colors = "4"
num-format = "0.4"
12 changes: 12 additions & 0 deletions crates/solverforge-cli/templates/basic/generic/solver.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "allocate_to_value_from_queue"

[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
[phases.forager]
accepted_count_limit = 4

[termination]
seconds_spent_limit = 30
48 changes: 46 additions & 2 deletions crates/solverforge-cli/templates/basic/generic/src/api/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,29 @@ use solverforge::SolverStatus;
pub struct ResourceDto {
pub index: usize,
pub name: String,
pub capacity: i64,
pub affinity_group: String,
}

impl From<&Resource> for ResourceDto {
fn from(r: &Resource) -> Self {
Self { index: r.index, name: r.name.clone() }
Self {
index: r.index,
name: r.name.clone(),
capacity: r.capacity,
affinity_group: r.affinity_group.clone(),
}
}
}

impl ResourceDto {
pub fn to_resource(&self) -> Resource {
Resource::new(self.index, &self.name)
Resource::new(
self.index,
&self.name,
self.capacity,
&self.affinity_group,
)
}
}

Expand All @@ -27,6 +39,8 @@ impl ResourceDto {
pub struct TaskDto {
pub id: String,
pub name: String,
pub demand: i64,
pub preferred_group: String,
pub resource: Option<ResourceDto>,
}

Expand All @@ -41,6 +55,32 @@ pub struct PlanDto {
pub solver_status: Option<SolverStatus>,
}

/// Constraint analysis result.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstraintAnalysisDto {
pub name: String,
#[serde(rename = "type")]
pub constraint_type: String,
pub weight: String,
pub score: String,
pub matches: Vec<ConstraintMatchDto>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstraintMatchDto {
pub score: String,
pub justification: String,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalyzeResponse {
pub score: String,
pub constraints: Vec<ConstraintAnalysisDto>,
}

impl PlanDto {
pub fn from_plan(plan: &Plan, status: Option<SolverStatus>) -> Self {
let resources: Vec<ResourceDto> = plan.resources.iter().map(ResourceDto::from).collect();
Expand All @@ -50,6 +90,8 @@ impl PlanDto {
.map(|t| TaskDto {
id: t.id.clone(),
name: t.name.clone(),
demand: t.demand,
preferred_group: t.preferred_group.clone(),
resource: t
.resource_idx
.and_then(|idx| plan.resources.get(idx))
Expand All @@ -75,6 +117,8 @@ impl PlanDto {
.map(|t| Task {
id: t.id.clone(),
name: t.name.clone(),
demand: t.demand,
preferred_group: t.preferred_group.clone(),
resource_idx: t
.resource
.as_ref()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod dto;
mod routes;
mod sse;

pub use routes::{router, AppState};
Loading
Loading