Skip to main content

knapsack_items_file/
main.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use roma_lib::{ChartObserver, HtmlReportObserver, Observable};
5use roma_lib::algorithms::{
6    Algorithm,
7    GeneticAlgorithm,
8    GeneticAlgorithmParameters,
9    TerminationCriteria,
10    TerminationCriterion,
11};
12use roma_lib::operator::{BinaryTournamentSelection, BitFlipMutation, SinglePointCrossover};
13use roma_lib::problem::build_knapsack_from_records;
14use roma_lib::solution_set::SolutionSet;
15use roma_lib::utils::cli::{
16    argument_value,
17    infer_format_from_extension,
18    parse_f64_flag_or,
19    parse_usize_flag_or,
20    resolve_path_from_flag_or_default,
21    seed_from_cli_or,
22};
23use roma_lib::utils::csv_adapter::read_csv_records;
24use roma_lib::utils::json_adapter::{get_json_value, read_json_records};
25use roma_lib::utils::yaml_adapter::{get_yaml_value, read_yaml_records};
26
27#[derive(Debug, Clone, Copy)]
28enum InputFormat {
29    Csv,
30    Json,
31    Yaml,
32}
33
34fn print_help_if_requested() -> bool {
35    let wants_help = std::env::args().skip(1).any(|arg| arg == "--help" || arg == "-h");
36    if !wants_help {
37        return false;
38    }
39
40    println!(
41        "Knapsack Items File Demo\n\
42Usage:\n\
43    cargo run --example knapsack_items_file -- [OPTIONS]\n\n\
44Options:\n\
45    --help, -h               Show this help message and exit\n\
46    --seed <u64>             Random seed for reproducible runs (default: 42)\n\
47    --input <path>           Input file path (.csv, .json, .yaml, .yml)\n\
48    --format <fmt>           Input format override: csv | json | yaml\n\
49                                                     If omitted, format is inferred from file extension\n\
50    --capacity <f64>         (CSV only) Knapsack capacity (default: 400.0)\n\
51    --limit <usize>          (CSV only) Maximum number of records (default: 120)\n\n\
52Record mapping policy (configured in code, not by CLI):\n\
53    CSV  -> records=root array, weight='weight', value='value'\n\
54    JSON -> records='dataset.items', weight='attributes.weight', value='attributes.value'\n\
55    YAML -> records='dataset.items', weight='attributes.weight', value='attributes.value'\n\n\
56Capacity/limit source:\n\
57    CSV  -> CLI flags (or defaults)\n\
58    JSON -> fields problem.capacity and problem.limit in file\n\
59    YAML -> fields problem.capacity and problem.limit in file\n\n\
60Examples:\n\
61    cargo run --example knapsack_items_file -- --input examples/knapsack_items_file/items.csv\n\
62    cargo run --example knapsack_items_file -- --input examples/knapsack_items_file/items.json\n\
63    cargo run --example knapsack_items_file -- --input examples/knapsack_items_file/items.yaml\n"
64    );
65
66    true
67}
68
69fn resolve_input_path() -> PathBuf {
70    let default_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
71        .join("examples")
72        .join("knapsack_items_file")
73        .join("items.csv");
74
75    resolve_path_from_flag_or_default("--input", default_path)
76}
77
78fn resolve_input_format(path: &Path) -> Result<InputFormat, String> {
79    if let Some(raw) = argument_value("--format") {
80        let normalized = raw.trim().to_ascii_lowercase();
81        return match normalized.as_str() {
82            "csv" => Ok(InputFormat::Csv),
83            "json" => Ok(InputFormat::Json),
84            "yaml" | "yml" => Ok(InputFormat::Yaml),
85            _ => Err(format!(
86                "Unsupported --format='{}'. Use csv, json or yaml",
87                raw
88            )),
89        };
90    }
91
92    infer_format_from_extension(path)
93        .and_then(|fmt| match fmt.as_str() {
94            "csv" => Some(InputFormat::Csv),
95            "json" => Some(InputFormat::Json),
96            "yaml" => Some(InputFormat::Yaml),
97            _ => None,
98        })
99        .ok_or_else(|| {
100            format!(
101                "Could not infer format from extension '{}'. Use --format=csv|json|yaml",
102                path.display()
103            )
104        })
105}
106
107fn records_path_for_format(input_format: InputFormat) -> &'static str {
108    match input_format {
109        InputFormat::Csv => "",
110        InputFormat::Json | InputFormat::Yaml => "dataset.items",
111    }
112}
113
114fn weight_key_for_format(input_format: InputFormat) -> &'static str {
115    match input_format {
116        InputFormat::Csv => "weight",
117        InputFormat::Json | InputFormat::Yaml => "attributes.weight",
118    }
119}
120
121fn value_key_for_format(input_format: InputFormat) -> &'static str {
122    match input_format {
123        InputFormat::Csv => "value",
124        InputFormat::Json | InputFormat::Yaml => "attributes.value",
125    }
126}
127
128fn read_records_from_input(
129    path: &Path,
130    input_format: InputFormat,
131    records_path: &str,
132) -> Result<Vec<HashMap<String, String>>, String> {
133    match input_format {
134        InputFormat::Csv => read_csv_records(path, ',')
135            .map_err(|e| format!("Failed to read CSV '{}': {}", path.display(), e)),
136        InputFormat::Json => read_json_records(path, records_path)
137            .map_err(|e| format!("Failed to read JSON '{}': {}", path.display(), e)),
138        InputFormat::Yaml => read_yaml_records(path, records_path)
139            .map_err(|e| format!("Failed to read YAML '{}': {}", path.display(), e)),
140    }
141}
142
143fn read_json_required_f64(path: &Path, key_path: &str) -> Result<f64, String> {
144    let raw = get_json_value(path, key_path)
145        .map_err(|e| format!("Failed to read JSON '{}': {}", path.display(), e))?
146        .ok_or_else(|| format!("Missing JSON key '{}'", key_path))?;
147
148    raw.parse::<f64>()
149        .map_err(|_| format!("JSON key '{}' must be numeric, got '{}'", key_path, raw))
150}
151
152fn read_json_required_usize(path: &Path, key_path: &str) -> Result<usize, String> {
153    let raw = get_json_value(path, key_path)
154        .map_err(|e| format!("Failed to read JSON '{}': {}", path.display(), e))?
155        .ok_or_else(|| format!("Missing JSON key '{}'", key_path))?;
156
157    raw.parse::<usize>()
158        .map_err(|_| format!("JSON key '{}' must be usize, got '{}'", key_path, raw))
159}
160
161fn read_yaml_required_f64(path: &Path, key_path: &str) -> Result<f64, String> {
162    let raw = get_yaml_value(path, key_path)
163        .map_err(|e| format!("Failed to read YAML '{}': {}", path.display(), e))?
164        .ok_or_else(|| format!("Missing YAML key '{}'", key_path))?;
165
166    raw.parse::<f64>()
167        .map_err(|_| format!("YAML key '{}' must be numeric, got '{}'", key_path, raw))
168}
169
170fn read_yaml_required_usize(path: &Path, key_path: &str) -> Result<usize, String> {
171    let raw = get_yaml_value(path, key_path)
172        .map_err(|e| format!("Failed to read YAML '{}': {}", path.display(), e))?
173        .ok_or_else(|| format!("Missing YAML key '{}'", key_path))?;
174
175    raw.parse::<usize>()
176        .map_err(|_| format!("YAML key '{}' must be usize, got '{}'", key_path, raw))
177}
178
179fn resolve_capacity_and_limit(path: &Path, input_format: InputFormat) -> Result<(f64, usize), String> {
180    match input_format {
181        InputFormat::Csv => Ok((
182            parse_f64_flag_or("--capacity", 400.0),
183            parse_usize_flag_or("--limit", 120),
184        )),
185        InputFormat::Json => Ok((
186            read_json_required_f64(path, "problem.capacity")?,
187            read_json_required_usize(path, "problem.limit")?,
188        )),
189        InputFormat::Yaml => Ok((
190            read_yaml_required_f64(path, "problem.capacity")?,
191            read_yaml_required_usize(path, "problem.limit")?,
192        )),
193    }
194}
195
196fn main() {
197    if print_help_if_requested() {
198        return;
199    }
200
201    let seed = seed_from_cli_or(42);
202    let input_path = resolve_input_path();
203    let input_format = resolve_input_format(&input_path).unwrap_or_else(|msg| panic!("{}", msg));
204    let records_path = records_path_for_format(input_format);
205    let weight_key = weight_key_for_format(input_format);
206    let value_key = value_key_for_format(input_format);
207    let (capacity, row_limit) =
208        resolve_capacity_and_limit(&input_path, input_format).unwrap_or_else(|msg| panic!("{}", msg));
209
210    let records = read_records_from_input(&input_path, input_format, records_path)
211        .unwrap_or_else(|msg| panic!("{}", msg));
212
213    let (problem, loaded_items) =
214        build_knapsack_from_records(&records, capacity, row_limit, weight_key, value_key)
215            .unwrap_or_else(|msg| panic!("{}", msg));
216
217    // Build the algorithm 
218    let parameters = GeneticAlgorithmParameters::new(
219        80,
220        0.85,
221        0.04,
222        SinglePointCrossover::new(),
223        BitFlipMutation::new(),
224        BinaryTournamentSelection::new(),
225        TerminationCriteria::new(vec![TerminationCriterion::MaxIterations(60)]),
226    )
227    .with_seed(seed);
228
229    let chart_observer = ChartObserver::new_default();
230    let html_observer = HtmlReportObserver::new_default();
231
232    let mut algorithm = GeneticAlgorithm::new(parameters);
233    algorithm.add_observer(Box::new(chart_observer));
234    algorithm.add_observer(Box::new(html_observer));
235    let result = algorithm.run(&problem).expect("Large CSV GA run failed");
236
237    if let Some(best) = result.best_solution(&problem) {
238        println!(
239            "Large dataset GA demo finished (seed={}). input='{}', format={:?}, capacity={}, limit={}, records_path='{}', weight_key='{}', value_key='{}', items={}, best fitness={:.4}",
240            seed,
241            input_path.display(),
242            input_format,
243            capacity,
244            row_limit,
245            records_path,
246            weight_key,
247            value_key,
248            loaded_items,
249            best.quality_value()
250        );
251    } else {
252        println!("Large CSV GA demo finished with no solutions (seed={})", seed);
253    }
254}