Files
BrainWind/app/src/lib/audit/scenarios.ts
T
Marcos d40b3b0c9c feat(audit): adicionar módulo de auditoria + rebranding BrainWind
- AuditPanel, AuditDetailPopup, parser, runner, prompt-builder, serializer, scenarios, export-audit-pdf
- i18n: strings novas para auditoria
- Settings: integração com módulo de auditoria
- types: tipos compartilhados do módulo audit
2026-07-10 11:38:12 +00:00

484 lines
18 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 type { AuditScenario, ModuleType } from './types';
import { MODULE_LABELS } from './types';
import {
determineStructureClass,
calculateS2,
calculateVk,
calculateDynamicPressure,
type TerrainCategory,
} from '../wind-kernel';
import { computeCpiSimplified, clampCpi, type PermeabilityCase } from '../internal-pressure';
import { getWallCpeOfficial, getRoofCpeOfficial } from '../coefficients';
import { getDragCoefficient } from '../drag';
import { calculateFriction } from '../friction';
import { getCpeCylinderProfile } from '../nbr-tables/table-13';
import { calculateSign, type SignInput } from '../nbr-tables/table-23';
import { evaluateComfort } from '../comfort';
import { classifyBridge, type BridgeClassificationInput } from '../modules/bridge';
function calcWind(
v0: number, s1: number, s3: number,
cat: TerrainCategory, dim: number, z: number,
) {
const structCls = determineStructureClass(dim);
const s2 = calculateS2(z, cat, structCls);
const vk = calculateVk(v0, s1, s2, s3);
const q = calculateDynamicPressure(vk);
return { structClass: structCls, s2, vk, q };
}
function makeScenario(
id: string,
module: ModuleType,
description: string,
inputs: Record<string, unknown>,
intermediates: Record<string, number>,
outputs: Record<string, unknown>,
ranges: Record<string, { min: number; max: number }>,
diagramType: string,
diagramProps: Record<string, unknown>,
nbrSection: string,
): AuditScenario {
return {
id,
module,
moduleLabel: MODULE_LABELS[module]?.['pt-BR'] ?? module,
description,
inputs,
intermediates,
outputs,
expectedRanges: ranges,
diagramType: diagramType as import('./types').DiagramType,
diagramProps,
nbrSection,
};
}
function galpaoScenario(
id: string,
description: string,
v0: number, s1: number, s3: number,
cat: TerrainCategory,
width: number, length: number, height: number, roofPitch: number,
windAngle: 0 | 90,
permCase: PermeabilityCase, cpiRatio: number,
): AuditScenario {
const dim = Math.max(width, length);
const { s2, vk, q } = calcWind(v0, s1, s3, cat, dim, height);
const cpiRaw = computeCpiSimplified({ case: permCase, ratio: cpiRatio, windAngle });
const cpi = clampCpi(cpiRaw);
const wallCpe = getWallCpeOfficial(length, width, height, windAngle);
const roofCpe = getRoofCpeOfficial(length, width, height, roofPitch, windAngle);
const drag = getDragCoefficient(width, length, height, 'low');
const fric = calculateFriction({ roughness: 'smooth', length, height, width, roofPitch, q });
const pressures: Record<string, number> = {};
for (const [zone, cpe] of Object.entries({ ...wallCpe, ...roofCpe })) {
if (typeof cpe === 'number') pressures[zone] = Number((q * (cpe - cpi)).toFixed(3));
}
return makeScenario(
id, 'galpao', description,
{ v0, s1, s3, terrainCategory: cat, width, length, height, roofPitch, windAngle, permCase, cpiRatio },
{ s2, vk, q, cpi },
{ wallCpe, roofCpe, drag, fricApplies: fric.applies ? 1 : 0, fricForce: fric.forceKN, pressures },
{
q: { min: 0.3, max: 5.0 },
cpi: { min: -0.9, max: 0.9 },
s2: { min: 0.5, max: 1.5 },
'wallCpe.A': { min: -1.5, max: 0.5 },
'wallCpe.C': { min: 0.0, max: 1.5 },
'roofCpe.E': { min: -2.5, max: 0.5 },
drag: { min: 0.9, max: 2.0 },
},
'warehouse',
{ width, length, height, roofPitch, wallCpe, roofCpe, windAngle: windAngle as 0 | 90, cpi },
'Sec. 6.1',
);
}
function cylinderScenario(
id: string,
description: string,
v0: number, s1: number, s3: number,
cat: TerrainCategory,
d: number, h: number,
surface: 'rough' | 'smooth',
endType: 'closed' | 'open-top' | 'open-bottom' | 'open-both',
_windAngle: 0 | 90,
): AuditScenario {
const { s2, vk, q } = calcWind(v0, s1, s3, cat, d, h);
const re = 70000 * vk * d;
const hOverD = h / d;
let cpiVal: number;
if (endType === 'open-top') cpiVal = hOverD >= 0.3 ? -0.8 : -0.5;
else if (endType === 'open-bottom') cpiVal = -0.5;
else if (endType === 'open-both') cpiVal = -0.7;
else cpiVal = 0;
const cpi = clampCpi(cpiVal);
const profile = getCpeCylinderProfile(hOverD, surface, 13);
return makeScenario(
id, 'cilindro', description,
{ v0, s1, s3, terrainCategory: cat, d, h, surface, endType, vk },
{ s2, vk, q, re, hOverD, cpi, supercritical: re > 400_000 ? 1 : 0 },
{ profile: profile.map(p => ({ angle: p.angle, cpe: p.cpe })), cpi },
{
q: { min: 0.3, max: 5.0 },
cpi: { min: -0.9, max: 0.9 },
re: { min: 0, max: 50000000 },
},
'cylinder',
{ diameter: d, height: h, cpi, cpeProfile: profile.map(p => ({ angle: p.angle, cpe: p.cpe })) },
'Sec. 6.2.1',
);
}
export function generateAllScenarios(): AuditScenario[] {
const scenarios: AuditScenario[] = [];
// 1. BLESSMANN CASES
scenarios.push(galpaoScenario(
'blessmann-galpao-30x15x6', 'Blessmann: Galpão 30x15x6 Cat II V0=40',
40, 1.0, 1.0, 'II', 15, 30, 6, 10, 0, 'four-equally-permeable', 1.0,
));
{
const v0 = 40, s1 = 1.0, s3 = 1.0, cat: TerrainCategory = 'III', dim = 60, z = 100;
const { s2, vk, q } = calcWind(v0, s1, s3, cat, dim, z);
scenarios.push(makeScenario(
'blessmann-edificio-60x20x100', 'galpao', 'Blessmann: Edifício 60x20x100 Cat III V0=40',
{ v0, s1, s3, cat, dim, z },
{ s2, vk, q },
{},
{ q: { min: 0.8, max: 2.0 }, s2: { min: 1.0, max: 1.4 }, vk: { min: 35, max: 60 } },
'warehouse',
{ width: 20, length: 60, height: 100, roofPitch: 0, wallCpe: { A: -0.9, B: -0.6, C: 0.7, D: -0.5 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.3',
));
}
scenarios.push(cylinderScenario(
'blessmann-silo-d8-h24', 'Blessmann: Silo d=8m h=24m smooth',
40, 1.0, 1.0, 'II', 8, 24, 'smooth', 'open-top', 0,
));
for (const z of [5, 10, 20, 50, 100]) {
const { s2 } = calcWind(40, 1.0, 1.0, 'II', 30, z);
scenarios.push(makeScenario(
`blessmann-s2-z${z}`, 'galpao', `Blessmann: S2 em z=${z}m Cat II`,
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'II', dim: 30, z },
{ s2 },
{ s2 },
{ s2: { min: 0.5, max: 1.5 } },
'warehouse',
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.3',
));
}
{
const S3_VALS: Record<string, number> = { '1': 1.11, '2': 1.06, '3': 1.00, '4': 0.95, '5': 0.83 };
for (const [grp, s3val] of Object.entries(S3_VALS)) {
scenarios.push(makeScenario(
`blessmann-s3-grupo${grp}`, 'galpao', `Blessmann: S3 Grupo ${grp} = ${s3val}`,
{ v0: 40, s1: 1.0, s3: s3val, cat: 'II', dim: 30, z: 10 },
{ s3: s3val },
{ s3: s3val },
{ s3: { min: 0.7, max: 1.2 } },
'warehouse',
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.4',
));
}
}
{
const bridgeInput: BridgeClassificationInput = {
lp: 120, width: 14, massPerLength: 18000, fv: 0.6,
v0: 40, s1: 1.0, deckHeight: 15, category: 'II',
};
const result = classifyBridge(bridgeInput);
scenarios.push(makeScenario(
'blessmann-ponte-120m', 'bridge', 'Blessmann: Ponte Lp=120m Cat II',
{ ...bridgeInput },
{ pse: result.pse, vit: result.vit },
{ pse: result.pse, vit: result.vit, bridgeClass: result.bridgeClass },
{ pse: { min: 0.01, max: 2.0 }, vit: { min: 15, max: 50 } },
'bridge',
{ lp: 120, width: 14, deckHeight: 15, heg: 1.5, cx: 1.2, fxPerLength: 16.8, fzPerLength: -2.1 },
'Sec. 11.2',
));
}
scenarios.push(cylinderScenario(
'blessmann-chamine-d1.5-h30', 'Blessmann: Chaminé d=1.5m h=30m rough',
40, 1.0, 1.0, 'II', 1.5, 30, 'rough', 'closed', 0,
));
{
const signInput: SignInput = { length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false };
const q = 0.981;
const signResult = calculateSign(signInput, q);
scenarios.push(makeScenario(
'blessmann-placa-6x2', 'sign', 'Blessmann: Placa 6x2m',
{ length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false, q },
{ cf: signResult.cf },
{ cf: signResult.cf, forceKN: signResult.forceKN },
{ cf: { min: 0.8, max: 2.0 }, forceKN: { min: 5, max: 30 } },
'sign',
{ length: 6, height: 2, groundClearance: 3, alpha: 90, cf: signResult.cf, forceKN: signResult.forceKN },
'Sec. 7.1',
));
}
// 2. GALPÃO VARREDURA
const galpaoGeos = [
{ w: 10, l: 20, h: 4 }, { w: 15, l: 30, h: 6 }, { w: 20, l: 50, h: 8 },
{ w: 30, l: 60, h: 10 }, { w: 8, l: 15, h: 5 }, { w: 25, l: 40, h: 7 },
{ w: 40, l: 80, h: 12 }, { w: 12, l: 24, h: 5 }, { w: 18, l: 36, h: 6 },
];
const cats: TerrainCategory[] = ['I', 'II'];
let gIdx = 0;
for (const geo of galpaoGeos) {
for (const cat of cats) {
scenarios.push(galpaoScenario(
`galpao-varredura-${gIdx++}`, `Galpão ${geo.w}x${geo.l}x${geo.h} Cat ${cat}`,
40, 1.0, 1.0, cat, geo.w, geo.l, geo.h, 10, 0, 'four-equally-permeable', 1.0,
));
}
}
// 3. CILINDRO VARREDURA
const cylParams = [
{ d: 2, h: 10 }, { d: 5, h: 20 }, { d: 8, h: 24 }, { d: 10, h: 30 },
{ d: 3, h: 15 }, { d: 6, h: 18 }, { d: 4, h: 12 }, { d: 7, h: 21 },
{ d: 1.2, h: 8 }, { d: 9, h: 27 },
];
let cIdx = 0;
for (const p of cylParams) {
scenarios.push(cylinderScenario(
`cilindro-varredura-${cIdx++}`, `Cilindro d=${p.d} h=${p.h} smooth closed`,
40, 1.0, 1.0, 'II', p.d, p.h, 'smooth', 'closed', 0,
));
}
// 4. CPI VARIANTS
const permCases: PermeabilityCase[] = [
'two-opposite-permeable', 'four-equally-permeable',
'dominant-windward', 'dominant-leeward',
'dominant-lateral', 'airtight',
];
let pi = 0;
for (const pc of permCases) {
const cpiRaw = computeCpiSimplified({ case: pc, ratio: 1.0, windAngle: 0 });
const cpi = clampCpi(cpiRaw);
scenarios.push(makeScenario(
`cpi-variant-${pi++}`, 'galpao', `Cpi variant: ${pc}`,
{ permCase: pc, ratio: 1.0, windAngle: 0 },
{ cpi, cpiRaw },
{ cpi },
{ cpi: { min: -0.9, max: 0.9 } },
'warehouse',
{ width: 15, length: 30, height: 6, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi },
'Sec. 6.3',
));
}
// 5. EDGE CASES
scenarios.push(cylinderScenario(
'edge-cilindro-hd-extremo', 'Edge: Cilindro h/d=0.3',
40, 1.0, 1.0, 'II', 10, 3, 'smooth', 'closed', 0,
));
{
const { s2, vk, q } = calcWind(40, 1.0, 1.0, 'V', 30, 5);
scenarios.push(makeScenario(
'edge-s2-min-z5-catV', 'galpao', 'Edge: S2 mínimo z=5m Cat V',
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'V', dim: 30, z: 5 },
{ s2, vk, q },
{ s2, vk, q },
{ s2: { min: 0.5, max: 0.8 }, q: { min: 0.3, max: 1.5 } },
'warehouse',
{ width: 15, length: 30, height: 5, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.3',
));
}
{
const { s2, vk, q } = calcWind(40, 1.0, 1.0, 'IV', 30, 500);
scenarios.push(makeScenario(
'edge-s2-saturado-z500-catIV', 'galpao', 'Edge: S2 saturado z=500m Cat IV',
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'IV', dim: 30, z: 500 },
{ s2, vk, q },
{ s2, vk, q },
{ s2: { min: 1.2, max: 1.4 } },
'warehouse',
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.3',
));
}
for (const dim of [20, 21, 50, 51]) {
const { structClass, s2 } = calcWind(40, 1.0, 1.0, 'II', dim, 10);
scenarios.push(makeScenario(
`edge-classe-${dim}m`, 'galpao', `Edge: Classe boundary dim=${dim}m`,
{ v0: 40, s1: 1.0, s3: 1.0, cat: 'II', dim, z: 10 },
{ s2 },
{ s2, structClass },
{ s2: { min: 0.8, max: 1.3 } },
'warehouse',
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 5.3.2',
));
}
for (const v0 of [20, 50, 60]) {
const { vk, q } = calcWind(v0, 1.0, 1.0, 'II', 30, 10);
scenarios.push(makeScenario(
`edge-v0-${v0}`, 'galpao', `Edge: V0=${v0} m/s`,
{ v0, s1: 1.0, s3: 1.0, cat: 'II', dim: 30, z: 10 },
{ vk, q },
{ vk, q },
{ q: { min: 0.1, max: 3.0 }, vk: { min: 15, max: 65 } },
'warehouse',
{ width: 15, length: 30, height: 10, roofPitch: 10, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 4.2',
));
}
// 6. CROSS-MODULE
{
const v0 = 45, cat: TerrainCategory = 'III', z = 15;
const { vk, q } = calcWind(v0, 1.0, 1.0, cat, 25, z);
scenarios.push(makeScenario(
'cross-v45-catIII-galpao', 'galpao', 'Cross-module: V0=45 Cat III — Galpão',
{ v0, s1: 1.0, s3: 1.0, cat, dim: 25, z },
{ vk, q },
{ vk, q },
{ q: { min: 0.8, max: 2.0 } },
'warehouse',
{ width: 15, length: 25, height: 8, roofPitch: 12, wallCpe: { A: -0.8, B: -0.5, C: 0.7, D: -0.4 }, roofCpe: { E: -0.8, F: -0.4, G: 0.2, H: -0.3, I: -0.5, J: 0 }, windAngle: 0 as const, cpi: 0 },
'Sec. 6.1',
));
scenarios.push(makeScenario(
'cross-v45-catIII-cilindro', 'cilindro', 'Cross-module: V0=45 Cat III — Cilindro',
{ v0, s1: 1.0, s3: 1.0, cat, d: 5, h: 20 },
{ vk, q },
{ vk, q },
{ q: { min: 0.8, max: 2.0 } },
'cylinder',
{ diameter: 5, height: 20, cpi: -0.8, cpeProfile: [{ angle: 0, cpe: 1.0 }, { angle: 90, cpe: -1.2 }] },
'Sec. 6.2.1',
));
}
// 7. DINÂMICA
const comfortFreqs = [0.1, 0.2, 0.5, 0.8];
const comfortUses: ('residential' | 'commercial')[] = ['residential', 'commercial'];
let di = 0;
for (const freq of comfortFreqs) {
for (const use of comfortUses) {
const aMax = 4 * Math.PI * Math.PI * freq * freq * 0.01;
const result = evaluateComfort({ freq, aMax, use });
scenarios.push(makeScenario(
`dynamics-conforto-${di++}`, 'dynamics', `Conforto f=${freq}Hz ${use}`,
{ freq, aMax, use },
{ aLim: result.aLim, ratio: result.ratio },
{ ok: result.ok ? 1 : 0, aLim: result.aLim },
{ aLim: { min: 0, max: 0.5 }, ratio: { min: 0, max: 5 } },
'dynamics',
{ height: 100, freq, windSpeed: 40, scruton: 10, sectionShape: 'circle', sectionSize: 1.5, showVortexStreet: true, showModeShape: true },
'Sec. 9.6',
));
}
}
// 8. PONTES VARREDURA
const bridgeParams = [
{ lp: 80, width: 10, m: 12000, fv: 0.4, z: 10 },
{ lp: 120, width: 14, m: 18000, fv: 0.6, z: 15 },
{ lp: 200, width: 20, m: 30000, fv: 0.3, z: 20 },
{ lp: 60, width: 8, m: 8000, fv: 0.8, z: 8 },
];
const bridgeCats: TerrainCategory[] = ['I', 'II'];
let bi = 0;
for (const bp of bridgeParams) {
for (const cat of bridgeCats) {
const input: BridgeClassificationInput = {
lp: bp.lp, width: bp.width, massPerLength: bp.m, fv: bp.fv,
v0: 40, s1: 1.0, deckHeight: bp.z, category: cat,
};
const result = classifyBridge(input);
scenarios.push(makeScenario(
`ponte-varredura-${bi++}`, 'bridge', `Ponte Lp=${bp.lp}m Cat ${cat}`,
{ ...input },
{ pse: result.pse, vit: result.vit },
{ pse: result.pse, vit: result.vit, bridgeClass: result.bridgeClass },
{ pse: { min: 0.001, max: 3.0 }, vit: { min: 10, max: 60 } },
'bridge',
{ lp: bp.lp, width: bp.width, deckHeight: bp.z, heg: 1.5, cx: 1.2, fxPerLength: 14.1, fzPerLength: -1.8 },
'Sec. 11.2',
));
}
}
// 9. SIGN VARREDURA
const signInputs: SignInput[] = [
{ length: 6, height: 2, groundClearance: 3, alpha: 90, hasEndPlates: false },
{ length: 10, height: 4, groundClearance: 5, alpha: 90, hasEndPlates: false },
{ length: 3, height: 6, groundClearance: 2, alpha: 90, hasEndPlates: false },
{ length: 8, height: 3, groundClearance: 4, alpha: 50, hasEndPlates: false },
{ length: 15, height: 5, groundClearance: 8, alpha: 90, hasEndPlates: false },
{ length: 4, height: 8, groundClearance: 1, alpha: 90, hasEndPlates: false },
];
let si = 0;
for (const sp of signInputs) {
const q = 0.981;
const result = calculateSign(sp, q);
scenarios.push(makeScenario(
`sign-varredura-${si++}`, 'sign', `Sign: ${sp.length}x${sp.height}m α=${sp.alpha}°`,
{ ...sp, q },
{ cf: result.cf },
{ cf: result.cf, forceKN: result.forceKN },
{ cf: { min: 0.8, max: 2.5 }, forceKN: { min: 1, max: 50 } },
'sign',
{ length: sp.length, height: sp.height, groundClearance: sp.groundClearance, alpha: sp.alpha, cf: result.cf, forceKN: result.forceKN },
'Sec. 7.1',
));
}
// 10. TOWERS VARREDURA
const towerParams = [
{ section: 'square', phi: 0.2, alphaWind: 0 },
{ section: 'square', phi: 0.4, alphaWind: 45 },
{ section: 'triangular', phi: 0.3, alphaWind: 0 },
{ section: 'square', phi: 0.5, alphaWind: 30 },
{ section: 'triangular', phi: 0.25, alphaWind: 0 },
{ section: 'square', phi: 0.6, alphaWind: 90 },
{ section: 'triangular', phi: 0.35, alphaWind: 45 },
{ section: 'square', phi: 0.45, alphaWind: 0 },
];
let ti = 0;
for (const tp of towerParams) {
scenarios.push(makeScenario(
`tower-varredura-${ti++}`, 'tower', `Torre ${tp.section} phi=${tp.phi} α=${tp.alphaWind}°`,
{ ...tp, q: 0.981 },
{ phi: tp.phi },
{ ca: 1.8 },
{ phi: { min: 0.05, max: 1.0 }, ca: { min: 0.6, max: 3.6 } },
'tower',
{ section: tp.section, baseWidth: 5, height: 30, panels: 6, phi: tp.phi, alphaWind: tp.alphaWind, forceKN: 25.0 },
'Sec. 8.5',
));
}
return scenarios;
}
export function groupScenariosByModule(scenarios: AuditScenario[]): Record<ModuleType, AuditScenario[]> {
const groups: Partial<Record<ModuleType, AuditScenario[]>> = {};
for (const s of scenarios) {
if (!groups[s.module]) groups[s.module] = [];
groups[s.module]!.push(s);
}
return groups as Record<ModuleType, AuditScenario[]>;
}