airpredict/packages/shared/src/prediction.ts

193 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { POLLUTANTS, Pollutant, StandardCode, STANDARD_LIMITS } from './pollutants.js';
import { PredictionRating } from './enums.js';
/** 单一污染物的散发模型参数 (per material, per pollutant) */
export interface EmissionParams {
/** 最低平衡释放量 Y0 (mg/m²) */
y0: number;
/** 平衡释放量范围 Yp (mg/m²) */
yp: number;
/** 平衡释放量变化率 B (m²/m³) */
b: number;
}
/** 一种材料在某空间的用量 + 各污染物散发参数 */
export interface SpaceMaterialInput {
materialId: string;
/** 使用面积 A (m²)。长度单位时调用方需先 ×0.1 折算 */
usageAmount: number;
params: Record<Pollutant, EmissionParams>;
}
/** 空间环境条件 */
export interface SpaceConditions {
/** 体积 V (m³) */
volume: number;
/** 温度 T (°C) */
temperature: number;
/** 湿度 H (%rh如 50内部折算为小数) */
humidity: number;
/** 通风换气次数 ACH (次/小时) */
ventilationRate: number;
standard: StandardCode;
}
export interface MaterialContribution {
materialId: string;
/** 各污染物:该材料占最终浓度的份额 (mg/m³) = Gn × C */
contribution: Record<Pollutant, number>;
/** 各污染物贡献率 Gn (0~1) = (Yn修正·Ln)/C密闭 */
contributionRate: Record<Pollutant, number>;
}
export interface SpacePredictionResult {
/** 各污染物最终预测浓度 C (mg/m³) */
concentration: Record<Pollutant, number>;
/** 各污染物密闭浓度 C密闭 (mg/m³),贡献率分母 */
cClosed: Record<Pollutant, number>;
/** 所用标准的各污染物限值 */
limits: Record<Pollutant, number>;
/** 各污染物是否超标 */
exceeded: Record<Pollutant, boolean>;
/** 各材料贡献 */
contributions: MaterialContribution[];
/** 各污染物的污染源材料ID仅超标污染物累计贡献率>50%的前几个材料) */
sources: Record<Pollutant, string[]>;
rating: PredictionRating;
}
/** 平衡释放量范围阈值Yp < 0.01 视为该污染物不释放 */
const YP_THRESHOLD = 0.01;
const zero = (): Record<Pollutant, number> =>
Object.fromEntries(POLLUTANTS.map((p) => [p, 0])) as Record<Pollutant, number>;
/**
* 单空间多污染物预测(中国建材院模型 20240307
* 每种污染物独立计算:
* Ln=A/V → 筛Yp≥0.01 → Ln修正=Σ(Li·Ypi)/Ypn → Yn修正=Y0+Yp·e^(Ln修正/B)
* → C密闭=Σ(Yi修正·Li) → E=0.51+8.74·C密闭/Σ(Li·Ypi) → U=1/(1+E·ACH)
* → C通风=U·C密闭 → WS=e^(0.102(T23))·e^(1.2312(H0.5)) → C=WS·C通风
* 贡献率 Gn=(Yn修正·Ln)/C密闭污染源=超标污染物中按Gn降序累计>50%的材料。
*/
export function predictSpace(
materials: SpaceMaterialInput[],
cond: SpaceConditions,
): SpacePredictionResult {
const V = Math.max(1e-6, cond.volume);
const ACH = Math.max(0, cond.ventilationRate);
const T = cond.temperature;
const H = cond.humidity > 1 ? cond.humidity / 100 : cond.humidity; // %rh → 小数
const WS = Math.exp(0.102 * (T - 23)) * Math.exp(1.2312 * (H - 0.5));
const limits = STANDARD_LIMITS[cond.standard];
// 真实承载率 Ln = A/V
const Ln = new Map<string, number>();
for (const m of materials) Ln.set(m.materialId, m.usageAmount / V);
const concentration = zero();
const cClosed = zero();
const exceeded = {} as Record<Pollutant, boolean>;
const sources = {} as Record<Pollutant, string[]>;
const contribByMat: Record<string, Record<Pollutant, number>> = {};
const rateByMat: Record<string, Record<Pollutant, number>> = {};
for (const m of materials) {
contribByMat[m.materialId] = zero();
rateByMat[m.materialId] = zero();
}
for (const p of POLLUTANTS) {
// 1. 筛选释放该污染物的材料Yp≥0.01
const emitters = materials.filter((m) => (m.params[p]?.yp ?? 0) >= YP_THRESHOLD);
// Σ(Li·Ypi)
const sumLYp = emitters.reduce((s, m) => s + Ln.get(m.materialId)! * m.params[p].yp, 0);
// 2. Yn修正 = Y0 + Yp·e^(Ln修正/B),其中 Ln修正 = Σ(Li·Ypi)/Ypn
const yCorr = new Map<string, number>();
for (const m of emitters) {
const { y0, yp, b } = m.params[p];
const lnCorr = yp > 0 ? sumLYp / yp : 0;
const term = b > 0 ? yp * Math.exp(-lnCorr / b) : 0;
yCorr.set(m.materialId, y0 + term);
}
// 3. 密闭浓度 C密闭 = Σ(Yn修正·Ln)
const cClose = emitters.reduce((s, m) => s + yCorr.get(m.materialId)! * Ln.get(m.materialId)!, 0);
cClosed[p] = round3(cClose);
// 4. 通风修正
const E = sumLYp > 0 ? 0.51 + 8.74 * (cClose / sumLYp) : 0;
const U = 1 / (1 + E * ACH);
const cVent = U * cClose;
// 5. 温湿度修正 → 最终浓度
const C = WS * cVent;
concentration[p] = round3(C);
// 6. 贡献率 Gn = (Yn修正·Ln)/C密闭
for (const m of emitters) {
const gn = cClose > 0 ? (yCorr.get(m.materialId)! * Ln.get(m.materialId)!) / cClose : 0;
rateByMat[m.materialId][p] = gn;
contribByMat[m.materialId][p] = round3(gn * C);
}
// 7. 超标判定 + 污染源累计Gn>50%
const over = C > limits[p];
exceeded[p] = over;
if (over) {
const sorted = emitters
.map((m) => ({ id: m.materialId, gn: rateByMat[m.materialId][p] }))
.sort((a, b) => b.gn - a.gn);
const src: string[] = [];
let cum = 0;
for (const s of sorted) {
src.push(s.id);
cum += s.gn;
if (cum > 0.5) break;
}
sources[p] = src;
} else {
sources[p] = [];
}
}
const contributions: MaterialContribution[] = materials.map((m) => ({
materialId: m.materialId,
contribution: contribByMat[m.materialId],
contributionRate: rateByMat[m.materialId],
}));
return {
concentration,
cClosed,
limits,
exceeded,
contributions,
sources,
rating: ratingFor(concentration, cond.standard),
};
}
function round3(n: number): number {
return Math.round(n * 1000) / 1000;
}
/**
* 评级(占位:官方评分/评级规则文档"待补充")。
* 现按各污染物相对限值的最差比值给级A≤60% B≤100% C≤150% D>150%。
*/
export function ratingFor(
concentration: Record<Pollutant, number>,
standard: StandardCode,
): PredictionRating {
const limits = STANDARD_LIMITS[standard];
let worst = 0;
for (const p of POLLUTANTS) {
worst = Math.max(worst, limits[p] > 0 ? concentration[p] / limits[p] : 0);
}
if (worst <= 0.6) return 'A';
if (worst <= 1.0) return 'B';
if (worst <= 1.5) return 'C';
return 'D';
}