feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
/**
* Strategy para cilindros de seção circular (NBR 6123:2023, sec. 6.2.1).
*
* Casos cobertos:
* - Silos / reservatórios / chaminés (eixo vertical)
* - Tubulações aéreas (eixo horizontal)
* - Topo aberto (Cpi específico pela Tabela 13 / sec. 6.3.2.3)
*/
import { getCpeCylinder, reynoldsCylinder, isSupercritical } from '../nbr-tables/table-13';
import { computeCpiCylinderOpenTop } from '../internal-pressure';
import { clampCpi } from '../internal-pressure';
export type CylinderEndType = 'closed' | 'open-top' | 'open-bottom' | 'open-both';
export interface CylinderInput {
/** Diâmetro (m) */
d: number;
/** Altura (m) */
h: number;
/** Velocidade característica Vk (m/s) */
vk: number;
/** Tipo de superfície */
surface: 'rough' | 'smooth';
/** Tipo de extremidade */
endType: CylinderEndType;
/** Cpi base (usado se fechado) */
baseCpi?: number;
}
export interface CylinderPoint {
angle: number;
cpe: number;
pressureKN_m2: number;
}
export interface CylinderResult {
re: number;
supercritical: boolean;
hOverD: number;
cpi: number;
cpiNote: string;
profile: CylinderPoint[];
/** Força horizontal total por unidade de altura (kN/m) — integração numérica */
forcePerHeightKN_m: number;
}
/** Integração numérica da força de arrasto em torno do cilindro */
function integrateCylinderForce(
profile: CylinderPoint[],
d: number,
): number {
let total = 0;
for (let i = 0; i < profile.length - 1; i++) {
const a = profile[i];
const b = profile[i + 1];
const da = (b.angle - a.angle) * Math.PI / 180;
const avg = (a.pressureKN_m2 + b.pressureKN_m2) / 2;
const radius = d / 2;
total += avg * da * radius;
}
return Number(total.toFixed(3));
}
export function calculateCylinder(input: CylinderInput): CylinderResult {
const { d, h, vk, surface, endType } = input;
const hOverD = h / d;
const re = reynoldsCylinder(vk, d);
const supercritical = isSupercritical(re);
let cpi = input.baseCpi ?? 0;
let cpiNote = 'Edição fechada — usando Cpi global.';
if (endType === 'open-top') {
cpi = clampCpi(computeCpiCylinderOpenTop(hOverD));
cpiNote = `Topo aberto: Cpi = ${cpi} (sec. 6.3.2.3, h/d = ${hOverD.toFixed(2)}).`;
} else if (endType === 'open-bottom') {
cpi = -0.5;
cpiNote = 'Base aberta: Cpi = 0,5 (conservador).';
} else if (endType === 'open-both') {
cpi = -0.7;
cpiNote = 'Topo e base abertos: Cpi = 0,7 (conservador).';
}
const angles = [0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180];
const profile: CylinderPoint[] = angles.map((angle) => {
const cpe = getCpeCylinder(angle, hOverD, surface);
const p = (0.613 * Math.pow(vk, 2) * (cpe - cpi)) / 1000; // kN/m²
return { angle, cpe, pressureKN_m2: Number(p.toFixed(3)) };
});
const forcePerHeightKN_m = integrateCylinderForce(profile, d);
return {
re,
supercritical,
hOverD,
cpi,
cpiNote,
profile,
forcePerHeightKN_m,
};
}