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 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}