102 lines
2.9 KiB
TypeScript
102 lines
2.9 KiB
TypeScript
/**
|
||
* 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,
|
||
};
|
||
} |