🚀 Auto-deploy: BrainWind atualizado em 14/07/2026 11:50:17

This commit is contained in:
2026-07-14 11:50:17 +00:00
parent 7fcf964254
commit 5899c5c6ad
14 changed files with 4172 additions and 1835 deletions
+1
View File
@@ -27,6 +27,7 @@ const MODULE_ROUTES: RouteModuleMap = {
'cross-module': '/galpao',
'edge-case': '/galpao',
'cpi-variant': '/galpao',
piperack: '/piperack',
};
interface AuditDetailPopupProps {
+9
View File
@@ -0,0 +1,9 @@
/**
* Script de dump para auditoria manual: gera JSON com todos os cenários.
* Executar via: npx tsx src/lib/__tests__/dump-scenarios.ts
*/
import { generateAllScenarios } from '../audit/scenarios';
const all = generateAllScenarios();
// eslint-disable-next-line no-console
console.log(JSON.stringify(all, null, 2));
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { computeCpiSimplified, clampCpi } from '../internal-pressure';
import { computeCpiSimplified, clampCpi, computeCpiEnvelope } from '../internal-pressure';
describe('Pressão Interna — sec. 6.3', () => {
describe('computeCpiSimplified', () => {
@@ -23,6 +23,27 @@ describe('Pressão Interna — sec. 6.3', () => {
});
});
describe('computeCpiEnvelope (envoltória)', () => {
it('four-equally-permeable retorna [-0.3, 0]', () => {
const env = computeCpiEnvelope({ case: 'four-equally-permeable' });
expect(env).toEqual([-0.3, 0]);
});
it('airtight retorna [-0.2, 0]', () => {
const env = computeCpiEnvelope({ case: 'airtight' });
expect(env).toEqual([-0.2, 0]);
});
it('dominant-windward retorna valor único [v, v]', () => {
const env = computeCpiEnvelope({ case: 'dominant-windward', ratio: 1 });
expect(env[0]).toBe(env[1]);
expect(env[0]).toBe(0.3);
});
it('two-opposite-permeable retorna valor único', () => {
const env = computeCpiEnvelope({ case: 'two-opposite-permeable', windAngle: 0 });
expect(env[0]).toBe(env[1]);
expect(env[0]).toBe(0.2);
});
});
describe('clampCpi (limites normativos)', () => {
it('Limita em +0,9', () => {
expect(clampCpi(1.5)).toBe(0.9);
+3 -3
View File
@@ -49,14 +49,14 @@ Retorne EXATAMENTE um JSON válido (sem markdown, sem code blocks) com o seguint
}
## Regras importantes
- status "PASS" = tudo correto ou variação insignificante (< 1% ou diferença absoluta < 0.05)
- status "WARN" = valores com imprecisão numérica moderada, mas aceitável
- status "PASS" = tudo correto ou empate técnico (variação insignificante < 1% ou diferença absoluta < 0.05)
- status "WARN" = imprecisão numérica moderada (entre 1% e 2%), ou desvio de método sem impacto grave
- status "FAIL" = erro real ou diferença grosseira (variação > 2%)
- verdict "FAIL" = pelo menos 1 check FAIL
- verdict "WARN" = pelo menos 1 check WARN e nenhum FAIL
- verdict "PASS" = todos checks PASS
- Seja preciso com números: compare os valores fornecidos pelo app com os que você deduziu matematicamente das tabelas e fórmulas
- EMPATE TÉCNICO: Se o seu cálculo diferir do app em menos de 1% (ex: 1.04 vs 1.05) devido a interpolação ou arredondamentos de passos intermediários, isso NÃO é uma falha. Você DEVE marcar como "PASS" ou no máximo "WARN", NUNCA "FAIL".
- EMPATE TÉCNICO: Se o seu cálculo diferir do app em menos de 1% (ex: 1.04 vs 1.05) devido a interpolação ou arredondamentos de passos intermediários, isso é considerado um empate técnico. Você DEVE classificar obrigatoriamente como "PASS". NUNCA marque como "WARN" nem como "FAIL" nessas situações.
- Só marque "FAIL" se a diferença for claramente um erro de fórmula, módulo errado ou leitura errada de tabela.
- IMPORTANTE: Cpi DEVE estar em [-0.9, +0.9] — qualquer valor fora é FAIL
- q deve ser sempre >= 0
+57 -15
View File
@@ -15,6 +15,8 @@ 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';
import { calculateTower } from '../modules/tower';
import { calculatePiperack, type PiperackInput } from '../modules/piperack';
function calcWind(
v0: number, s1: number, s3: number,
@@ -70,7 +72,7 @@ function galpaoScenario(
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 fric = calculateFriction({ roughness: 'smooth', length, height, width, roofPitch, q, windAngle });
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));
@@ -358,11 +360,13 @@ export function generateAllScenarios(): AuditScenario[] {
{ 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',
));
const cylWind = calcWind(v0, 1.0, 1.0, cat, 20, 20); // cil h=20, max(d,h)=20, z=20
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 },
{ vk: cylWind.vk, q: cylWind.q },
{ vk: cylWind.vk, q: cylWind.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 }] },
@@ -447,29 +451,67 @@ export function generateAllScenarios(): AuditScenario[] {
// 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 },
{ section: 'square' as const, phi: 0.2, alphaWind: 0 as const },
{ section: 'square' as const, phi: 0.4, alphaWind: 45 as const },
{ section: 'triangular' as const, phi: 0.3, alphaWind: 0 as const },
{ section: 'square' as const, phi: 0.5, alphaWind: 45 as const },
{ section: 'triangular' as const, phi: 0.25, alphaWind: 0 as const },
{ section: 'square' as const, phi: 0.6, alphaWind: 90 as const },
{ section: 'triangular' as const, phi: 0.35, alphaWind: 45 as const },
{ section: 'square' as const, phi: 0.45, alphaWind: 0 as const },
];
let ti = 0;
for (const tp of towerParams) {
const q = 0.981;
const aFace = 5 * 30; // baseWidth = 5, height = 30
const tr = calculateTower({
section: tp.section,
barType: 'flat',
phi: tp.phi,
aFace,
alphaWind: tp.alphaWind,
q,
});
scenarios.push(makeScenario(
`tower-varredura-${ti++}`, 'tower', `Torre ${tp.section} phi=${tp.phi} α=${tp.alphaWind}°`,
{ ...tp, q: 0.981 },
{ ...tp, q },
{ phi: tp.phi },
{ ca: 1.8 },
{ phi: { min: 0.05, max: 1.0 }, ca: { min: 0.6, max: 3.6 } },
{ ca: tr.caEff, forceKN: tr.forceKN },
{ phi: { min: 0.05, max: 1.0 }, ca: { min: 0.6, max: 3.6 }, forceKN: { min: 1, max: 1000 } },
'tower',
{ section: tp.section, baseWidth: 5, height: 30, panels: 6, phi: tp.phi, alphaWind: tp.alphaWind, forceKN: 25.0 },
{ section: tp.section, baseWidth: 5, height: 30, panels: 6, phi: tp.phi, alphaWind: tp.alphaWind, forceKN: tr.forceKN },
'Sec. 8.5',
));
}
// 11. PIPERACK VARREDURA
const piperackParams: Array<{ name: string; input: Omit<PiperackInput, 'q'> }> = [
{ name: 'Pipe-1', input: { width: 10, height: 4, elevation: 6, spacing: 5, numFrames: 3, phiStruct: 0.15, pipes: [{ id: 'p1', diameter: 1.0, elevationOffset: 4 }] } },
{ name: 'Pipe-2', input: { width: 12, height: 5, elevation: 8, spacing: 6, numFrames: 4, phiStruct: 0.20, pipes: [{ id: 'p1', diameter: 0.5, elevationOffset: 5 }, { id: 'p2', diameter: 0.5, elevationOffset: 5 }] } },
{ name: 'Pipe-3', input: { width: 8, height: 3, elevation: 5, spacing: 4, numFrames: 2, phiStruct: 0.10, pipes: [] } },
{ name: 'Pipe-4', input: { width: 15, height: 6, elevation: 10, spacing: 6, numFrames: 5, phiStruct: 0.30, pipes: [{ id: 'p1', diameter: 1.5, elevationOffset: 6 }] } },
{ name: 'Pipe-5', input: { width: 20, height: 8, elevation: 12, spacing: 8, numFrames: 3, phiStruct: 0.25, pipes: [{ id: 'p1', diameter: 2.0, elevationOffset: 8 }, { id: 'p2', diameter: 1.0, elevationOffset: 6 }] } },
{ name: 'Pipe-6', input: { width: 10, height: 5, elevation: 7, spacing: 3, numFrames: 6, phiStruct: 0.40, pipes: [{ id: 'p1', diameter: 0.8, elevationOffset: 5 }] } },
{ name: 'Pipe-7', input: { width: 6, height: 4, elevation: 4, spacing: 4, numFrames: 2, phiStruct: 0.50, pipes: [{ id: 'p1', diameter: 1.0, elevationOffset: 4 }] } },
{ name: 'Pipe-8', input: { width: 25, height: 10, elevation: 15, spacing: 10, numFrames: 4, phiStruct: 0.20, pipes: [{ id: 'p1', diameter: 3.0, elevationOffset: 10 }] } },
];
let pi_idx = 0;
for (const pp of piperackParams) {
const q = 0.981;
const input: PiperackInput = { ...pp.input, q };
const tr = calculatePiperack(input);
scenarios.push(makeScenario(
`piperack-varredura-${pi_idx++}`, 'piperack', `Piperack ${pp.name}: w=${input.width} h=${input.height} n=${input.numFrames}`,
{ ...input },
{ phiTotal: tr.phiTotal, effectiveElevation: tr.effectiveElevation },
{ caFrontal: tr.caFrontal, eta: tr.eta, globalForce: tr.globalForce },
{ phiTotal: { min: 0.1, max: 1.0 }, caFrontal: { min: 1.6, max: 3.0 }, eta: { min: 0.3, max: 1.0 }, globalForce: { min: 1, max: 5000 } },
'warehouse', // Usamos warehouse como fallback visual ou você pode ter um piperack
{ width: input.width, height: input.height, length: input.spacing * input.numFrames }, // Props mínimas
'Sec. 8.4', // Estruturas reticuladas
));
}
return scenarios;
}
+3 -1
View File
@@ -23,7 +23,8 @@ export type ModuleType =
| 'dynamics'
| 'cross-module'
| 'edge-case'
| 'cpi-variant';
| 'cpi-variant'
| 'piperack';
export const MODULE_LABELS: Record<ModuleType, { 'pt-BR': string; 'en-US': string }> = {
galpao: { 'pt-BR': 'Galpão', 'en-US': 'Warehouse' },
@@ -39,6 +40,7 @@ export const MODULE_LABELS: Record<ModuleType, { 'pt-BR': string; 'en-US': strin
'cross-module': { 'pt-BR': 'Cross-Module', 'en-US': 'Cross-Module' },
'edge-case': { 'pt-BR': 'Edge Cases', 'en-US': 'Edge Cases' },
'cpi-variant': { 'pt-BR': 'Cpi Variants', 'en-US': 'Cpi Variants' },
piperack: { 'pt-BR': 'Piperack', 'en-US': 'Piperack' },
};
export type DiagramType =
+2 -3
View File
@@ -17,7 +17,7 @@
import { getWallCpeNBR6123 } from './nbr-tables/table-6';
import { getRoofCpeNBR6123 } from './nbr-tables/table-7';
import { getShedRoofCpeNBR6123 } from './nbr-tables/table-8';
import { getShedRoofCpeNBR6123, type WindAngleT8 } from './nbr-tables/table-8';
import { getValleyRoofCpeNBR6123 } from './nbr-tables/table-9';
import { getMultiSpanSymmetricCpeNBR6123 } from './nbr-tables/table-10';
@@ -78,8 +78,7 @@ export function getRoofCpeOfficial(
return getRoofCpeNBR6123(h, b, theta, windAngle);
}
export function getShedRoofCpe(theta: number, windAngle: 0 | 90 | 180 | 270 = 0) {
// @ts-ignore
export function getShedRoofCpe(theta: number, windAngle: WindAngleT8 = 0) {
return getShedRoofCpeNBR6123(theta, windAngle);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function evaluateComfort(input: ComfortInput): ComfortResult {
description: 'Fora da faixa 0,061,00 Hz — aplicar critério da ISO 10137.',
};
}
const ka = use === 'commercial' ? 6.12 : 4.06;
const ka = use === 'commercial' ? 6.12 : 4.058;
const aLim = 0.01 * ka * Math.pow(freq, 1.124);
const ratio = aMax / aLim;
return {
+25 -12
View File
@@ -20,16 +20,18 @@ export const FRICTION_CF: Readonly<Record<SurfaceRoughness, number>> = {
export interface FrictionInput {
roughness: SurfaceRoughness;
/** Comprimento l0 da estrutura (m) */
/** Comprimento da estrutura (face a) (m) */
length: number;
/** Altura h */
height: number;
/** Largura b */
/** Largura da estrutura (face b) (m) */
width: number;
/** Inclinação do telhado (graus) */
roofPitch: number;
/** Pressão dinâmica q em kN/m² */
q: number;
/** Ângulo do vento: 0 (perpendicular à face a) ou 90 (perpendicular à face b) */
windAngle: 0 | 90;
}
export interface FrictionResult {
@@ -53,28 +55,39 @@ export function roofArea(a: number, b: number, pitchDeg: number): number {
}
export function calculateFriction(input: FrictionInput): FrictionResult {
const ratioLh = input.length / input.height;
const ratioLb = input.length / input.width;
const { length, width, height, roofPitch, windAngle, roughness, q } = input;
// l0: comprimento da superfície paralela ao vento
// b0: dimensão perpendicular ao vento
const l0 = windAngle === 90 ? length : width;
const b0 = windAngle === 90 ? width : length;
const ratioLh = l0 / height;
const ratioLb = l0 / b0;
const applies = ratioLh > 4 || ratioLb > 4;
const cf = FRICTION_CF[input.roughness];
const cf = FRICTION_CF[roughness];
if (!applies) {
return { applies, roofArea: 0, wallsArea: 0, cf, forceKN: 0 };
}
const roofAreaM2 = roofArea(input.length, input.width, input.roofPitch);
const roofSlant = roofAreaM2;
// Áreas das paredes paralelas ao vento
const wallsArea = 2 * l0 * height;
const wallAreaUpwind = input.height * input.length;
const wallAreaDownwind = input.height * input.length;
const totalArea = roofSlant + wallAreaUpwind + wallAreaDownwind;
let roofSlant = 0;
// A cobertura só é paralela ao vento se o vento soprar ao longo da cumeeira (windAngle = 90)
// ou se o telhado for plano (pitch = 0)
if (windAngle === 90 || roofPitch === 0) {
roofSlant = roofArea(length, width, roofPitch);
}
const forceKN = cf * input.q * totalArea;
const totalArea = roofSlant + wallsArea;
const forceKN = cf * q * totalArea;
return {
applies,
roofArea: roofSlant,
wallsArea: wallAreaUpwind + wallAreaDownwind,
wallsArea,
cf,
forceKN: Number(forceKN.toFixed(3)),
};
+19
View File
@@ -78,6 +78,25 @@ export function computeCpiSimplified(input: SimplifiedCpiInput): number {
}
}
/**
* Retorna os dois valores de Cpi que devem ser considerados para envoltória
* quando o caso de permeabilidade exige (sec. 6.3.2).
*
* Para `four-equally-permeable`, a norma exige considerar Cpi = 0.3 e Cpi = 0,
* adotando o mais desfavorável para cada verificação.
* Para os demais casos, retorna apenas o valor único.
*/
export function computeCpiEnvelope(input: SimplifiedCpiInput): readonly [number, number] {
if (input.case === 'four-equally-permeable') {
return [-0.3, 0] as const;
}
if (input.case === 'airtight') {
return [-0.2, 0] as const;
}
const val = computeCpiSimplified(input);
return [val, val] as const;
}
/** Cilindro sem aberturas e topo aberto (sec. 6.3.2.3) */
export function computeCpiCylinderOpenTop(hOverD: number): number {
if (hOverD >= 0.3) return -0.8;