chai/
lib.rs

1//! libchai 是使用 Rust 实现的汉字编码输入方案的优化算法。它同时发布为一个 Rust crate 和一个 NPM 模块,前者可以在 Rust 项目中安装为依赖来使用,后者可以通过汉字自动拆分系统的图形界面来使用。
2//!
3//! chai 是使用 libchai 实现的命令行程序,用户提供方案配置文件、拆分表和评测信息,本程序能够生成编码并评测一系列指标,以及基于退火算法优化元素的布局。
4
5pub mod config;
6pub mod data;
7pub mod encoders;
8pub mod objectives;
9pub mod operators;
10pub mod optimizers;
11
12use chrono::Local;
13use clap::{Parser, Subcommand};
14use config::{ObjectiveConfig, OptimizationConfig, SolverConfig, 配置};
15use console_error_panic_hook::set_once;
16use csv::{ReaderBuilder, WriterBuilder};
17use data::{原始可编码对象, 数据};
18use data::{原始当量信息, 原始键位分布信息, 码表项};
19use encoders::default::默认编码器;
20use encoders::编码器;
21use js_sys::Function;
22use objectives::default::默认目标函数;
23use objectives::{metric::Metric, 目标函数};
24use operators::default::默认操作;
25use optimizers::{优化方法, 优化问题};
26use serde::{Deserialize, Serialize};
27use serde_wasm_bindgen::{from_value, to_value};
28use serde_with::skip_serializing_none;
29use std::collections::HashMap;
30use std::fs::{create_dir_all, read_to_string, write, OpenOptions};
31use std::io::Write;
32use std::iter::FromIterator;
33use std::path::{Path, PathBuf};
34use wasm_bindgen::{prelude::*, JsError};
35
36/// 错误类型
37#[derive(Debug, Clone)]
38pub struct 错误 {
39    pub message: String,
40}
41
42impl From<String> for 错误 {
43    fn from(value: String) -> Self {
44        Self { message: value }
45    }
46}
47
48impl From<&str> for 错误 {
49    fn from(value: &str) -> Self {
50        Self {
51            message: value.to_string(),
52        }
53    }
54}
55
56impl From<错误> for JsError {
57    fn from(value: 错误) -> Self {
58        JsError::new(&value.message)
59    }
60}
61
62/// 图形界面参数的定义
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct 图形界面参数 {
65    pub 配置: 配置,
66    pub 词列表: Vec<原始可编码对象>,
67    pub 原始键位分布信息: 原始键位分布信息,
68    pub 原始当量信息: 原始当量信息,
69}
70
71impl Default for 图形界面参数 {
72    fn default() -> Self {
73        Self {
74            配置: 配置::default(),
75            词列表: vec![],
76            原始键位分布信息: HashMap::new(),
77            原始当量信息: HashMap::new(),
78        }
79    }
80}
81
82/// 向用户反馈的消息类型
83#[derive(Serialize)]
84#[serde(tag = "type", rename_all = "snake_case")]
85#[skip_serializing_none]
86pub enum 消息 {
87    TrialMax {
88        temperature: f64,
89        accept_rate: f64,
90    },
91    TrialMin {
92        temperature: f64,
93        improve_rate: f64,
94    },
95    Parameters {
96        t_max: f64,
97        t_min: f64,
98    },
99    Progress {
100        steps: usize,
101        temperature: f64,
102        metric: Metric,
103    },
104    BetterSolution {
105        metric: Metric,
106        config: 配置,
107        save: bool,
108    },
109    Elapsed(u128),
110}
111
112/// 定义了向用户报告消息的接口,用于统一命令行和图形界面的输出方式
113///
114/// 命令行界面、图形界面只需要各自实现 post 方法,就可向用户报告各种用户数据
115pub trait 界面 {
116    fn 发送(&self, 消息: 消息);
117}
118
119/// 通过图形界面来使用 libchai 的入口,实现了界面特征
120#[wasm_bindgen]
121pub struct Web {
122    回调: Function,
123    参数: 图形界面参数,
124}
125
126/// 用于在图形界面验证输入的配置是否正确
127#[wasm_bindgen]
128pub fn validate(js_config: JsValue) -> Result<JsValue, JsError> {
129    set_once();
130    let config: 配置 = from_value(js_config)?;
131    let config_str = serde_yaml::to_string(&config).unwrap();
132    Ok(to_value(&config_str)?)
133}
134
135#[wasm_bindgen]
136impl Web {
137    pub fn new(回调: Function) -> Web {
138        set_once();
139        let 参数 = 图形界面参数::default();
140        Self { 回调, 参数 }
141    }
142
143    pub fn sync(&mut self, 前端参数: JsValue) -> Result<(), JsError> {
144        self.参数 = from_value(前端参数)?;
145        Ok(())
146    }
147
148    pub fn encode_evaluate(&self, 前端目标函数配置: JsValue) -> Result<JsValue, JsError> {
149        let 目标函数配置: ObjectiveConfig = from_value(前端目标函数配置)?;
150        let 图形界面参数 {
151            mut 配置,
152            原始键位分布信息,
153            原始当量信息,
154            词列表,
155        } = self.参数.clone();
156        配置.optimization = Some(OptimizationConfig {
157            objective: 目标函数配置,
158            constraints: None,
159            metaheuristic: None,
160        });
161        let 数据 = 数据::新建(配置, 词列表, 原始键位分布信息, 原始当量信息)?;
162        let mut 编码器 = 默认编码器::新建(&数据)?;
163        let mut 编码结果 = 编码器.编码(&数据.初始映射, &None).clone();
164        let 码表 = 数据.生成码表(&编码结果);
165        let mut 目标函数 = 默认目标函数::新建(&数据)?;
166        let (指标, _) = 目标函数.计算(&mut 编码结果);
167        Ok(to_value(&(码表, 指标))?)
168    }
169
170    pub fn optimize(&self) -> Result<(), JsError> {
171        let 图形界面参数 {
172            配置,
173            原始键位分布信息,
174            原始当量信息,
175            词列表,
176        } = self.参数.clone();
177        let 优化方法配置 = 配置.clone().optimization.unwrap().metaheuristic.unwrap();
178        let 数据 = 数据::新建(配置, 词列表, 原始键位分布信息, 原始当量信息)?;
179        let 编码器 = 默认编码器::新建(&数据)?;
180        let 目标函数 = 默认目标函数::新建(&数据)?;
181        let 操作 = 默认操作::新建(&数据)?;
182        let mut 问题 = 优化问题::新建(数据, 编码器, 目标函数, 操作);
183        let SolverConfig::SimulatedAnnealing(退火) = 优化方法配置;
184        退火.优化(&mut 问题, self);
185        Ok(())
186    }
187}
188
189impl 界面 for Web {
190    fn 发送(&self, message: 消息) {
191        let js_message = to_value(&message).unwrap();
192        self.回调.call1(&JsValue::null(), &js_message).unwrap();
193    }
194}
195
196/// 命令行参数的定义
197#[derive(Parser, Clone)]
198#[command(name = "汉字自动拆分系统")]
199#[command(author, version, about, long_about)]
200#[command(propagate_version = true)]
201pub struct 命令行参数 {
202    #[command(subcommand)]
203    pub command: 命令,
204    /// 方案文件,默认为 config.yaml
205    pub config: Option<PathBuf>,
206    /// 频率序列表,默认为 elements.txt
207    #[arg(short, long, value_name = "FILE")]
208    pub encodables: Option<PathBuf>,
209    /// 单键用指分布表,默认为 assets 目录下的 key_distribution.txt
210    #[arg(short, long, value_name = "FILE")]
211    pub key_distribution: Option<PathBuf>,
212    /// 双键速度当量表,默认为 assets 目录下的 pair_equivalence.txt
213    #[arg(short, long, value_name = "FILE")]
214    pub pair_equivalence: Option<PathBuf>,
215    /// 线程数,默认为 1
216    #[arg(short, long)]
217    pub threads: Option<usize>,
218}
219
220/// 命令行中所有可用的子命令
221#[derive(Subcommand, Clone)]
222pub enum 命令 {
223    /// 使用方案文件和拆分表计算出字词编码并统计各类评测指标
224    Encode,
225    /// 基于拆分表和方案文件中的配置优化元素布局
226    Optimize,
227}
228
229/// 通过命令行来使用 libchai 的入口,实现了界面特征
230pub struct 命令行 {
231    pub 参数: 命令行参数,
232    pub 输出目录: PathBuf,
233}
234
235impl 命令行 {
236    pub fn 新建(args: 命令行参数, maybe_output_dir: Option<PathBuf>) -> Self {
237        let output_dir = maybe_output_dir.unwrap_or_else(|| {
238            let time = Local::now().format("%m-%d+%H_%M_%S").to_string();
239            PathBuf::from(format!("output-{}", time))
240        });
241        create_dir_all(output_dir.clone()).unwrap();
242        Self {
243            参数: args,
244            输出目录: output_dir,
245        }
246    }
247
248    pub fn 读取(name: &str) -> 数据 {
249        let config = format!("examples/{}.yaml", name);
250        let elements = format!("examples/{}.txt", name);
251        let 参数 = 命令行参数 {
252            command: 命令::Optimize,
253            config: Some(PathBuf::from(config)),
254            encodables: Some(PathBuf::from(elements)),
255            key_distribution: None,
256            pair_equivalence: None,
257            threads: None,
258        };
259        let cli = 命令行::新建(参数, None);
260        cli.准备数据()
261    }
262
263    fn read<I, T>(path: PathBuf) -> T
264    where
265        I: for<'de> Deserialize<'de>,
266        T: FromIterator<I>,
267    {
268        let mut reader = ReaderBuilder::new()
269            .delimiter(b'\t')
270            .has_headers(false)
271            .flexible(true)
272            .from_path(path)
273            .unwrap();
274        reader.deserialize().map(|x| x.unwrap()).collect()
275    }
276
277    pub fn 准备数据(&self) -> 数据 {
278        let 命令行参数 {
279            config,
280            encodables: elements,
281            key_distribution,
282            pair_equivalence,
283            ..
284        } = self.参数.clone();
285        let config_path = config.unwrap_or(PathBuf::from("config.yaml"));
286        let config_content = read_to_string(&config_path)
287            .unwrap_or_else(|_| panic!("文件 {} 不存在", config_path.display()));
288        let config: 配置 = serde_yaml::from_str(&config_content).unwrap();
289        let elements_path = elements.unwrap_or(PathBuf::from("elements.txt"));
290        let encodables: Vec<原始可编码对象> = Self::read(elements_path);
291
292        let assets_dir = Path::new("assets");
293        let keq_path = key_distribution.unwrap_or(assets_dir.join("key_distribution.txt"));
294        let key_distribution: 原始键位分布信息 = Self::read(keq_path);
295        let peq_path = pair_equivalence.unwrap_or(assets_dir.join("pair_equivalence.txt"));
296        let pair_equivalence: 原始当量信息 = Self::read(peq_path);
297        数据::新建(config, encodables, key_distribution, pair_equivalence).unwrap()
298    }
299
300    pub fn 输出编码结果(&self, entries: Vec<码表项>) {
301        let path = self.输出目录.join("编码.txt");
302        let mut writer = WriterBuilder::new()
303            .delimiter(b'\t')
304            .has_headers(false)
305            .from_path(&path)
306            .unwrap();
307        for 码表项 {
308            name,
309            full,
310            full_rank,
311            short,
312            short_rank,
313        } in entries
314        {
315            writer
316                .serialize((&name, &full, &full_rank, &short, &short_rank))
317                .unwrap();
318        }
319        writer.flush().unwrap();
320        println!("已完成编码,结果保存在 {} 中", path.clone().display());
321    }
322
323    pub fn 输出评测指标(&self, metric: Metric) {
324        let path = self.输出目录.join("评测指标.yaml");
325        print!("{}", metric);
326        let metric_str = serde_yaml::to_string(&metric).unwrap();
327        write(&path, metric_str).unwrap();
328    }
329
330    pub fn 生成子命令行(&self, index: usize) -> 命令行 {
331        let child_dir = self.输出目录.join(format!("{}", index));
332        命令行::新建(self.参数.clone(), Some(child_dir))
333    }
334}
335
336impl 界面 for 命令行 {
337    fn 发送(&self, message: 消息) {
338        let mut writer: Box<dyn Write> = if self.参数.threads.is_some() {
339            let log_path = self.输出目录.join("log.txt");
340            let file = OpenOptions::new()
341                .create(true) // 如果文件不存在,则创建
342                .append(true) // 追加写入,不覆盖原有内容
343                .open(log_path)
344                .expect("Failed to open file");
345            Box::new(file)
346        } else {
347            Box::new(std::io::stdout())
348        };
349        let result = match message {
350            消息::TrialMax {
351                temperature,
352                accept_rate,
353            } => writeln!(
354                &mut writer,
355                "若温度为 {:.2e},接受率为 {:.2}%",
356                temperature,
357                accept_rate * 100.0
358            ),
359            消息::TrialMin {
360                temperature,
361                improve_rate,
362            } => writeln!(
363                &mut writer,
364                "若温度为 {:.2e},改进率为 {:.2}%",
365                temperature,
366                improve_rate * 100.0
367            ),
368            消息::Parameters { t_max, t_min } => writeln!(
369                &mut writer,
370                "参数寻找完成,从最高温 {} 降到最低温 {}……",
371                t_max, t_min
372            ),
373            消息::Elapsed(time) => writeln!(&mut writer, "计算一次评测用时:{} μs", time),
374            消息::Progress {
375                steps,
376                temperature,
377                metric,
378            } => writeln!(
379                &mut writer,
380                "已执行 {} 步,当前温度为 {:.2e},当前评测指标如下:\n{}",
381                steps, temperature, metric
382            ),
383            消息::BetterSolution {
384                metric,
385                config,
386                save,
387            } => {
388                let time = Local::now();
389                let prefix = time.format("%m-%d+%H_%M_%S_%3f").to_string();
390                let config_path = self.输出目录.join(format!("{}.yaml", prefix));
391                let metric_path = self.输出目录.join(format!("{}.metric.yaml", prefix));
392                let mut res1 = writeln!(
393                    &mut writer,
394                    "{} 系统搜索到了一个更好的方案,评测指标如下:\n{}",
395                    time.format("%H:%M:%S"),
396                    metric
397                );
398                let config = serde_yaml::to_string(&config).unwrap();
399                let metric = serde_yaml::to_string(&metric).unwrap();
400                if save {
401                    write(metric_path, metric).unwrap();
402                    write(config_path, config).unwrap();
403                    let res2 = writeln!(
404                        &mut writer,
405                        "方案文件保存于 {}.yaml 中,评测指标保存于 {}.metric.yaml 中",
406                        prefix, prefix
407                    );
408                    res1 = res1.and(res2);
409                }
410                res1
411            }
412        };
413        result.unwrap();
414    }
415}