Skip to content

Commit 0c06b30

Browse files
FIREFLY-1694: Add aliases for flux units
1 parent 2a5a6b4 commit 0c06b30

File tree

1 file changed

+76
-9
lines changed

1 file changed

+76
-9
lines changed

src/firefly/js/charts/dataTypes/SpectrumUnitConversion.js

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ const UnitXref = {
221221
* @typedef {string} MeasurementKey - unique identifier for a measurement type, used as a key in Measurement.
222222
*/
223223

224-
/**
224+
/**
225225
* @typedef {Object} Measurement
226226
* @property {MeasurementKey} key - the key of the measurement type
227227
* @property {string} value - the value of the measurement type
@@ -245,8 +245,7 @@ const Measurement = {
245245
* @property {MeasurementKey} type - the type of measurement this unit belongs to, one of the keys in `Measurement`.
246246
* @property {string} label - the label for the unit, used in dropdown options and axis labels. If undefined, the key
247247
* is used as the label.
248-
* @property {Array<string>} aliases - an object containing aliases for the unit, used for case-insensitive matching. If
249-
* undefined, only the key (and label) is used for matching.
248+
* @property {Array<string>} aliases - list of aliases for the unit, used for matching besides the key and label.
250249
*/
251250

252251
/**
@@ -271,15 +270,15 @@ const UnitMetadata = {
271270
A : {
272271
type: Measurement.LAMBDA.key,
273272
label: WAVELENGTH_UNITS.angstrom.symbol,
274-
aliases: ['angstrom', 'angstroms'] //case-insensitive
273+
aliases: ['Angstrom', 'angstrom']
275274
},
276275
nm : {
277276
type: Measurement.LAMBDA.key,
278277
},
279278
um : {
280279
type: Measurement.LAMBDA.key,
281280
label: WAVELENGTH_UNITS.um.symbol,
282-
aliases: ['micron', 'microns'] //case-insensitive
281+
aliases: ['micron', 'microns']
283282
},
284283
mm : {
285284
type: Measurement.LAMBDA.key,
@@ -294,7 +293,6 @@ const UnitMetadata = {
294293
'W/m^2/Hz' : {
295294
type: Measurement.F_NU.key,
296295
label: 'W/m²/Hz',
297-
aliases: [], //TODO: generate aliases in dot product notation with **, as it is with ** for powers
298296
},
299297
'erg/s/cm^2/Hz' : {
300298
type: Measurement.F_NU.key,
@@ -329,7 +327,7 @@ const UnitMetadata = {
329327

330328

331329
/**
332-
* Maps any unit value/label/alias back to a key in UnitXref (and UnitMetadata).
330+
* Maps any unit representation (value/label/alias) back to a key in UnitXref (and UnitMetadata).
333331
* @param u {string} - the unit to normalize
334332
* @return {UnitKey|null} - the key in UnitXref if found, otherwise null
335333
*/
@@ -339,11 +337,80 @@ function normalizeUnit(u) {
339337
if (UnitXref[u]) return u;
340338

341339
for (const [key, meta] of Object.entries(UnitMetadata)) {
342-
// u is a case-insensitive alias of a key in UnitXref
343-
if (meta?.aliases?.some((alias) => alias.toLowerCase() === u.toLowerCase())) return key;
340+
// u is an alias of a key in UnitXref
341+
const aliases = meta?.aliases ?? [];
342+
aliases.push(...getFluxAliases(key));
343+
if (aliases.some((alias) => alias === u)) return key;
344344

345345
// u is a label of a key in UnitXref
346346
if (meta?.label === u) return key;
347347
}
348348
return null;
349349
}
350+
351+
/* Get all possible representations (key, label, aliases) for a given unitKey */
352+
function getAllRepresentations(unitKey, includeLabel=true) {
353+
const meta = UnitMetadata[unitKey];
354+
if (!meta) return []; //invalid unitKey
355+
const reps = [unitKey]; // include the key itself
356+
if (includeLabel && meta.label && meta.label !== unitKey) reps.push(meta.label);
357+
if (Array.isArray(meta.aliases)) reps.push(...meta.aliases);
358+
return reps;
359+
}
360+
361+
/**
362+
* Decomposes a unit string into its base units and their powers.
363+
* @param unit {string} - the unit string to decompose, e.g. 'erg/s/cm^2/A'
364+
* @returns {Map<any, any>} - a Map where keys are base units and values are their powers, e.g.
365+
* `Map{'erg'=> 1, 's'=> -1, 'cm'=> -2, 'A'=> -1}`
366+
*/
367+
function decomposeUnit(unit) {
368+
const pattern = /([*./]?)([A-Za-z]+)(?:\^([+-]?\d+))?/g;
369+
const result = new Map(); // to preserve the order of units
370+
const matches = unit.matchAll(pattern);
371+
for (const match of matches) {
372+
const [, operator, baseUnit, exponent] = match;
373+
let power = exponent ? Number(exponent) : 1;
374+
if (operator === '/') power = -power; // '/' implies negative exponent, '*' or '.' implies positive exponent
375+
const prevPower = result.get(baseUnit) || 0;
376+
result.set(baseUnit, prevPower + power);
377+
}
378+
return result;
379+
}
380+
381+
/**
382+
* Get all possible aliases for a flux unit, this includes permutations of its expression notations and its consisting
383+
* units' aliases.
384+
* @param unitKey {UnitKey}
385+
* @returns {Array<string>}
386+
*/
387+
const getFluxAliases = (unitKey) => {
388+
if (!UnitMetadata[unitKey]?.type.startsWith('F')) return []; //not a flux unit
389+
390+
let fluxRepresentations;
391+
if (UnitMetadata[unitKey]?.type.startsWith('F_')) { //flux density (in nu or lambda)
392+
const nuOrLambdaUnitKey = unitKey.substring(unitKey.lastIndexOf('/')+1); // extract the last part after '/'
393+
// nu or lambda's representations propagate in the flux density representations
394+
// e.g. erg/s/cm^2/A -> erg/s/cm^2/Angstrom, erg/s/cm^2/angstrom, because of the 'A' unit's alternative representations
395+
fluxRepresentations = getAllRepresentations(nuOrLambdaUnitKey, false).map(
396+
(rep) => unitKey.replace(nuOrLambdaUnitKey, rep));
397+
}
398+
else {
399+
fluxRepresentations = [unitKey]; // inband flux remains the same since it is independent of nu or lambda
400+
}
401+
402+
const aliases = [];
403+
for (const fluxUnit of fluxRepresentations) { // generate all possible expression notations for the flux unit
404+
const asteriskPowerExpr = fluxUnit.replaceAll('^', '**'); // e.g. erg/s/cm**2/A
405+
const invisiblePowerExpr = fluxUnit.replaceAll('^', ''); // e.g. erg/s/cm2/A
406+
407+
const multiplicationExpr = Array.from(decomposeUnit(fluxUnit).entries())
408+
.map(([unit, power]) => power === 1 ? unit : `${unit}**${power}`)
409+
.join('.'); // e.g. erg.s**-1.cm**-2.A**-1
410+
411+
// TODO: also support numerical scale-factor in section 2.10 in https://ivoa.net/documents/VOUnits/20231215/REC-VOUnits-1.1.pdf
412+
// possibly alias function can return a boolean to do such matching
413+
aliases.push(fluxUnit, asteriskPowerExpr, invisiblePowerExpr, multiplicationExpr);
414+
}
415+
return Array.from(new Set(aliases)); // remove duplicates
416+
};

0 commit comments

Comments
 (0)