Add Monte Carlo (MCM) acceptance evaluation
Add a 10000-iteration Monte Carlo simulation on top of the existing GUM uncertainty path. For IRa and Ir it reports the simulated mean, standard deviation, P2.5/P97.5 (95% coverage interval), and pass/fail probability against configurable standard values (default IRa/Ir <= 1.0). - src/mcm.rs: dependency-free SplitMix64 PRNG + Box-Muller normals, deterministic seed for reproducible results - domain: AcceptanceLimits, McmResult, McmIndexStats; wire into SampleInput/CalculationResult - UI: compact calibration table, standard-value inputs, side-by-side GUM/MCM result cards, and a placeholder before calculation - run.md: run and packaging instructions Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1080c1b03
commit
0c06d51c4a
|
|
@ -0,0 +1,105 @@
|
|||
# 运行与打包说明
|
||||
|
||||
建筑材料放射性判定系统(Rust + Tauri + React + TypeScript)。
|
||||
|
||||
## 1. 环境要求
|
||||
|
||||
| 工具 | 版本 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Node.js | ≥ 18 | 前端构建(Vite) |
|
||||
| npm | 随 Node 安装 | 依赖管理 |
|
||||
| Rust | ≥ 1.78(已验证 1.93) | 计算引擎与 Tauri 后端,`rustup` 安装 |
|
||||
| Tauri 前置依赖 | Windows: WebView2 Runtime(Win11 已内置) | 见 <https://tauri.app/start/prerequisites/> |
|
||||
|
||||
首次准备:
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
```
|
||||
|
||||
> Rust 依赖在首次 `cargo`/`tauri` 命令时自动下载编译。
|
||||
|
||||
## 2. 开发运行
|
||||
|
||||
### 2.1 仅前端(浏览器预览,不含 Tauri 原生命令)
|
||||
|
||||
```powershell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
打开 <http://localhost:1420>。注意:此模式下 `invoke("calculate")` 不可用,仅用于调样式。
|
||||
|
||||
### 2.2 完整桌面应用(推荐)
|
||||
|
||||
```powershell
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
该命令会自动启动 Vite 开发服务器并编译 Rust 后端,弹出桌面窗口,支持热重载。
|
||||
|
||||
## 3. 测试
|
||||
|
||||
计算引擎(含蒙特卡洛仿真)的单元/集成测试:
|
||||
|
||||
```powershell
|
||||
cargo test -p ceramic-radioactivity
|
||||
```
|
||||
|
||||
前端类型检查与生产构建:
|
||||
|
||||
```powershell
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 4. 打包(生成 Windows 安装包)
|
||||
|
||||
1. 在 `src-tauri/tauri.conf.json` 中将 `bundle.active` 设为 `true`:
|
||||
|
||||
```json
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis", "msi"],
|
||||
"icon": ["icons/icon.ico"]
|
||||
}
|
||||
```
|
||||
|
||||
- `nsis` 生成 `.exe` 安装包,`msi` 生成 `.msi`(需安装 WiX Toolset,可只保留 `nsis`)。
|
||||
- `icon` 需指向有效图标;可用 `npx tauri icon path\to\logo.png` 生成整套图标。
|
||||
|
||||
2. 执行构建:
|
||||
|
||||
```powershell
|
||||
npm run tauri:build
|
||||
```
|
||||
|
||||
3. 产物输出目录:
|
||||
|
||||
```
|
||||
src-tauri/target/release/ # 可执行文件 建筑材料放射性判定.exe
|
||||
src-tauri/target/release/bundle/nsis/ # NSIS 安装包
|
||||
src-tauri/target/release/bundle/msi/ # MSI 安装包(如启用)
|
||||
```
|
||||
|
||||
> 首次 release 构建较慢(需编译全部 Rust 依赖),后续增量构建会显著加快。
|
||||
|
||||
## 5. 项目结构
|
||||
|
||||
```
|
||||
src/ 计算引擎(库 crate:ceramic-radioactivity)
|
||||
├─ domain.rs 输入/输出/校准/判定/MCM 数据结构
|
||||
├─ calculator.rs GUM 法不确定度计算与三态判定
|
||||
└─ mcm.rs 蒙特卡洛法仿真(10000 次,含 PRNG)
|
||||
tests/ 计算引擎测试
|
||||
src-tauri/ Tauri 桌面外壳(command: calculate)
|
||||
ui/ React + Ant Design 前端
|
||||
run.md 本说明
|
||||
```
|
||||
|
||||
## 6. 功能说明(计算与判定)
|
||||
|
||||
- **GUM 法**:A 类(标准差 / 极差法)、B 类不确定度合成,输出 IRa、Ir 及相对不确定度。
|
||||
- `ur ≤ 20%` → `OK`;否则次数 < 6 → `请增加试验次数至 6 次`;否则 → `校准仪器后重新测量`。
|
||||
- **蒙特卡洛法(MCM)**:对各核素校准比活度按正态分布抽样,10000 次仿真传播至 IRa、Ir,
|
||||
输出**平均值、标准偏差、P2.5、P97.5**(95% 包含区间),并与**标准值**比较给出**合格 / 不合格概率**。
|
||||
- 标准值(IRa、Ir 限值)可在界面“校准参数 / 判定标准值”中修改,默认均为 `1.0`(GB 6566 主体材料)。
|
||||
- 仿真使用固定随机种子,结果可复现。
|
||||
|
|
@ -2,6 +2,7 @@ use crate::domain::{
|
|||
CalculationError, CalculationResult, Conclusion, IndexResult, NuclideMeasurements,
|
||||
NuclideResult, SampleInput,
|
||||
};
|
||||
use crate::mcm::run_monte_carlo;
|
||||
|
||||
const ACCEPTANCE_LIMIT_PERCENT: f64 = 20.0;
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ pub fn calculate_sample(input: SampleInput) -> Result<CalculationResult, Calcula
|
|||
Conclusion::RecalibrateInstrument
|
||||
};
|
||||
|
||||
let mcm = run_monte_carlo(&ra, &th, &k, &input.limits);
|
||||
|
||||
Ok(CalculationResult {
|
||||
measurement_count: n,
|
||||
ra,
|
||||
|
|
@ -33,6 +36,7 @@ pub fn calculate_sample(input: SampleInput) -> Result<CalculationResult, Calcula
|
|||
ira,
|
||||
ir,
|
||||
conclusion,
|
||||
mcm,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,25 @@ pub struct SampleInput {
|
|||
pub ra: NuclideMeasurements,
|
||||
pub th: NuclideMeasurements,
|
||||
pub k: NuclideMeasurements,
|
||||
/// 合格判定标准值(限值)。前端可省略,默认 IRa ≤ 1.0、Ir ≤ 1.0(GB 6566 主体材料)。
|
||||
#[serde(default)]
|
||||
pub limits: AcceptanceLimits,
|
||||
}
|
||||
|
||||
/// 内照射指数 IRa 与外照射指数 Ir 的合格判定标准值(限值)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AcceptanceLimits {
|
||||
pub ira_limit: f64,
|
||||
pub ir_limit: f64,
|
||||
}
|
||||
|
||||
impl Default for AcceptanceLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ira_limit: 1.0,
|
||||
ir_limit: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -32,6 +51,42 @@ pub struct CalculationResult {
|
|||
pub ira: IndexResult,
|
||||
pub ir: IndexResult,
|
||||
pub conclusion: Conclusion,
|
||||
/// 蒙特卡洛法(MCM)仿真结果。
|
||||
pub mcm: McmResult,
|
||||
}
|
||||
|
||||
/// 蒙特卡洛法(MCM)整体仿真结果。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct McmResult {
|
||||
/// 仿真次数(默认 10000)。
|
||||
pub iterations: usize,
|
||||
/// 内照射指数 IRa 的仿真统计。
|
||||
pub ira: McmIndexStats,
|
||||
/// 外照射指数 Ir 的仿真统计。
|
||||
pub ir: McmIndexStats,
|
||||
/// 综合合格概率:每次仿真中 IRa 与 Ir 同时不超过各自标准值的比例。
|
||||
pub overall_pass_probability: f64,
|
||||
/// 综合不合格概率 = 1 - overall_pass_probability。
|
||||
pub overall_fail_probability: f64,
|
||||
}
|
||||
|
||||
/// 单个指数的 MCM 仿真统计量。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct McmIndexStats {
|
||||
/// 仿真平均值。
|
||||
pub mean: f64,
|
||||
/// 仿真标准偏差。
|
||||
pub std_dev: f64,
|
||||
/// 2.5 百分位(95% 包含区间下限)。
|
||||
pub p2_5: f64,
|
||||
/// 97.5 百分位(95% 包含区间上限)。
|
||||
pub p97_5: f64,
|
||||
/// 用于比较的标准值(限值)。
|
||||
pub standard_value: f64,
|
||||
/// 合格概率:仿真值不超过标准值的比例。
|
||||
pub pass_probability: f64,
|
||||
/// 不合格概率 = 1 - pass_probability。
|
||||
pub fail_probability: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
mod calculator;
|
||||
mod domain;
|
||||
mod mcm;
|
||||
|
||||
pub use calculator::calculate_sample;
|
||||
pub use domain::{
|
||||
CalculationError, CalculationResult, CalibrationParams, Conclusion, IndexResult,
|
||||
NuclideMeasurements, NuclideResult, SampleInput,
|
||||
AcceptanceLimits, CalculationError, CalculationResult, CalibrationParams, Conclusion,
|
||||
IndexResult, McmIndexStats, McmResult, NuclideMeasurements, NuclideResult, SampleInput,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
//! 蒙特卡洛法(MCM)不确定度传播与合格概率评估。
|
||||
//!
|
||||
//! 按 JJF 1059.2 的思路,对内照射指数 IRa 与外照射指数 Ir 进行 10000 次仿真:
|
||||
//! 每次仿真从各核素校准比活度的分布中抽样,经测量模型传播后得到 IRa、Ir 的样本,
|
||||
//! 进而给出仿真平均值、标准偏差、95% 包含区间(P2.5、P97.5),并与标准值比较给出合格概率。
|
||||
//!
|
||||
//! 每个核素的校准比活度按正态分布 `N(mean_calibrated, combined_uncertainty)` 抽样,
|
||||
//! 其中均值与标准(合成)不确定度直接复用 GUM 法已算出的 `NuclideResult`,
|
||||
//! 因此 MCM 的均值/标准偏差与 GUM 结果保持一致,而包含区间与合格概率由仿真分布直接得到。
|
||||
|
||||
use crate::domain::{AcceptanceLimits, McmIndexStats, McmResult, NuclideResult};
|
||||
|
||||
/// 默认仿真次数。
|
||||
pub const DEFAULT_ITERATIONS: usize = 10_000;
|
||||
|
||||
/// 固定随机种子,保证同一输入得到可复现的仿真结果(便于出具一致的报告与测试)。
|
||||
const SEED: u64 = 0x853C_49E6_748F_EA9B;
|
||||
|
||||
/// 对给定的核素结果与标准值执行 MCM 仿真。
|
||||
pub fn run_monte_carlo(
|
||||
ra: &NuclideResult,
|
||||
th: &NuclideResult,
|
||||
k: &NuclideResult,
|
||||
limits: &AcceptanceLimits,
|
||||
) -> McmResult {
|
||||
let iterations = DEFAULT_ITERATIONS;
|
||||
let mut rng = SplitMix64::new(SEED);
|
||||
|
||||
let mut ira_samples = Vec::with_capacity(iterations);
|
||||
let mut ir_samples = Vec::with_capacity(iterations);
|
||||
let mut overall_pass = 0usize;
|
||||
|
||||
for _ in 0..iterations {
|
||||
let c_ra = sample_normal(&mut rng, ra.mean_calibrated, ra.combined_uncertainty);
|
||||
let c_th = sample_normal(&mut rng, th.mean_calibrated, th.combined_uncertainty);
|
||||
let c_k = sample_normal(&mut rng, k.mean_calibrated, k.combined_uncertainty);
|
||||
|
||||
let ira = c_ra / 200.0;
|
||||
let ir = c_ra / 370.0 + c_th / 260.0 + c_k / 4200.0;
|
||||
|
||||
if ira <= limits.ira_limit && ir <= limits.ir_limit {
|
||||
overall_pass += 1;
|
||||
}
|
||||
|
||||
ira_samples.push(ira);
|
||||
ir_samples.push(ir);
|
||||
}
|
||||
|
||||
let ira_stats = summarize(&mut ira_samples, limits.ira_limit);
|
||||
let ir_stats = summarize(&mut ir_samples, limits.ir_limit);
|
||||
let overall_pass_probability = overall_pass as f64 / iterations as f64;
|
||||
|
||||
McmResult {
|
||||
iterations,
|
||||
ira: ira_stats,
|
||||
ir: ir_stats,
|
||||
overall_pass_probability,
|
||||
overall_fail_probability: 1.0 - overall_pass_probability,
|
||||
}
|
||||
}
|
||||
|
||||
/// 由样本计算统计量。会就地排序 `samples` 以取百分位。
|
||||
fn summarize(samples: &mut [f64], standard_value: f64) -> McmIndexStats {
|
||||
let n = samples.len();
|
||||
let mean = samples.iter().sum::<f64>() / n as f64;
|
||||
let variance = samples
|
||||
.iter()
|
||||
.map(|value| (value - mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ (n as f64 - 1.0);
|
||||
let std_dev = variance.sqrt();
|
||||
|
||||
let pass = samples.iter().filter(|&&value| value <= standard_value).count();
|
||||
let pass_probability = pass as f64 / n as f64;
|
||||
|
||||
samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let p2_5 = percentile(samples, 0.025);
|
||||
let p97_5 = percentile(samples, 0.975);
|
||||
|
||||
McmIndexStats {
|
||||
mean,
|
||||
std_dev,
|
||||
p2_5,
|
||||
p97_5,
|
||||
standard_value,
|
||||
pass_probability,
|
||||
fail_probability: 1.0 - pass_probability,
|
||||
}
|
||||
}
|
||||
|
||||
/// 已排序样本的线性插值百分位(p 取 0..=1)。
|
||||
fn percentile(sorted: &[f64], p: f64) -> f64 {
|
||||
let n = sorted.len();
|
||||
if n == 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
if n == 1 {
|
||||
return sorted[0];
|
||||
}
|
||||
let rank = p * (n as f64 - 1.0);
|
||||
let lower = rank.floor() as usize;
|
||||
let upper = rank.ceil() as usize;
|
||||
let weight = rank - lower as f64;
|
||||
sorted[lower] * (1.0 - weight) + sorted[upper] * weight
|
||||
}
|
||||
|
||||
/// 由标准正态抽样得到 `N(mean, std_dev)` 样本(std_dev 为 0 时退化为常数)。
|
||||
fn sample_normal(rng: &mut SplitMix64, mean: f64, std_dev: f64) -> f64 {
|
||||
if std_dev == 0.0 {
|
||||
return mean;
|
||||
}
|
||||
mean + std_dev * rng.next_standard_normal()
|
||||
}
|
||||
|
||||
/// SplitMix64 伪随机数发生器:无外部依赖、确定性、统计性质良好。
|
||||
struct SplitMix64 {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl SplitMix64 {
|
||||
fn new(seed: u64) -> Self {
|
||||
Self { state: seed }
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = self.state;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// 返回 [0, 1) 区间内的均匀分布样本。
|
||||
fn next_uniform(&mut self) -> f64 {
|
||||
// 取高 53 位映射到 [0, 1),与 f64 尾数精度一致。
|
||||
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
|
||||
}
|
||||
|
||||
/// Box-Muller 变换生成标准正态样本。
|
||||
fn next_standard_normal(&mut self) -> f64 {
|
||||
// 避免 ln(0):将 u1 下限钳到一个极小正数。
|
||||
let u1 = self.next_uniform().max(f64::MIN_POSITIVE);
|
||||
let u2 = self.next_uniform();
|
||||
(-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use ceramic_radioactivity::{
|
||||
calculate_sample, CalibrationParams, Conclusion, NuclideMeasurements, SampleInput,
|
||||
calculate_sample, AcceptanceLimits, CalibrationParams, Conclusion, NuclideMeasurements,
|
||||
SampleInput,
|
||||
};
|
||||
|
||||
fn default_input() -> SampleInput {
|
||||
|
|
@ -28,6 +29,10 @@ fn default_input() -> SampleInput {
|
|||
coverage_factor: 2.0,
|
||||
},
|
||||
},
|
||||
limits: AcceptanceLimits {
|
||||
ira_limit: 1.0,
|
||||
ir_limit: 1.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +85,62 @@ fn rejects_mismatched_measurement_counts() {
|
|||
assert!(err.to_string().contains("same measurement count"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monte_carlo_matches_analytical_mean_and_uncertainty() {
|
||||
let result = calculate_sample(default_input()).expect("valid sample should calculate");
|
||||
|
||||
assert_eq!(result.mcm.iterations, 10_000);
|
||||
|
||||
// MCM 仿真均值/标准偏差应与 GUM 解析结果一致(仿真随机误差在 ~1% 量级)。
|
||||
assert_close(result.mcm.ira.mean, result.ira.value, 5e-4);
|
||||
assert_close(result.mcm.ir.mean, result.ir.value, 5e-4);
|
||||
assert_close(
|
||||
result.mcm.ira.std_dev,
|
||||
result.ira.standard_uncertainty,
|
||||
result.ira.standard_uncertainty * 0.05,
|
||||
);
|
||||
assert_close(
|
||||
result.mcm.ir.std_dev,
|
||||
result.ir.standard_uncertainty,
|
||||
result.ir.standard_uncertainty * 0.05,
|
||||
);
|
||||
|
||||
// 95% 包含区间应包住均值。
|
||||
assert!(result.mcm.ira.p2_5 < result.mcm.ira.mean);
|
||||
assert!(result.mcm.ira.mean < result.mcm.ira.p97_5);
|
||||
|
||||
// 默认样本的 IRa≈0.46、Ir≈0.75 都远低于标准值 1.0,合格概率应为 1。
|
||||
assert_close(result.mcm.ira.pass_probability, 1.0, 1e-12);
|
||||
assert_close(result.mcm.ir.pass_probability, 1.0, 1e-12);
|
||||
assert_close(result.mcm.overall_pass_probability, 1.0, 1e-12);
|
||||
assert_close(result.mcm.overall_fail_probability, 0.0, 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monte_carlo_is_deterministic_for_same_input() {
|
||||
let first = calculate_sample(default_input()).expect("valid sample should calculate");
|
||||
let second = calculate_sample(default_input()).expect("valid sample should calculate");
|
||||
|
||||
assert_eq!(first.mcm, second.mcm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monte_carlo_gives_about_half_pass_probability_when_limit_equals_mean() {
|
||||
let mut input = default_input();
|
||||
let analytical = calculate_sample(input.clone()).expect("valid sample should calculate");
|
||||
|
||||
// 将 IRa 标准值设为其均值,合格概率应接近 0.5。
|
||||
input.limits.ira_limit = analytical.ira.value;
|
||||
let result = calculate_sample(input).expect("valid sample should calculate");
|
||||
|
||||
assert_close(result.mcm.ira.pass_probability, 0.5, 0.03);
|
||||
assert_close(
|
||||
result.mcm.ira.fail_probability,
|
||||
1.0 - result.mcm.ira.pass_probability,
|
||||
1e-12,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_close(actual: f64, expected: f64, tolerance: f64) {
|
||||
assert!(
|
||||
(actual - expected).abs() <= tolerance,
|
||||
|
|
|
|||
216
ui/src/App.tsx
216
ui/src/App.tsx
|
|
@ -1,16 +1,6 @@
|
|||
import { useMemo, useRef, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
ConfigProvider,
|
||||
Descriptions,
|
||||
Form,
|
||||
InputNumber,
|
||||
Table,
|
||||
Tag
|
||||
} from "antd";
|
||||
import { Alert, Button, Card, ConfigProvider, InputNumber, Table, Tag } from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
|
||||
type Conclusion = "Ok" | "IncreaseMeasurementsToSix" | "RecalibrateInstrument";
|
||||
|
|
@ -26,10 +16,34 @@ type NuclideMeasurements = {
|
|||
calibration: CalibrationParams;
|
||||
};
|
||||
|
||||
type AcceptanceLimits = {
|
||||
ira_limit: number;
|
||||
ir_limit: number;
|
||||
};
|
||||
|
||||
type SampleInput = {
|
||||
ra: NuclideMeasurements;
|
||||
th: NuclideMeasurements;
|
||||
k: NuclideMeasurements;
|
||||
limits: AcceptanceLimits;
|
||||
};
|
||||
|
||||
type McmIndexStats = {
|
||||
mean: number;
|
||||
std_dev: number;
|
||||
p2_5: number;
|
||||
p97_5: number;
|
||||
standard_value: number;
|
||||
pass_probability: number;
|
||||
fail_probability: number;
|
||||
};
|
||||
|
||||
type McmResult = {
|
||||
iterations: number;
|
||||
ira: McmIndexStats;
|
||||
ir: McmIndexStats;
|
||||
overall_pass_probability: number;
|
||||
overall_fail_probability: number;
|
||||
};
|
||||
|
||||
type NuclideResult = {
|
||||
|
|
@ -56,6 +70,7 @@ type CalculationResult = {
|
|||
ira: IndexResult;
|
||||
ir: IndexResult;
|
||||
conclusion: Conclusion;
|
||||
mcm: McmResult;
|
||||
};
|
||||
|
||||
type MeasurementRow = {
|
||||
|
|
@ -67,6 +82,10 @@ type MeasurementRow = {
|
|||
|
||||
type ResultRow = { name: string } & NuclideResult;
|
||||
|
||||
type McmRow = { name: string } & McmIndexStats;
|
||||
|
||||
type CalibrationRow = { key: string; name: string } & CalibrationParams;
|
||||
|
||||
type FocusableInput = {
|
||||
focus: () => void;
|
||||
};
|
||||
|
|
@ -77,6 +96,8 @@ const defaultCalibration = {
|
|||
k: { factor: 0.961, expanded_uncertainty_percent: 6.7, coverage_factor: 2 }
|
||||
};
|
||||
|
||||
const defaultLimits: AcceptanceLimits = { ira_limit: 1.0, ir_limit: 1.0 };
|
||||
|
||||
const initialRows: MeasurementRow[] = [
|
||||
{ key: 1, ra: 100, th: 110, k: 560 },
|
||||
{ key: 2, ra: 102, th: 111, k: 565 },
|
||||
|
|
@ -97,8 +118,14 @@ function formatNumber(value: number, digits = 4) {
|
|||
return value.toFixed(digits);
|
||||
}
|
||||
|
||||
function formatPercent(value: number, digits = 2) {
|
||||
if (!Number.isFinite(value)) return "-";
|
||||
return (value * 100).toFixed(digits) + "%";
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [rows, setRows] = useState<MeasurementRow[]>(initialRows);
|
||||
const [limits, setLimits] = useState<AcceptanceLimits>(defaultLimits);
|
||||
const [result, setResult] = useState<CalculationResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -129,7 +156,8 @@ function App() {
|
|||
return {
|
||||
ra: { measured_values: toValues("ra"), calibration: defaultCalibration.ra },
|
||||
th: { measured_values: toValues("th"), calibration: defaultCalibration.th },
|
||||
k: { measured_values: toValues("k"), calibration: defaultCalibration.k }
|
||||
k: { measured_values: toValues("k"), calibration: defaultCalibration.k },
|
||||
limits
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -221,53 +249,137 @@ function App() {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="校准参数" className="panel compact-panel">
|
||||
<Form layout="vertical">
|
||||
{Object.entries(defaultCalibration).map(([key, calibration]) => (
|
||||
<Descriptions key={key} bordered size="small" column={1} className="calibration-block">
|
||||
<Descriptions.Item label={key.toUpperCase()}>{calibration.factor}</Descriptions.Item>
|
||||
<Descriptions.Item label="U">{calibration.expanded_uncertainty_percent}%</Descriptions.Item>
|
||||
<Descriptions.Item label="k">{calibration.coverage_factor}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
))}
|
||||
</Form>
|
||||
<Card title="校准参数 / 判定标准值" className="panel compact-panel">
|
||||
<Table<CalibrationRow>
|
||||
className="calibration-table"
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowKey="key"
|
||||
dataSource={[
|
||||
{ key: "ra", name: "Ra", ...defaultCalibration.ra },
|
||||
{ key: "th", name: "Th", ...defaultCalibration.th },
|
||||
{ key: "k", name: "K", ...defaultCalibration.k }
|
||||
]}
|
||||
columns={[
|
||||
{ title: "核素", dataIndex: "name", align: "center" },
|
||||
{ title: "a", dataIndex: "factor", align: "center" },
|
||||
{
|
||||
title: "U",
|
||||
dataIndex: "expanded_uncertainty_percent",
|
||||
align: "center",
|
||||
render: (value: number) => `${value}%`
|
||||
},
|
||||
{ title: "k", dataIndex: "coverage_factor", align: "center" }
|
||||
]}
|
||||
/>
|
||||
<div className="limits-form">
|
||||
<div className="limit-field">
|
||||
<span className="limit-label">IRa 标准值</span>
|
||||
<InputNumber
|
||||
value={limits.ira_limit}
|
||||
min={0}
|
||||
step={0.1}
|
||||
onChange={(value) => setLimits((current) => ({ ...current, ira_limit: value ?? 0 }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="limit-field">
|
||||
<span className="limit-label">Ir 标准值</span>
|
||||
<InputNumber
|
||||
value={limits.ir_limit}
|
||||
min={0}
|
||||
step={0.1}
|
||||
onChange={(value) => setLimits((current) => ({ ...current, ir_limit: value ?? 0 }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{error ? <Alert type="error" message={error} showIcon /> : null}
|
||||
|
||||
{result ? (
|
||||
<Card title="计算结果" className="panel">
|
||||
<div className="result-grid">
|
||||
<ResultTile title="IRa" value={result.ira.value} uncertainty={result.ira.relative_uncertainty_percent} />
|
||||
<ResultTile title="Ir" value={result.ir.value} uncertainty={result.ir.relative_uncertainty_percent} />
|
||||
<div className="result-tile conclusion-tile">
|
||||
<span>判定</span>
|
||||
<Tag color={result.conclusion === "Ok" ? "success" : "warning"}>
|
||||
{conclusionText[result.conclusion]}
|
||||
</Tag>
|
||||
<div className="results-area">
|
||||
{result ? (
|
||||
<div className="results-row">
|
||||
<Card title="计算结果(GUM 法)" className="panel">
|
||||
<div className="result-grid">
|
||||
<ResultTile title="IRa" value={result.ira.value} uncertainty={result.ira.relative_uncertainty_percent} />
|
||||
<ResultTile title="Ir" value={result.ir.value} uncertainty={result.ir.relative_uncertainty_percent} />
|
||||
<div className="result-tile conclusion-tile">
|
||||
<span>判定</span>
|
||||
<Tag color={result.conclusion === "Ok" ? "success" : "warning"}>
|
||||
{conclusionText[result.conclusion]}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Table<ResultRow>
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
size="small"
|
||||
dataSource={[
|
||||
{ name: "Ra-226", ...result.ra },
|
||||
{ name: "Th-232", ...result.th },
|
||||
{ name: "K-40", ...result.k }
|
||||
]}
|
||||
columns={[
|
||||
{ title: "核素", dataIndex: "name" },
|
||||
{ title: "均值", dataIndex: "mean_measured", render: (value: number) => formatNumber(value) },
|
||||
{ title: "校准活度", dataIndex: "mean_calibrated", render: (value: number) => formatNumber(value) },
|
||||
{ title: "A 类", dataIndex: "type_a_uncertainty", render: (value: number) => formatNumber(value) },
|
||||
{ title: "B 类相对", dataIndex: "type_b_relative", render: (value) => formatNumber(value * 100, 3) + "%" },
|
||||
{ title: "合成", dataIndex: "combined_uncertainty", render: (value: number) => formatNumber(value) }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title={`蒙特卡洛仿真(MCM,${result.mcm.iterations} 次)`} className="panel">
|
||||
<div className="result-grid">
|
||||
<div className="result-tile">
|
||||
<span>综合合格概率</span>
|
||||
<strong>{formatPercent(result.mcm.overall_pass_probability)}</strong>
|
||||
<small>IRa 与 Ir 同时合格</small>
|
||||
</div>
|
||||
<div className="result-tile">
|
||||
<span>综合不合格概率</span>
|
||||
<strong>{formatPercent(result.mcm.overall_fail_probability)}</strong>
|
||||
<small>任一指数超标</small>
|
||||
</div>
|
||||
<div className="result-tile conclusion-tile">
|
||||
<span>仿真判定</span>
|
||||
<Tag color={result.mcm.overall_pass_probability >= 0.95 ? "success" : "warning"}>
|
||||
{result.mcm.overall_pass_probability >= 0.95 ? "合格" : "不合格风险"}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Table<McmRow>
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
size="small"
|
||||
dataSource={[
|
||||
{ name: "IRa", ...result.mcm.ira },
|
||||
{ name: "Ir", ...result.mcm.ir }
|
||||
]}
|
||||
columns={[
|
||||
{ title: "指数", dataIndex: "name" },
|
||||
{ title: "平均值", dataIndex: "mean", render: (value: number) => formatNumber(value) },
|
||||
{ title: "标准偏差", dataIndex: "std_dev", render: (value: number) => formatNumber(value) },
|
||||
{ title: "P2.5", dataIndex: "p2_5", render: (value: number) => formatNumber(value) },
|
||||
{ title: "P97.5", dataIndex: "p97_5", render: (value: number) => formatNumber(value) },
|
||||
{ title: "标准值", dataIndex: "standard_value", render: (value: number) => formatNumber(value, 2) },
|
||||
{
|
||||
title: "合格概率",
|
||||
dataIndex: "pass_probability",
|
||||
render: (value: number) => (
|
||||
<Tag color={value >= 0.95 ? "success" : "warning"}>{formatPercent(value)}</Tag>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<Table<ResultRow>
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
size="small"
|
||||
dataSource={[
|
||||
{ name: "Ra-226", ...result.ra },
|
||||
{ name: "Th-232", ...result.th },
|
||||
{ name: "K-40", ...result.k }
|
||||
]}
|
||||
columns={[
|
||||
{ title: "核素", dataIndex: "name" },
|
||||
{ title: "均值", dataIndex: "mean_measured", render: (value: number) => formatNumber(value) },
|
||||
{ title: "校准活度", dataIndex: "mean_calibrated", render: (value: number) => formatNumber(value) },
|
||||
{ title: "A 类不确定度", dataIndex: "type_a_uncertainty", render: (value: number) => formatNumber(value) },
|
||||
{ title: "B 类相对值", dataIndex: "type_b_relative", render: (value) => formatNumber(value * 100, 3) + "%" },
|
||||
{ title: "合成不确定度", dataIndex: "combined_uncertainty", render: (value: number) => formatNumber(value) }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="results-placeholder">请计算后查看结果</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
|
|
|
|||
|
|
@ -12,20 +12,47 @@ body {
|
|||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 240px;
|
||||
grid-auto-rows: 450px;
|
||||
grid-auto-rows: 440px;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.results-area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.results-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.panel {
|
||||
|
|
@ -70,13 +97,36 @@ body {
|
|||
}
|
||||
|
||||
.compact-panel .ant-card-body {
|
||||
height: calc(100% - 38px);
|
||||
overflow: hidden;
|
||||
padding: 8px 10px 6px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calibration-block {
|
||||
margin-bottom: 8px;
|
||||
.calibration-table {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.limits-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.limit-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.limit-label {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
color: #5f6368;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.limit-field .ant-input-number {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
|
|
@ -91,6 +141,17 @@ body {
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.results-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.results-row > .panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
|
@ -99,8 +160,8 @@ body {
|
|||
}
|
||||
|
||||
.result-tile {
|
||||
min-height: 68px;
|
||||
padding: 8px 10px;
|
||||
min-height: 58px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
|
|
@ -116,7 +177,7 @@ body {
|
|||
}
|
||||
|
||||
.result-tile strong {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
|
@ -145,23 +206,14 @@ body {
|
|||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-label,
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-content {
|
||||
padding: 5px 8px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-label {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.app-shell {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content-grid,
|
||||
.result-grid {
|
||||
.result-grid,
|
||||
.results-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue