diff --git a/packages/shared/src/prediction.ts b/packages/shared/src/prediction.ts index d65e0f7..6e279a5 100644 --- a/packages/shared/src/prediction.ts +++ b/packages/shared/src/prediction.ts @@ -14,110 +14,167 @@ export interface EmissionParams { /** 一种材料在某空间的用量 + 各污染物散发参数 */ export interface SpaceMaterialInput { materialId: string; - /** 使用量(配合用量单位,通常为面积 m² 或其它) */ + /** 使用面积 A (m²)。长度单位时调用方需先 ×0.1 折算 */ usageAmount: number; - /** 每项污染物的散发参数 */ params: Record; } /** 空间环境条件 */ export interface SpaceConditions { - /** 体积 m³ */ + /** 体积 V (m³) */ 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; - /** 每项污染物的贡献率 0~1 */ + /** 各污染物贡献率 Gn (0~1) = (Yn修正·Ln)/C密闭 */ contributionRate: Record; } export interface SpacePredictionResult { - /** 各污染物预测浓度 mg/m³ */ + /** 各污染物最终预测浓度 C (mg/m³) */ concentration: Record; + /** 各污染物密闭浓度 C密闭 (mg/m³),贡献率分母 */ + cClosed: Record; + /** 所用标准的各污染物限值 */ + limits: Record; /** 各污染物是否超标 */ exceeded: Record; /** 各材料贡献 */ contributions: MaterialContribution[]; + /** 各污染物的污染源材料ID(仅超标污染物;累计贡献率>50%的前几个材料) */ + sources: Record; rating: PredictionRating; } +/** 平衡释放量范围阈值:Yp < 0.01 视为该污染物不释放 */ +const YP_THRESHOLD = 0.01; + const zero = (): Record => Object.fromEntries(POLLUTANTS.map((p) => [p, 0])) as Record; /** - * ⚠️ 核心散发速率公式 —— 把你已标定的公式放这里。 - * 当前为占位实现:稳态面积散发速率随温度/湿度做简单修正。 - * - * 返回某材料某污染物的稳态面积散发速率 (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 → 筛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%的材料。 */ 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> = {}; + // 真实承载率 Ln = A/V + const Ln = new Map(); + for (const m of materials) Ln.set(m.materialId, m.usageAmount / V); + + const concentration = zero(); + const cClosed = zero(); + const exceeded = {} as Record; + const sources = {} as Record; + const contribByMat: Record> = {}; + const rateByMat: Record> = {}; 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(); + 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; + 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: 超过。 + * 评级(占位:官方评分/评级规则文档"待补充")。 + * 现按各污染物相对限值的最差比值给级:A≤60% B≤100% C≤150% D>150%。 */ export function ratingFor( concentration: Record,