specpress
Version:
Export PDF and/or DOCX files from a subset of Markdown, ASN.1 and JSON files
473 lines • 20.2 kB
JavaScript
// ChannelBandwidthPerBand.ts — Channel bandwidth data model
import { LoadJsonFileToDict } from "./JsonTools.js";
import { BaseClass, BaseList, KEYS, UnsupportedKeyException, InvalidInputTypeException } from "./Utils.js";
import { logger } from "./Logger.js";
import { HtmlTable } from "./HtmlTable.js";
import { BandNumber } from "./BandNumber.js";
import { SCS } from "./SubcarrierSpacing.js";
import { join } from "node:path";
//////////////////////////////
// Constants
/** Maximum supported carrier bandwidth in MHz. */
export const MAX_CARRIER_BW_MHZ = 2400;
//////////////////////////////
// Exceptions
/** Thrown when a carrier bandwidth value is out of range. */
export class InvalidCarrierBandwidthException extends Error {
constructor(message) { super(message); this.name = "InvalidCarrierBandwidthException"; }
}
/** Thrown when a required SCS value is missing. */
export class MissingScsException extends Error {
constructor(message) { super(message); this.name = "MissingScsException"; }
}
/** Thrown when a required bandwidth list is missing. */
export class MissingBandwidthListException extends Error {
constructor(message) { super(message); this.name = "MissingBandwidthListException"; }
}
/** Thrown when a required band number is missing. */
export class MissingBandNumberException extends Error {
constructor(message) { super(message); this.name = "MissingBandNumberException"; }
}
/** Thrown when a required SCS list is missing. */
export class MissingScsListException extends Error {
constructor(message) { super(message); this.name = "MissingScsListException"; }
}
/** Thrown when the specification field does not match the expected value. */
export class InvalidSpecificationException extends Error {
constructor(message) { super(message); this.name = "InvalidSpecificationException"; }
}
/** Enumeration of uplink/downlink support types for a channel bandwidth. */
export class ENUMS {
static OPTIONAL = "optional";
static MANDATORY = "mandatory";
static UNDEFINED = "undefined";
static SCELL_ONLY = "scell_only";
}
const VALID_UL_DL_TYPES = [ENUMS.OPTIONAL, ENUMS.MANDATORY, ENUMS.UNDEFINED, ENUMS.SCELL_ONLY];
////////////////////////////////////////////////////////////////
// ChannelBandwidth //
////////////////////////////////////////////////////////////////
/**
* A single channel bandwidth entry with uplink/downlink support type.
*
* Represents one supported bandwidth value (in MHz) for a given band and SCS,
* along with whether uplink and downlink are mandatory, optional, undefined,
* or SCell-only.
*/
export class ChannelBandwidth extends BaseClass {
/** Bandwidth in MHz. */
bw = 0.0;
/** Uplink support type (mandatory, optional, undefined, scell_only). */
uplink = ENUMS.MANDATORY;
/** Downlink support type (mandatory, optional, undefined, scell_only). */
downlink = ENUMS.MANDATORY;
/**
* @param aValue — a dict from JSON, an existing ChannelBandwidth to copy, or a plain number.
* @param aParent — the parent SCSEntry.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof ChannelBandwidth) {
this.bw = aValue.bw;
this.uplink = aValue.uplink;
this.downlink = aValue.downlink;
}
else if (typeof aValue === "number") {
this.bw = aValue;
}
else if (typeof aValue === "object" && aValue !== null) {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.bw) {
this.bw = aValue[oneKey];
}
else if (oneKey === KEYS.uplink) {
this.uplink = aValue[oneKey];
}
else if (oneKey === KEYS.downlink) {
this.downlink = aValue[oneKey];
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
else {
throw new InvalidInputTypeException(`Cannot initialize ChannelBandwidth with an input type of ${typeof aValue}`);
}
}
/** Validates that bw is in range and uplink/downlink types are valid. */
validate() {
if (this.bw === null || this.bw === undefined || this.bw <= 0.0 || this.bw > MAX_CARRIER_BW_MHZ) {
throw new InvalidCarrierBandwidthException(`Carrier bandwdiths must be in the range (0..${MAX_CARRIER_BW_MHZ}] but was ${this.bw}.`);
}
if (!VALID_UL_DL_TYPES.includes(this.uplink)) {
throw new Error(`The type '${this.uplink}' is not valid for Bandwidth->uplink.`);
}
if (!VALID_UL_DL_TYPES.includes(this.downlink)) {
throw new Error(`The type '${this.downlink}' is not valid for Bandwidth->downlink.`);
}
return this;
}
getTag() { return `${this.bw}`; }
/** Sets the uplink support type. @returns this for chaining. */
setUplink(aType) {
if (!VALID_UL_DL_TYPES.includes(aType)) {
throw new Error(`The type '${aType}' is not valid for Bandwidth->uplink.`);
}
if (aType === ENUMS.UNDEFINED && this.downlink === ENUMS.UNDEFINED) {
throw new Error(`Uplink cannot be ${ENUMS.UNDEFINED} if downlink is ${ENUMS.UNDEFINED}`);
}
this.uplink = aType;
return this;
}
/** Returns the uplink support type. */
getUplink() { return this.uplink; }
/** Sets the downlink support type. @returns this for chaining. */
setDownlink(aType) {
if (!VALID_UL_DL_TYPES.includes(aType)) {
throw new Error(`The type '${aType}' is not valid for Bandwidth->downlink.`);
}
if (aType === ENUMS.UNDEFINED && this.uplink === ENUMS.UNDEFINED) {
throw new Error(`Donwlink cannot be ${ENUMS.UNDEFINED} if uplink is ${ENUMS.UNDEFINED}`);
}
this.downlink = aType;
return this;
}
/** Returns the downlink support type. */
getDownlink() { return this.downlink; }
/** Returns the bandwidth as a float (MHz). */
asFloat() { return this.bw; }
toString() {
let s = `${this.bw}`;
if (this.uplink !== ENUMS.MANDATORY || this.downlink !== ENUMS.MANDATORY) {
if (this.uplink === ENUMS.MANDATORY)
s += "(u";
else if (this.uplink === ENUMS.OPTIONAL)
s += "((u)|";
else if (this.uplink === ENUMS.SCELL_ONLY)
s += "((us)|";
else
s += "(-|";
if (this.downlink === ENUMS.MANDATORY)
s += "d)";
else if (this.downlink === ENUMS.OPTIONAL)
s += "(d))";
else if (this.downlink === ENUMS.SCELL_ONLY)
s += "(ds))";
else
s += "-)";
}
return s;
}
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
anEncoder.writeKeyAndValue(KEYS.bw, this.bw, aLevel + 1, "", ",\n");
anEncoder.writeKeyAndValue(KEYS.uplink, this.uplink, aLevel + 1, "", ",\n");
anEncoder.writeKeyAndValue(KEYS.downlink, this.downlink, aLevel + 1, "", "\n");
anEncoder.write("}", aLevel);
}
}
////////////////////////////////////////////////////////////////
// SCSEntry //
////////////////////////////////////////////////////////////////
/**
* Groups a subcarrier spacing value with its list of supported channel bandwidths.
*
* Each SCSEntry belongs to a ChBwOneBand and contains one SCS value
* (e.g. 15 kHz, 30 kHz) and the bandwidths supported at that SCS.
*/
export class SCSEntry extends BaseClass {
/** The subcarrier spacing in kHz. */
scs;
/** The list of supported channel bandwidths at this SCS. */
bandwidthList = [];
/**
* @param aValue — a dict from JSON or an existing SCSEntry to copy.
* @param aParent — the parent ChBwOneBand.
*/
constructor(aValue, aParent = null) {
super(aParent);
if (aValue instanceof SCSEntry) {
this.scs = aValue.scs;
for (const oneBw of aValue.bandwidthList) {
this.bandwidthList.push(new ChannelBandwidth(oneBw, this));
}
}
else if (typeof aValue === "object" && aValue !== null) {
for (const oneKey of Object.keys(aValue)) {
if (oneKey === KEYS.scs) {
this.scs = new SCS(aValue[KEYS.scs]);
}
else if (oneKey === KEYS.bandwidthList) {
for (const oneBw of aValue[KEYS.bandwidthList]) {
this.bandwidthList.push(new ChannelBandwidth(oneBw, this));
}
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
else {
throw new InvalidInputTypeException(`Cannot initialize SCSEntry with an input type of ${typeof aValue}`);
}
}
/** Validates that scs and bandwidthList are present, and validates each bandwidth. */
validate() {
if (this.scs === null || this.scs === undefined) {
throw new MissingScsException(`${this.getDescriptor()}: Shall contain a 'scs' key`);
}
if (this.bandwidthList === null || this.bandwidthList === undefined) {
throw new MissingBandwidthListException(`${this.getDescriptor()}: Shall contain a 'bandwidthList' key`);
}
for (const oneBW of this.bandwidthList) {
oneBW.validate();
}
return this;
}
/** Returns true if the given bandwidth (MHz) is in this SCS entry's bandwidth list. */
isBandwidthSupported(aBandwidth) {
for (const oneBW of this.bandwidthList) {
if (oneBW.asFloat() === aBandwidth)
return true;
}
return false;
}
getTag() { return `${+this.scs}`; }
toString() { return `${+this.scs} kHz: ${this.bandwidthList}`; }
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
anEncoder.writeKeyAndValue(KEYS.scs, +this.scs, aLevel + 1, "", ",\n");
anEncoder.writeKeyAndValue(KEYS.bandwidthList, this.bandwidthList, aLevel + 1, "", "\n");
anEncoder.write("}", aLevel);
}
toHTML(aHtmlTable, aRow = 0, aColumn = 0) {
aHtmlTable.setValue(aRow, aColumn, `${+this.scs}`);
aHtmlTable.setValue(aRow, aColumn + 1, this.bandwidthList.map(b => b.toString()).join(", "));
}
}
////////////////////////////////////////////////////////////////
// ChBwOneBand //
////////////////////////////////////////////////////////////////
/**
* Channel bandwidth definition for one NR band.
*
* Contains the band number, a list of SCS entries (each with its supported
* bandwidths), and optional notes. Corresponds to one JSON file in the
* `FR1_NR_bands` or `FR2_NR_bands` folders.
*/
export class ChBwOneBand extends BaseClass {
/** The NR band number (e.g. n1, n78). */
bandNumber = null;
/** SCS entries with their supported bandwidths. */
scsList = [];
/** Optional specification notes. */
notes = {};
/** Specification number (e.g. "38.101-1"). */
specification = null;
/** Schema version. */
schemaVersion = null;
/**
* @param aDict — a dict from JSON or an existing ChBwOneBand to copy.
* @param aParent — the parent ChannelBandwidthList.
*/
constructor(aDict, aParent = null) {
super(aParent);
if (aDict instanceof ChBwOneBand) {
this.bandNumber = aDict.bandNumber;
for (const oneElement of aDict.scsList) {
this.scsList.push(new SCSEntry(oneElement, this));
}
this.notes = { ...aDict.notes };
this.specification = aDict.specification;
this.schemaVersion = aDict.schemaVersion;
}
else {
for (const oneKey of Object.keys(aDict)) {
if (oneKey === KEYS.bandNumber) {
this.bandNumber = new BandNumber(aDict[KEYS.bandNumber]);
}
else if (oneKey === KEYS.scsList) {
for (const oneScsEntry of aDict[KEYS.scsList]) {
this.scsList.push(new SCSEntry(oneScsEntry, this));
}
}
else if (oneKey === KEYS.notes) {
for (const oneNote of Object.keys(aDict[KEYS.notes])) {
this.notes[oneNote] = true;
}
}
else if (oneKey === KEYS.specification) {
this.specification = aDict[KEYS.specification];
}
else if (oneKey === KEYS.schemaVersion) {
this.schemaVersion = aDict[KEYS.schemaVersion];
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Does not support the given key '${oneKey}' as element`);
}
}
}
}
/** Validates that bandNumber and scsList are present, and validates each SCS entry. */
validate() {
if (this.bandNumber === null) {
throw new MissingBandNumberException(`${this.getDescriptor()}: Shall contain a 'bandNumber' key`);
}
const expectedSpec = this.bandNumber.getSpecification();
if (this.specification === null) {
this.specification = expectedSpec;
}
else if (this.specification !== expectedSpec) {
throw new InvalidSpecificationException(`${this.getDescriptor()}: specification is '${this.specification}' but expected '${expectedSpec}' based on band number.`);
}
if (this.scsList.length === 0) {
throw new MissingScsListException(`${this.getDescriptor()}: Shall contain a 'scsList' key`);
}
for (const oneScsEntry of this.scsList) {
oneScsEntry.validate();
}
return this;
}
/** Returns true if the given bandwidth (MHz) is supported by any SCS entry in this band. */
isBandwidthSupported(aBandwidth) {
for (const oneScsEntry of this.scsList) {
if (oneScsEntry.isBandwidthSupported(aBandwidth))
return true;
}
return false;
}
getTag() {
if (this.bandNumber !== null)
return this.bandNumber.toString();
return `${this}`;
}
toString() {
if (this.bandNumber === null)
return "ChBwOneBand";
return this.bandNumber.toString();
}
toJSON(anEncoder, aLevel) {
anEncoder.write("{\n", aLevel);
if (this.specification !== null) {
anEncoder.writeKeyAndValue(KEYS.specification, this.specification, aLevel + 1, "", ",\n");
}
if (this.schemaVersion !== null) {
anEncoder.writeKeyAndValue(KEYS.schemaVersion, this.schemaVersion, aLevel + 1, "", ",\n");
}
anEncoder.writeKeyAndValue(KEYS.bandNumber, this.bandNumber.toString(), aLevel + 1, "", ",\n");
anEncoder.writeKeyAndValue(KEYS.scsList, this.scsList, aLevel + 1);
if (Object.keys(this.notes).length > 0) {
anEncoder.writeKeyAndValue(KEYS.notes, this.notes, aLevel + 1, ",\n");
}
anEncoder.write("\n}", aLevel);
}
toHTML(aHtmlTable, aRow = 0, aColumn = 0) {
let rowIndexToUse = aHtmlTable.getNrofRows();
aHtmlTable.setValue(rowIndexToUse, aColumn, this.bandNumber.toString());
for (const oneSCS of this.scsList) {
oneSCS.toHTML(aHtmlTable, rowIndexToUse, aColumn + 1);
rowIndexToUse = aHtmlTable.getNrofRows();
}
for (const oneNote of Object.keys(this.notes)) {
aHtmlTable.setValue(rowIndexToUse, aColumn + 2, oneNote);
rowIndexToUse = aHtmlTable.getNrofRows();
}
}
}
////////////////////////////////////////////////////////////////
// ChannelBandwidthList //
////////////////////////////////////////////////////////////////
/**
* Collection of channel bandwidth definitions for all NR bands.
*
* Keyed by band number string (e.g. `"n1"`, `"n78"`). Supports loading from
* individual JSON files or a single multi-band file, validation, and export
* to JSON or HTML.
*/
export class ChannelBandwidthList extends BaseList {
/** HTML table used for HTML export. */
html;
constructor(aParent = null) {
super(aParent);
this.html = new HtmlTable();
}
_createEntry(aValue, aParent) {
return new ChBwOneBand(aValue, aParent);
}
_getEntryId(anEntry) {
return anEntry.bandNumber.toString();
}
/** Returns true if the given band number exists in this list. */
hasBand(aBandNumber) {
return this.has(String(aBandNumber));
}
/**
* Returns true if the given bandwidth is supported for the given band.
* @param aBandNumber — the band to check.
* @param aBandwidth — the bandwidth in MHz.
*/
isChBwSupported(aBandNumber, aBandwidth) {
if (!this.hasBand(aBandNumber))
return false;
return this.get(String(aBandNumber)).isBandwidthSupported(aBandwidth);
}
/** Loads channel bandwidths from a single JSON file containing a bandList array. */
loadFromFile(aJsonFile) {
const tempDict = LoadJsonFileToDict(aJsonFile);
logger.log(`${this.getDescriptor()}: Adding channel bandwidths.`);
for (const oneKey of Object.keys(tempDict)) {
if (oneKey === KEYS.bandList) {
this.addAll(tempDict[KEYS.bandList]);
}
else {
throw new UnsupportedKeyException(`${this.getDescriptor()}: Uknown key: '${oneKey}'`);
}
}
logger.log(`${this.getDescriptor()}: Imported ${tempDict[KEYS.bandList].length} Channel BWs. Validating table...`);
this.validate();
logger.log(`${this.getDescriptor()}: Successfully imported ${tempDict[KEYS.bandList].length} Channel Bandwidths from file '${aJsonFile}' and validated the entire table with ${this.data.size} Channel Bandwidths.`);
}
_getTargetSubfolder(anEntry) {
const chBw = anEntry;
if (chBw.bandNumber.isFr1()) {
return join("ts-38.101-1", "operating bands");
}
return join("ts-38.101-2", "operating bands");
}
_getFileName(anEntry) {
return `${anEntry.getTag()}.json`;
}
/** Exports all channel bandwidths to an HTML table file, sorted by band number. */
storeAsHtmlFile(aFileName) {
ChannelBandwidthList.addTableHeaders(this.html);
const sorted = [...this.data.values()].sort((a, b) => +a.bandNumber - +b.bandNumber);
for (const oneChBw of sorted) {
oneChBw.toHTML(this.html);
}
this.html.dump(aFileName);
logger.log(`${this.getDescriptor()}: Wrote ${this.data.size} channel bandwidths to HTML file '${aFileName}'`);
}
/** Adds standard band table headers to the given HTML table. */
static addTableHeaders(aHtmlTable) {
aHtmlTable.setValue(0, 0, 'Band');
aHtmlTable.setValue(0, 1, 'SCS [kHz]');
aHtmlTable.setValue(0, 2, 'Bandwidths [MHz]');
}
/**
* Renders a single band as a complete HTML table string.
* @param data — Raw JSON object containing band data
* @returns HTML table string with headers and one band row
*/
static renderAsHtml(data) {
const band = new ChBwOneBand(data);
const htmlTable = new HtmlTable();
ChannelBandwidthList.addTableHeaders(htmlTable);
band.toHTML(htmlTable);
return htmlTable.toHtmlString();
}
toJSON(anEncoder, aLevel) {
throw new Error("ChannelBandwidthList.toJSON() is not implemented");
}
}
//# sourceMappingURL=ChannelBandwidthPerBand.js.map