feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Mudança de rugosidade do terreno — NBR 6123:2023, sec. 5.5
|
||||
*
|
||||
* Implementa o perfil de velocidades (fatores S₂) na zona de transição
|
||||
* entre duas categorias de rugosidade.
|
||||
*/
|
||||
|
||||
import type { TerrainCategory } from './wind-kernel';
|
||||
import { Z0_BY_CATEGORY, ZG_BY_CATEGORY } from './nbr-tables/table-5';
|
||||
|
||||
export interface RoughnessTransitionInput {
|
||||
/** Categoria da edificação (terreno próximo) */
|
||||
near: TerrainCategory;
|
||||
/** Categoria do terreno a barlavento (mais afastado) */
|
||||
far: TerrainCategory;
|
||||
/** Distância horizontal x a partir da linha de mudança (m) */
|
||||
distance: number;
|
||||
/** Altura z acima do terreno (m) */
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface RoughnessTransitionResult {
|
||||
z1: number;
|
||||
z2: number;
|
||||
/** S₂ equivalente na posição (x, z) */
|
||||
s2: number;
|
||||
}
|
||||
|
||||
/** Calcula A conforme sec. 5.5.1 ou 5.5.2 */
|
||||
function coefficientA(z02: number, z01: number, case2to1: boolean): number {
|
||||
if (case2to1) {
|
||||
return 0.63 - 0.03 * Math.log(z02 / z01);
|
||||
}
|
||||
return 0.73 - 0.03 * Math.log(z02 / z01);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cálculo da altura z₁ (topo da camada de transição — perfil da
|
||||
* categoria mais próxima). Caso 5.5.1: z_{02} > z_{01}.
|
||||
*/
|
||||
function computeZ1(z02: number, z01: number, x: number, case2to1: boolean): number {
|
||||
const A = coefficientA(z02, z01, case2to1);
|
||||
return A * z02 * Math.pow(x / z02, 0.8);
|
||||
}
|
||||
|
||||
/** Caso 5.5.2: z_{02} < z_{01} — calcula z₁ diferente */
|
||||
function computeZ1Case5_5_2(z02: number, x: number): number {
|
||||
const A = 0.73 - 0.03 * Math.log(z02 / 0.07);
|
||||
return A * z02 * Math.pow(x / z02, 0.8);
|
||||
}
|
||||
|
||||
/** S₂ da categoria para uma altura z (usa tabela de parâmetros) */
|
||||
function s2FromCategory(cat: TerrainCategory, z: number, params: Map<TerrainCategory, { b: number; p: number; fr: number }>): number {
|
||||
const p = params.get(cat);
|
||||
if (!p) throw new Error(`Categoria inválida: ${cat}`);
|
||||
const zEff = Math.max(5, Math.min(z, ZG_BY_CATEGORY[cat]));
|
||||
return p.b * p.fr * Math.pow(zEff / 10, p.p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula S₂ efetivo considerando mudança de rugosidade.
|
||||
* Requer tabela de parâmetros já construída pelo chamador (vide
|
||||
* `getDefaultParams` em wind-kernel).
|
||||
*/
|
||||
export function applyRoughnessChange(
|
||||
input: RoughnessTransitionInput,
|
||||
params: Map<TerrainCategory, { b: number; p: number; fr: number }>,
|
||||
): RoughnessTransitionResult {
|
||||
const { near, far, distance, height } = input;
|
||||
const z01 = Z0_BY_CATEGORY[near];
|
||||
const z02 = Z0_BY_CATEGORY[far];
|
||||
const x = Math.max(0, distance);
|
||||
|
||||
const case2to1 = z02 > z01;
|
||||
|
||||
let z1: number;
|
||||
if (case2to1) {
|
||||
z1 = computeZ1(z02, z01, x, true);
|
||||
} else {
|
||||
z1 = computeZ1Case5_5_2(z02, x);
|
||||
}
|
||||
|
||||
const s2Far = s2FromCategory(far, height, params);
|
||||
const s2Near = s2FromCategory(near, height, params);
|
||||
|
||||
let s2: number;
|
||||
if (height >= z1) {
|
||||
s2 = s2Far;
|
||||
} else {
|
||||
const z2 = case2to1
|
||||
? 0.36 * z02 * Math.pow(x / z02, 0.8)
|
||||
: 0.36 * z02 * Math.pow(x / z02, 0.8);
|
||||
if (height <= z2) {
|
||||
s2 = s2Near;
|
||||
} else {
|
||||
const t = (height - z2) / (z1 - z2);
|
||||
s2 = s2Near + t * (s2Far - s2Near);
|
||||
}
|
||||
}
|
||||
|
||||
return { z1, z2: 0.36 * Z0_BY_CATEGORY[far] * Math.pow(x / Z0_BY_CATEGORY[far], 0.8), s2 };
|
||||
}
|
||||
Reference in New Issue
Block a user