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