feat(phase4): 接入真实预测计算模型(中国建材院)

替换占位公式,按模型精确实现 prediction.ts:
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(T−23))·e^(1.2312(H−0.5)) → C=WS·C通风
贡献率 Gn=(Yn修正·Ln)/C密闭;污染源=超标污染物按Gn降序累计>50%。
结果新增 cClosed/limits/sources 字段。已用官方算例校验最终浓度全部±0.01通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zty 2026-06-12 10:31:26 +08:00
parent 956954cf09
commit 022bd721ee
1 changed files with 113 additions and 56 deletions

View File

@ -14,110 +14,167 @@ export interface EmissionParams {
/** 一种材料在某空间的用量 + 各污染物散发参数 */
export interface SpaceMaterialInput {
materialId: string;
/** 使用量(配合用量单位,通常为面积 m² 或其它) */
/** 使用面积 A (m²)。长度单位时调用方需先 ×0.1 折算 */
usageAmount: number;
/** 每项污染物的散发参数 */
params: Record<Pollutant, EmissionParams>;
}
/** 空间环境条件 */
export interface SpaceConditions {
/** 体积 m³ */
/** 体积 V () */
volume: number;
/** 温度 */
/** 温度 T (°C) */
temperature: number;
/** 湿度 %rh */
/** 湿度 H (%rh,如 50内部折算为小数) */
humidity: number;
/** 通风换气率 次/小时 (ACH) */
/** 通风换气次数 ACH (次/小时) */
ventilationRate: number;
standard: StandardCode;
}
export interface MaterialContribution {
materialId: string;
/** 每项污染物对最终浓度的贡献 (mg/m³) */
/** 各污染物:该材料占最终浓度的份额 (mg/m³) = Gn × C */
contribution: Record<Pollutant, number>;
/** 每项污染物的贡献率 0~1 */
/** 各污染物贡献率 Gn (0~1) = (Yn修正·Ln)/C密闭 */
contributionRate: Record<Pollutant, number>;
}
export interface SpacePredictionResult {
/** 各污染物预测浓度 mg/m³ */
/** 各污染物最终预测浓度 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>;
/**
*
* /湿
*
* (mg/m²·h)
*
* @param p Y0/Yp/B
* @param cond 湿
*/
export function emissionRate(p: EmissionParams, cond: SpaceConditions): number {
// —— PLACEHOLDER替换为你的标定公式 ——
// 占位:以 Y0 为基线Yp 受温度偏离 25℃ 影响B 作为湿度/装载修正系数。
const tempFactor = 1 + 0.02 * (cond.temperature - 25); // 每偏离1℃修正2%
const humFactor = 1 + 0.005 * (cond.humidity - 50); // 每偏离50%rh修正0.5%
const rate = (p.y0 + p.yp * Math.max(0, tempFactor - 1) + p.b * humFactor * 0.0) * tempFactor * humFactor;
return Math.max(0, rate);
}
/**
*
*
* G = = ACH × V × C => C = G / (ACH × V)
* G_pollutant = Σ_material ( emissionRate × usageAmount )
* 20240307
*
* Ln=A/V Yp0.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];
const concentration = zero();
const denom = Math.max(1e-6, cond.ventilationRate * cond.volume);
// 累计各材料各污染物的生成量
const rawContrib: Record<string, Record<Pollutant, number>> = {};
// 真实承载率 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) {
rawContrib[m.materialId] = zero();
for (const pol of POLLUTANTS) {
const G = emissionRate(m.params[pol], cond) * m.usageAmount;
const c = G / denom;
rawContrib[m.materialId][pol] = c;
concentration[pol] += c;
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) => {
const contribution = rawContrib[m.materialId];
const contributionRate = zero();
for (const pol of POLLUTANTS) {
contributionRate[pol] = concentration[pol] > 0 ? contribution[pol] / concentration[pol] : 0;
}
return { materialId: m.materialId, contribution, contributionRate };
});
const contributions: MaterialContribution[] = materials.map((m) => ({
materialId: m.materialId,
contribution: contribByMat[m.materialId],
contributionRate: rateByMat[m.materialId],
}));
const exceeded = Object.fromEntries(
POLLUTANTS.map((p) => [p, concentration[p] > limits[p]]),
) as Record<Pollutant, boolean>;
return {
concentration,
cClosed,
limits,
exceeded,
contributions,
sources,
rating: ratingFor(concentration, cond.standard),
};
}
return { concentration, exceeded, contributions, rating: ratingFor(concentration, cond.standard) };
function round3(n: number): number {
return Math.round(n * 1000) / 1000;
}
/**
*
* A: 全部 60% ; B: 100%; C: 150%; D: 超过
* /"待补充"
* A60% B100% C150% D>150%
*/
export function ratingFor(
concentration: Record<Pollutant, number>,