🚀 Auto-deploy: BrainWind atualizado em 14/07/2026 11:50:17
This commit is contained in:
@@ -27,6 +27,7 @@ const MODULE_ROUTES: RouteModuleMap = {
|
||||
'cross-module': '/galpao',
|
||||
'edge-case': '/galpao',
|
||||
'cpi-variant': '/galpao',
|
||||
piperack: '/piperack',
|
||||
};
|
||||
|
||||
interface AuditDetailPopupProps {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function evaluateComfort(input: ComfortInput): ComfortResult {
|
||||
description: 'Fora da faixa 0,06–1,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
@@ -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)),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user