From c05b4b1f0663cbcf07aaa5602359dbbd97289608 Mon Sep 17 00:00:00 2001 From: ARCJ137442 <61109168+ARCJ137442@users.noreply.github.com> Date: Thu, 1 Aug 2024 11:08:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:construction:=20=E7=9F=AD=E6=B5=AE?= =?UTF-8?q?=E7=82=B9=E7=B2=BE=E5=BA=A6=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在「Narsese预期」中增加「短浮点精度匹配」功能,用于统一不同精度的真值、预算值判等;适用case:OpenNARS与PyNARS在小数点精度上「两位🆚四位」的不一致 --- src/bin/babelnar_cli/runtime_manage.rs | 1 + src/bin/babelnar_cli/vm_config.rs | 37 +++- src/test_tools/vm_interact/mod.rs | 43 ++-- .../vm_interact/narsese_expectation.rs | 190 ++++++++++++------ 4 files changed, 189 insertions(+), 82 deletions(-) diff --git a/src/bin/babelnar_cli/runtime_manage.rs b/src/bin/babelnar_cli/runtime_manage.rs index ede6128..655cca2 100644 --- a/src/bin/babelnar_cli/runtime_manage.rs +++ b/src/bin/babelnar_cli/runtime_manage.rs @@ -420,6 +420,7 @@ where output_cache, config.user_input, nal_root_path, + config.short_float_epoch, ); // 处理错误 if let Err(e) = put_result { diff --git a/src/bin/babelnar_cli/vm_config.rs b/src/bin/babelnar_cli/vm_config.rs index b5d993d..609c737 100644 --- a/src/bin/babelnar_cli/vm_config.rs +++ b/src/bin/babelnar_cli/vm_config.rs @@ -70,6 +70,10 @@ use std::{ path::{Component, Path, PathBuf}, }; +/// 默认的浮点精度 +/// * 🚩【2024-08-01 10:28:12】目前与[`narsese`]统一使用[`f64`] +pub type Float = narsese::api::FloatPrecision; + /// 允许的配置文件扩展名 /// * 🚩【2024-04-07 18:30:24】目前支持JSON与HJSON /// * 📌其顺序决定了在「扩展名优先补充」中的遍历顺序 @@ -99,9 +103,11 @@ macro_rules! coalesce_clones { /// * 📌这意味着其总是能派生[`Default`] /// * ⚠️其中的所有**相对路径**,在[`read_config_extern`]中都基于**配置文件自身** /// * 🎯不论CLI自身所处何处,均保证配置读取稳定 +/// * 🚩【2024-08-01 10:31:10】因引入浮点类型[`Float`],放弃派生[`Eq`]特征(传递性丧失) +/// * 📄含[`NaN`](Float::NAN)、[`Infinity`](Float::INFINITY)、[`-Infinity`](Float::NEG_INFINITY) #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // 🔗参考: -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq)] pub struct LaunchConfig { /// 配置的加载路径 /// * 🎯用于记录「基于配置自身的配置路径」 @@ -162,6 +168,11 @@ pub struct LaunchConfig { /// * 🚩【2024-04-04 02:19:36】默认值由「运行时转换」决定 /// * 🎯兼容「多启动配置合并」 pub strict_mode: Option, + + /// 短浮点精度 + /// * 🎯判等模糊性:用于解决不同版本NARS的小数位数差异问题(统一限定在最低位) + /// * 🚩只需「真值/预算值」处的短浮点与预期之差在一定范围内,而无需绝对精确匹配 + pub short_float_epoch: Option, } /// 使用`const`常量存储「空启动配置」 @@ -179,6 +190,7 @@ const EMPTY_LAUNCH_CONFIG: LaunchConfig = LaunchConfig { input_mode: None, auto_restart: None, strict_mode: None, + short_float_epoch: None, }; /// NAVM虚拟机(运行时)运行时配置 @@ -186,7 +198,7 @@ const EMPTY_LAUNCH_CONFIG: LaunchConfig = LaunchConfig { /// * 🚩自[`LaunchConfig`]加载而来 #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] // 🔗参考: -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub struct RuntimeConfig { /// 配置的加载路径 /// * 🎯用于记录「基于配置自身的配置路径」 @@ -237,6 +249,12 @@ pub struct RuntimeConfig { /// * 📜默认值:`false`(关闭) #[serde(default = "bool_false")] pub strict_mode: bool, + + /// 短浮点精度 + /// * 🚩必选:[`None`]将视为默认值 + /// * 📜默认值:`0.0`(绝对匹配) + #[serde(default = "default_epoch")] + pub short_float_epoch: Float, } /// 布尔值`true` @@ -257,6 +275,13 @@ const fn bool_false() -> bool { false } +/// 默认精度 +/// * 🎯配置解析中「默认为`0.0`」的默认值指定 +#[inline(always)] +const fn default_epoch() -> Float { + 0.0 +} + /// 尝试将启动时配置[`LaunchConfig`]转换成运行时配置[`RuntimeConfig`] /// * 📌默认项:存在默认值,如「启用用户输入」「不自动重启」 /// * 📌必选项:要求必填值,如「转译器组」「启动命令」 @@ -277,13 +302,15 @@ impl TryFrom for RuntimeConfig { prelude_nal: config.prelude_nal, // * 🚩默认项统一用`unwrap_or` // 默认启用用户输入 - user_input: config.user_input.unwrap_or(true), + user_input: config.user_input.unwrap_or(bool_true()), // 输入模式传递默认值 input_mode: config.input_mode.unwrap_or_default(), // 不自动重启 - auto_restart: config.auto_restart.unwrap_or(false), + auto_restart: config.auto_restart.unwrap_or(bool_false()), // 不开启严格模式 - strict_mode: config.strict_mode.unwrap_or(false), + strict_mode: config.strict_mode.unwrap_or(bool_false()), + // 完全严格的短浮点 + short_float_epoch: config.short_float_epoch.unwrap_or(default_epoch()), }) } } diff --git a/src/test_tools/vm_interact/mod.rs b/src/test_tools/vm_interact/mod.rs index ca6fcd8..1515b3a 100644 --- a/src/test_tools/vm_interact/mod.rs +++ b/src/test_tools/vm_interact/mod.rs @@ -4,6 +4,7 @@ use super::{NALInput, OutputExpectation, OutputExpectationError}; use crate::cli_support::{error_handling_boost::error_anyhow, io::output_print::OutputType}; use anyhow::Result; use nar_dev_utils::{if_return, ResultBoost}; +use narsese::api::FloatPrecision; use navm::{cmd::Cmd, output::Output, vm::VmRuntime}; use std::{ops::ControlFlow, path::Path}; @@ -17,7 +18,7 @@ mod term_equal; /// 实现/预期匹配功能 impl OutputExpectation { /// 判断一个「NAVM输出」是否与自身相符合 - pub fn matches(&self, output: &Output) -> bool { + pub fn matches(&self, output: &Output, precision_epoch: FloatPrecision) -> bool { // 输出类型 if let Some(expected) = &self.output_type { if_return! { expected != output.type_name() => false } @@ -29,7 +30,10 @@ impl OutputExpectation { (Some(..), None) => return false, // 预期输出都有⇒判断Narsese是否相同 (Some(expected), Some(out)) => { - if_return! { !is_expected_narsese_lexical(expected, out) => false } + if_return! { + !is_expected_narsese_lexical(expected, out, precision_epoch) + => false + } } _ => (), } @@ -72,19 +76,22 @@ pub fn put_nal( // 不能传入「启动配置」,就要传入「是否启用用户输入」状态变量 enabled_user_input: bool, nal_root_path: &Path, + precision_epoch: FloatPrecision, ) -> Result<()> { + // TODO: 【2024-08-01 10:54:34】各个分支单独提取到函数 + use NALInput::*; match input { // 置入NAVM指令 - NALInput::Put(cmd) => vm.input_cmd(cmd), + Put(cmd) => vm.input_cmd(cmd), // 睡眠 - NALInput::Sleep(duration) => { + Sleep(duration) => { // 睡眠指定时间 std::thread::sleep(duration); // 返回`ok` Ok(()) } // 等待一个符合预期的NAVM输出 - NALInput::Await(expectation) => loop { + Await(expectation) => loop { let output = match vm.fetch_output() { Ok(output) => { // 加入缓存 @@ -99,20 +106,22 @@ pub fn put_nal( } }; // 只有匹配了才返回 - if expectation.matches(&output) { + if expectation.matches(&output, precision_epoch) { break Ok(()); } }, // 检查是否有NAVM输出符合预期 - NALInput::ExpectContains(expectation) => { + ExpectContains(expectation) => { // 先尝试拉取所有输出到「输出缓存」 while let Some(output) = vm.try_fetch_output()? { output_cache.put(output)?; } // 然后读取并匹配缓存 - let result = output_cache.for_each(|output| match expectation.matches(output) { - true => ControlFlow::Break(true), - false => ControlFlow::Continue(()), + let result = output_cache.for_each(|output| { + match expectation.matches(output, precision_epoch) { + true => ControlFlow::Break(true), + false => ControlFlow::Continue(()), + } })?; match result { // 只有匹配到了一个,才返回Ok @@ -127,7 +136,7 @@ pub fn put_nal( // } } // 检查在指定的「最大步数」内,是否有NAVM输出符合预期(弹性步数`0~最大步数`) - NALInput::ExpectCycle(max_cycles, step_cycles, step_duration, expectation) => { + ExpectCycle(max_cycles, step_cycles, step_duration, expectation) => { let mut cycles = 0; while cycles < max_cycles { // 推理步进 @@ -142,9 +151,11 @@ pub fn put_nal( output_cache.put(output)?; } // 然后读取并匹配缓存 - let result = output_cache.for_each(|output| match expectation.matches(output) { - true => ControlFlow::Break(true), - false => ControlFlow::Continue(()), + let result = output_cache.for_each(|output| { + match expectation.matches(output, precision_epoch) { + true => ControlFlow::Break(true), + false => ControlFlow::Continue(()), + } })?; // 匹配到一个⇒提前返回Ok if let Some(true) = result { @@ -158,7 +169,7 @@ pub fn put_nal( // 保存(所有)输出 // * 🚩输出到一个文本文件中 // * ✨复合JSON「对象数组」格式 - NALInput::SaveOutputs(path_str) => { + SaveOutputs(path_str) => { // 先收集所有输出的字符串 let mut file_str = "[".to_string(); output_cache.for_each(|output| { @@ -184,7 +195,7 @@ pub fn put_nal( Ok(()) } // 终止虚拟机 - NALInput::Terminate { + Terminate { if_not_user, result, } => { diff --git a/src/test_tools/vm_interact/narsese_expectation.rs b/src/test_tools/vm_interact/narsese_expectation.rs index a3b6a80..2a4578f 100644 --- a/src/test_tools/vm_interact/narsese_expectation.rs +++ b/src/test_tools/vm_interact/narsese_expectation.rs @@ -5,7 +5,7 @@ use super::term_equal::*; use anyhow::Result; use nar_dev_utils::if_return; use narsese::{ - api::NarseseValue, + api::{FloatPrecision, NarseseValue}, conversion::{ inter_type::lexical_fold::TryFoldInto, string::impl_enum::{format_instances::FORMAT_ASCII as FORMAT_ASCII_ENUM, NarseseFormat}, @@ -21,11 +21,19 @@ use util::macro_once; /// 判断「输出是否(在Narsese语义层面)符合预期」 /// * 🎯词法Narsese⇒枚举Narsese,以便从语义上判断 -pub fn is_expected_narsese_lexical(expected: &Narsese, out: &Narsese) -> bool { - _is_expected_narsese(expected.clone(), out.clone()) +pub fn is_expected_narsese_lexical( + expected: &Narsese, + out: &Narsese, + precision_epoch: FloatPrecision, +) -> bool { + _is_expected_narsese(expected.clone(), out.clone(), precision_epoch) } -fn _is_expected_narsese(mut expected: Narsese, mut out: Narsese) -> bool { +fn _is_expected_narsese( + mut expected: Narsese, + mut out: Narsese, + precision_epoch: FloatPrecision, +) -> bool { // 先比对词项 fn get_term_mut(narsese: &mut Narsese) -> &mut Term { use NarseseValue::*; @@ -40,13 +48,14 @@ fn _is_expected_narsese(mut expected: Narsese, mut out: Narsese) -> bool { } // * 🚩特制的「词项判等」截断性逻辑 | 🚩语义层面判等词项 if_return! { - !semantical_equal_mut(get_term_mut(&mut expected), get_term_mut(&mut out)) => false + !semantical_equal_mut(get_term_mut(&mut expected), get_term_mut(&mut out)) + => false }; // * 🚩折叠剩余部分,并开始判断 let fold = PartialFoldResult::try_from; match (fold(expected), fold(out)) { // * 🚩若均解析成功⇒进一步判等 - (Ok(expected), Ok(out)) => expected.is_expected_out(&out), + (Ok(expected), Ok(out)) => expected.is_expected_out(&out, precision_epoch), // * 🚩任一解析失败⇒直接失败 _ => false, } @@ -68,7 +77,7 @@ struct PartialFoldResult { /// * 🚩【2024-06-11 16:02:10】目前对「词项比对」使用特殊逻辑,而对其它结构照常比较 /// * ✅均已经考虑「没有值可判断」的情况 impl PartialFoldResult { - fn is_expected_out(&self, out: &Self) -> bool { + fn is_expected_out(&self, out: &Self, precision_epoch: FloatPrecision) -> bool { macro_once! { /// 一系列针对Option解包的条件判断: /// * 🚩均为Some⇒展开内部代码逻辑 @@ -121,13 +130,13 @@ impl PartialFoldResult { // 真值一致 expected @ self.truth, out @ out.truth => - is_expected_truth(expected, out) // * 🚩特殊情况(需兼容)特殊处理 + is_expected_truth(expected, out, precision_epoch) // * 🚩特殊情况(需兼容)特殊处理 } && { @EMPTY_WILDCARD // ! 空值通配 // 预算值一致 expected @ self.budget, out @ out.budget => - is_expected_budget(expected, out) // * 🚩特殊情况(需兼容)特殊处理 + is_expected_budget(expected, out, precision_epoch) // * 🚩特殊情况(需兼容)特殊处理 } } } @@ -188,17 +197,43 @@ impl TryFrom for PartialFoldResult { } } +/// 判断「短浮点之间是否相等」(在指定精度范围内) +/// * 🎯应对不同小数精度的NARS输出,统一在某精度内相等 +/// * 🚩【2024-08-01 10:36:31】需要引入配置 +/// * 📝|expected - out| ≤ precision_epoch +fn is_expected_float( + expected: &FloatPrecision, + out: &FloatPrecision, + precision_epoch: FloatPrecision, +) -> bool { + // * 🚩精度=0 ⇒ 直接判等 + if precision_epoch == 0.0 { + return expected == out; + } + // * 🚩其它 ⇒ 绝对值小于等于 + (expected - out).abs() <= precision_epoch +} + /// 判断「输出是否在真值层面符合预期」 /// * 🎯空真值的语句,应该符合「固定真值的语句」的预期——相当于「通配符」 #[inline] -fn is_expected_truth(expected: &EnumTruth, out: &EnumTruth) -> bool { +fn is_expected_truth( + expected: &EnumTruth, + out: &EnumTruth, + precision_epoch: FloatPrecision, +) -> bool { match (expected, out) { // 预期空真值⇒通配 (EnumTruth::Empty, ..) => true, - // 预期单真值 - (EnumTruth::Single(f_e), EnumTruth::Single(f_o) | EnumTruth::Double(f_o, ..)) => f_e == f_o, + // 预期单真值⇒部分通配 + (EnumTruth::Single(f_e), EnumTruth::Single(f_o) | EnumTruth::Double(f_o, ..)) => { + is_expected_float(f_e, f_o, precision_epoch) + } // 预期双真值 - (EnumTruth::Double(..), EnumTruth::Double(..)) => expected == out, + (EnumTruth::Double(f_e, c_e), EnumTruth::Double(f_o, c_o)) => { + is_expected_float(f_e, f_o, precision_epoch) + && is_expected_float(c_e, c_o, precision_epoch) + } // 其它情况 _ => false, } @@ -207,7 +242,11 @@ fn is_expected_truth(expected: &EnumTruth, out: &EnumTruth) -> bool { /// 判断「输出是否在预算值层面符合预期」 /// * 🎯空预算的语句,应该符合「固定预算值的语句」的预期——相当于「通配符」 #[inline] -fn is_expected_budget(expected: &EnumBudget, out: &EnumBudget) -> bool { +fn is_expected_budget( + expected: &EnumBudget, + out: &EnumBudget, + precision_epoch: FloatPrecision, +) -> bool { match (expected, out) { // 预期空预算⇒通配 (EnumBudget::Empty, ..) => true, @@ -215,14 +254,21 @@ fn is_expected_budget(expected: &EnumBudget, out: &EnumBudget) -> bool { ( EnumBudget::Single(p_e), EnumBudget::Single(p_o) | EnumBudget::Double(p_o, ..) | EnumBudget::Triple(p_o, ..), - ) => p_e == p_o, + ) => is_expected_float(p_e, p_o, precision_epoch), // 预期双预算 ( EnumBudget::Double(p_e, d_e), EnumBudget::Double(p_o, d_o) | EnumBudget::Triple(p_o, d_o, ..), - ) => p_e == p_o && d_e == d_o, + ) => { + is_expected_float(p_e, p_o, precision_epoch) + && is_expected_float(d_e, d_o, precision_epoch) + } // 预期三预算 - (EnumBudget::Triple(..), EnumBudget::Triple(..)) => expected == out, + (EnumBudget::Triple(p_e, d_e, q_e), EnumBudget::Triple(p_o, d_o, q_o)) => { + is_expected_float(p_e, p_o, precision_epoch) + && is_expected_float(d_e, d_o, precision_epoch) + && is_expected_float(q_e, q_o, precision_epoch) + } // 其它情况 _ => false, } @@ -253,60 +299,82 @@ mod tests { #[test] fn is_expected_narsese_lexical() { + /// 正例断言带精度 + fn test(expected: Narsese, out: Narsese, precision_epoch: FloatPrecision) { + assert!( + super::is_expected_narsese_lexical(&expected, &out, precision_epoch), + "正例断言失败!\nexpected: {expected:?}, out: {out:?}" + ); + } + /// 反例断言带精度 + fn test_negative(expected: Narsese, out: Narsese, precision_epoch: FloatPrecision) { + assert!( + !super::is_expected_narsese_lexical(&expected, &out, precision_epoch), + "反例断言失败!\nexpected: {expected:?}, out: {out:?}" + ); + } // * 🚩正例 macro_once! { - macro test($($expected:expr => $out:expr $(,)?)*) { - $( - let expected = nse!($expected); - let out = nse!($out); - assert!( - super::is_expected_narsese_lexical(&expected, &out), - "正例断言失败!\nexpected: {expected:?}, out: {out:?}" - ); - )* + macro test { + ( // 分派&展开 + $($expected:literal ==$config:tt== $out:literal $(,)?)* + ) => { + $( + test!(@SPECIFIC $expected, $out, $config); + )* + } + ( // 正例 + @SPECIFIC + $expected:literal, + $out:literal, + {$epoch:literal} + ) => { + test(nse!($expected), nse!($out), $epoch) + } + ( // 反例 + @SPECIFIC + $expected:literal, + $out:literal, + {! $epoch:literal} + ) => { + test_negative(nse!($expected), nse!($out), $epoch) + } } + // * 🚩正例 // 常规词项、语句、任务 - "A" => "A", - "A." => "A.", - "A?" => "A?", - "A! %1.0;0.9%" => "A! %1.0;0.9%" - "$0.5;0.5;0.5$ A@" => "$0.5;0.5;0.5$ A@", - "$0.5;0.5;0.5$ A. %1.0;0.9%" => "$0.5;0.5;0.5$ A. %1.0;0.9%", + "A" =={0.0}== "A", + "A." =={0.0}== "A.", + "A?" =={0.0}== "A?", + "A! %1.0;0.9%" =={0.0}== "A! %1.0;0.9%" + "$0.5;0.5;0.5$ A@" =={0.0}== "$0.5;0.5;0.5$ A@", + "$0.5;0.5;0.5$ A. %1.0;0.9%" =={0.0}== "$0.5;0.5;0.5$ A. %1.0;0.9%", // 真值通配 - "A." => "A. %1.0;0.9%", - "A!" => "A! %1.0;0.9%", + "A." =={0.0}== "A. %1.0;0.9%", + "A!" =={0.0}== "A! %1.0;0.9%", // 预算值通配 - "A." => "$0.5;0.5;0.5$ A.", - "A!" => "$0.5;0.5;0.5$ A!", - "A." => "$0.5;0.5;0.5$ A. %1.0;0.9%", - "A!" => "$0.5;0.5;0.5$ A! %1.0;0.9%", + "A." =={0.0}== "$0.5;0.5;0.5$ A.", + "A!" =={0.0}== "$0.5;0.5;0.5$ A!", + "A." =={0.0}== "$0.5;0.5;0.5$ A. %1.0;0.9%", + "A!" =={0.0}== "$0.5;0.5;0.5$ A! %1.0;0.9%", + // TODO: 真值精度内匹配 + // TODO: 预算值精度内匹配 // 源自实际应用 "<(&&,<$1 --> lock>,<$2 --> key>) ==> <$1 --> (/,open,$2,_)>>. %1.00;0.45%" - => "<(&&,<$1 --> key>,<$2 --> lock>) ==> <$2 --> (/,open,$1,_)>>. %1.00;0.45%" - } - // * 🚩反例 - macro_once! { - macro test($($expected:literal != $out:literal $(,)?)*) { - $( - let expected = nse!($expected); - let out = nse!($out); - assert!( - !super::is_expected_narsese_lexical(&expected, &out), - "反例断言失败!\nexpected: {expected:?}, out: {out:?}" - ); - )* - } - "A" != "B", - "A." != "A?", - "A?" != " B>?", + =={0.0}== "<(&&,<$1 --> key>,<$2 --> lock>) ==> <$2 --> (/,open,$1,_)>>. %1.00;0.45%" + // * 🚩反例 + "A" =={!0.0}== "B", + "A." =={!0.0}== "A?", + "A?" =={!0.0}== " B>?", // 真值通配(反向就不行) - "A. %1.0;0.9%" != "A.", - "A! %1.0;0.9%" != "A!", + "A. %1.0;0.9%" =={!0.0}== "A.", + "A! %1.0;0.9%" =={!0.0}== "A!", // 预算值通配(反向就不行) - "$0.5;0.5;0.5$ A." != "A.", - "$0.5;0.5;0.5$ A!" != "A!", - "$0.5;0.5;0.5$ A. %1.0;0.9%" != "A.", - "$0.5;0.5;0.5$ A! %1.0;0.9%" != "A!", + "$0.5;0.5;0.5$ A." =={!0.0}== "A.", + "$0.5;0.5;0.5$ A!" =={!0.0}== "A!", + "$0.5;0.5;0.5$ A. %1.0;0.9%" =={!0.0}== "A.", + "$0.5;0.5;0.5$ A! %1.0;0.9%" =={!0.0}== "A!", + // TODO: 真值精度内失配 + // TODO: 预算值精度内失配 } }