scripts/_dosm-product-demand.mjs
Rama main · réplica de lectura del repositorio original.
// Malaysia's refined-product demand from the Department of Statistics
// Malaysia (DOSM) open data API: annual final energy consumption by
// commodity (energy_consumption_commodity), in ktoe, from the Energy
// Commission's National Energy Balance. JODI carries Malaysia's product
// trade but no demand, so this is the energy shock's only demand for it.
//
// Final consumption excludes fuel burned for power generation and refinery
// own use. Values are converted to kb/d at IEA default net calorific values
// and standard barrels per tonne; the year's calendar days divide.
export const DOSM_ENERGY_CONSUMPTION_URL = 'https://api.data.gov.my/data-catalogue/?id=energy_consumption_commodity&limit=5000';
/** Energy shock product key → DOSM commodity. DOSM has no naphtha row. */
export const DOSM_PRODUCT_COMMODITIES = {
gasoline: 'petrol',
diesel: 'diesel',
jet: 'jet_fuel',
lpg: 'lpg',
};
const GJ_PER_TOE = 41.868;
// IEA default net calorific values (GJ/t) and barrels per tonne.
export const PRODUCT_CONVERSION = {
gasoline: { ncvGjPerT: 44.3, bblPerT: 8.53 },
diesel: { ncvGjPerT: 43.0, bblPerT: 7.46 },
jet: { ncvGjPerT: 44.1, bblPerT: 7.88 },
lpg: { ncvGjPerT: 46.0, bblPerT: 11.6 },
};
const daysInYear = (year) => ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 366 : 365);
/** Annual ktoe of one product to kb/d over that calendar year. */
export function ktoeToKbd(ktoe, product, year) {
const { ncvGjPerT, bblPerT } = PRODUCT_CONVERSION[product];
const kt = (ktoe * GJ_PER_TOE) / ncvGjPerT;
return (kt * bblPerT) / daysInYear(year);
}
/**
* The latest year in which DOSM has every modeled product, as kb/d (one
* decimal) and the ktoe it came from; null when no year is complete.
*
* @param {Array<{date?: string, commodity?: string, consumption?: unknown}>} rows
*/
export function dosmProductDemand(rows) {
const byYear = new Map();
for (const r of rows ?? []) {
const year = Number(String(r?.date ?? '').slice(0, 4));
const value = r?.consumption;
if (!Number.isInteger(year) || typeof value !== 'number' || !Number.isFinite(value) || value < 0) continue;
if (!byYear.has(year)) byYear.set(year, new Map());
byYear.get(year).set(String(r.commodity), value);
}
const entries = Object.entries(DOSM_PRODUCT_COMMODITIES);
for (const year of [...byYear.keys()].sort((a, b) => b - a)) {
const values = byYear.get(year);
if (!entries.every(([, commodity]) => values.get(commodity) > 0)) continue;
const ktoe = Object.fromEntries(entries.map(([key, commodity]) => [key, values.get(commodity)]));
const products = Object.fromEntries(entries.map(([key]) => [key, Math.round(ktoeToKbd(ktoe[key], key, year) * 10) / 10]));
return { year, ktoe, products };
}
return null;
}