@vessel-dsp/compiler 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/bbd-clock.d.ts.map +1 -1
- package/dist/bbd-clock.js +5 -0
- package/dist/bbd-clock.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +305 -12
- package/dist/compile.js.map +1 -1
- package/dist/device-laws.d.ts.map +1 -1
- package/dist/device-laws.js +552 -97
- package/dist/device-laws.js.map +1 -1
- package/dist/fixtures/circuits.d.ts +23 -2
- package/dist/fixtures/circuits.d.ts.map +1 -1
- package/dist/fixtures/circuits.js +205 -2
- package/dist/fixtures/circuits.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +4 -0
- package/dist/lower.js.map +1 -1
- package/dist/netlist.d.ts +52 -0
- package/dist/netlist.d.ts.map +1 -1
- package/dist/netlist.js +585 -13
- package/dist/netlist.js.map +1 -1
- package/dist/part-catalog.d.ts +35 -6
- package/dist/part-catalog.d.ts.map +1 -1
- package/dist/part-catalog.js +2544 -443
- package/dist/part-catalog.js.map +1 -1
- package/dist/registry-data/bjt.d.ts +3 -0
- package/dist/registry-data/bjt.d.ts.map +1 -0
- package/dist/registry-data/bjt.js +1350 -0
- package/dist/registry-data/bjt.js.map +1 -0
- package/dist/registry-data/charge-pump.d.ts +3 -0
- package/dist/registry-data/charge-pump.d.ts.map +1 -0
- package/dist/registry-data/charge-pump.js +204 -0
- package/dist/registry-data/charge-pump.js.map +1 -0
- package/dist/registry-data/diode.d.ts +3 -0
- package/dist/registry-data/diode.d.ts.map +1 -0
- package/dist/registry-data/diode.js +683 -0
- package/dist/registry-data/diode.js.map +1 -0
- package/dist/registry-data/index.d.ts +20 -0
- package/dist/registry-data/index.d.ts.map +1 -0
- package/dist/registry-data/index.js +100 -0
- package/dist/registry-data/index.js.map +1 -0
- package/dist/registry-data/jfet.d.ts +3 -0
- package/dist/registry-data/jfet.d.ts.map +1 -0
- package/dist/registry-data/jfet.js +605 -0
- package/dist/registry-data/jfet.js.map +1 -0
- package/dist/registry-data/loader.d.ts +137 -0
- package/dist/registry-data/loader.d.ts.map +1 -0
- package/dist/registry-data/loader.js +350 -0
- package/dist/registry-data/loader.js.map +1 -0
- package/dist/registry-data/mosfet.d.ts +3 -0
- package/dist/registry-data/mosfet.d.ts.map +1 -0
- package/dist/registry-data/mosfet.js +63 -0
- package/dist/registry-data/mosfet.js.map +1 -0
- package/dist/registry-data/opamp.d.ts +37 -0
- package/dist/registry-data/opamp.d.ts.map +1 -0
- package/dist/registry-data/opamp.js +669 -0
- package/dist/registry-data/opamp.js.map +1 -0
- package/dist/registry-data/regulator.d.ts +3 -0
- package/dist/registry-data/regulator.d.ts.map +1 -0
- package/dist/registry-data/regulator.js +189 -0
- package/dist/registry-data/regulator.js.map +1 -0
- package/dist/registry-data/schema.d.ts +56 -0
- package/dist/registry-data/schema.d.ts.map +1 -0
- package/dist/registry-data/schema.js +142 -0
- package/dist/registry-data/schema.js.map +1 -0
- package/dist/registry-data/zener.d.ts +3 -0
- package/dist/registry-data/zener.d.ts.map +1 -0
- package/dist/registry-data/zener.js +547 -0
- package/dist/registry-data/zener.js.map +1 -0
- package/dist/registry.d.ts +72 -5
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +65 -6
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +255 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/netlist.js
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
import { componentDevices, deviceTerminalRoles, parseInterchangeYamlWithTopology, } from "@vessel-dsp/core";
|
|
19
19
|
import { load as loadYaml } from "js-yaml";
|
|
20
20
|
import { StageRefusal } from "./errors.js";
|
|
21
|
-
import { foldToken } from "./registry.js";
|
|
21
|
+
import { emptyRegistry, foldToken, registryEntryFor } from "./registry.js";
|
|
22
|
+
import { classParamNames, requiredClassParams, } from "./registry-data/index.js";
|
|
22
23
|
import { findBistableLatches } from "./latch-seed.js";
|
|
23
24
|
import { speakerOnePort } from "./speaker-load.js";
|
|
24
25
|
import { GROUND, } from "./types.js";
|
|
@@ -632,6 +633,7 @@ export function readNetlist(source, options) {
|
|
|
632
633
|
? declaredNodesFromConnectivity(document, parsed.connectivity)
|
|
633
634
|
: new Map();
|
|
634
635
|
const nodeOf = resolveNodes(document, declaredNodes);
|
|
636
|
+
const registry = options?.registry ?? emptyRegistry;
|
|
635
637
|
const warnings = [];
|
|
636
638
|
/**
|
|
637
639
|
* **A switch throw or winding tap alone on its node cannot route anything.**
|
|
@@ -931,7 +933,11 @@ export function readNetlist(source, options) {
|
|
|
931
933
|
packageDeviceId: declared.id,
|
|
932
934
|
kind: deviceKind,
|
|
933
935
|
nodes: owned.map((entry) => entry.node),
|
|
934
|
-
|
|
936
|
+
...applyDeviceOverride(component, deviceKind, parametersFor(component, deviceKind), {
|
|
937
|
+
warnings,
|
|
938
|
+
registry,
|
|
939
|
+
deviceId: `${component.id}.${declared.id}`,
|
|
940
|
+
}),
|
|
935
941
|
// The control belongs to the package, so only the first device carries it.
|
|
936
942
|
// A ganged part states the same control on both and the owner map already
|
|
937
943
|
// keeps the first.
|
|
@@ -946,7 +952,11 @@ export function readNetlist(source, options) {
|
|
|
946
952
|
id: String(component.id),
|
|
947
953
|
kind,
|
|
948
954
|
nodes,
|
|
949
|
-
|
|
955
|
+
...applyDeviceOverride(component, kind, parametersFor(component, kind), {
|
|
956
|
+
warnings,
|
|
957
|
+
registry,
|
|
958
|
+
deviceId: String(component.id),
|
|
959
|
+
}),
|
|
950
960
|
control,
|
|
951
961
|
identity: identityOf(effectiveTerminals, effectiveTerminals.map((terminal) => declaredRole(terminal))),
|
|
952
962
|
isLed: componentKind === "led" ? true : undefined,
|
|
@@ -1098,7 +1108,7 @@ export function readNetlist(source, options) {
|
|
|
1098
1108
|
device: owner.id,
|
|
1099
1109
|
declared: componentTaper.unreadable,
|
|
1100
1110
|
detail: `Control ${id}: ${owner.id} declares taper "${componentTaper.unreadable}", which ` +
|
|
1101
|
-
"this stage cannot turn into one of its
|
|
1111
|
+
"this stage cannot turn into one of its laws, so the control renders " +
|
|
1102
1112
|
"linear. An audio pot rendered linear is audible across the whole sweep.",
|
|
1103
1113
|
});
|
|
1104
1114
|
}
|
|
@@ -1998,6 +2008,152 @@ kind) {
|
|
|
1998
2008
|
? value
|
|
1999
2009
|
: null;
|
|
2000
2010
|
}
|
|
2011
|
+
/** Hertz per frequency unit, compared whole. `mHz` and any other spelling are refused, not guessed. */
|
|
2012
|
+
const frequencyUnitScale = {
|
|
2013
|
+
"": 1,
|
|
2014
|
+
Hz: 1,
|
|
2015
|
+
hz: 1,
|
|
2016
|
+
HZ: 1,
|
|
2017
|
+
kHz: 1e3,
|
|
2018
|
+
KHz: 1e3,
|
|
2019
|
+
khz: 1e3,
|
|
2020
|
+
MHz: 1e6,
|
|
2021
|
+
GHz: 1e9,
|
|
2022
|
+
};
|
|
2023
|
+
/**
|
|
2024
|
+
* A declared frequency, in hertz, or `null` when the component states none.
|
|
2025
|
+
*
|
|
2026
|
+
* **The whole value is read, and a value that is not a frequency is refused rather than skipped.**
|
|
2027
|
+
* Three forms are legal and each is checked end to end:
|
|
2028
|
+
*
|
|
2029
|
+
* - a number, taken as hertz;
|
|
2030
|
+
* - a string that is exactly a number and an optional frequency unit -- `"3 MHz"`, `"1.2 MHz"`,
|
|
2031
|
+
* `"3000000"`, `"3e6 Hz"` -- where a missing unit means hertz and the unit is compared as a closed
|
|
2032
|
+
* set (`Hz`, `kHz`, `MHz`, `GHz`), never by stripping after a delimiter;
|
|
2033
|
+
* - a structured `{value, unit}`, whose `value` wins over `raw` because it already carries the
|
|
2034
|
+
* conversion (the same reason `Aol` reads `value`), with the unit from the same closed set.
|
|
2035
|
+
*
|
|
2036
|
+
* `parseQuantity` is not used here on purpose. It reads a number then ignores whatever follows, so
|
|
2037
|
+
* `"1.2 V/us"` -- a slew rate -- would come back as 1.2: the right number for the wrong reason, which
|
|
2038
|
+
* is the trap the `SR` note above describes. A frequency property that holds `"3 MHz typ"`, `"3M"` or
|
|
2039
|
+
* a slew rate is a packet error, so this throws and names the component instead of dropping it.
|
|
2040
|
+
*
|
|
2041
|
+
* A non-positive or non-finite frequency is refused for the same reason: it states something, and
|
|
2042
|
+
* what it states is not a frequency.
|
|
2043
|
+
*/
|
|
2044
|
+
export function typedFrequencyHz(component, key) {
|
|
2045
|
+
const property = component.properties[key];
|
|
2046
|
+
if (property === undefined || property === null) {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
const refuse = (why) => {
|
|
2050
|
+
throw new NetlistError(`${component.id}: ${key} ${JSON.stringify(property)} ${why}; a declared frequency must be a number, a number with Hz/kHz/MHz/GHz, or a {value, unit}`);
|
|
2051
|
+
};
|
|
2052
|
+
let hertz;
|
|
2053
|
+
if (typeof property === "number") {
|
|
2054
|
+
hertz = property;
|
|
2055
|
+
}
|
|
2056
|
+
else if (typeof property === "string") {
|
|
2057
|
+
const text = property
|
|
2058
|
+
.trim()
|
|
2059
|
+
.replace(/^['"]+|['"]+$/gu, "")
|
|
2060
|
+
.trim();
|
|
2061
|
+
if (text === "") {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
const match = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)\s*([A-Za-z]*)$/u.exec(text);
|
|
2065
|
+
const scale = match === null ? undefined : frequencyUnitScale[match[2]];
|
|
2066
|
+
if (match === null || scale === undefined) {
|
|
2067
|
+
return refuse("is not a number with a frequency unit");
|
|
2068
|
+
}
|
|
2069
|
+
hertz = Number(match[1]) * scale;
|
|
2070
|
+
}
|
|
2071
|
+
else if (typeof property === "object" && !Array.isArray(property)) {
|
|
2072
|
+
const { value, unit } = property;
|
|
2073
|
+
if (typeof value !== "number") {
|
|
2074
|
+
return refuse("has no numeric value");
|
|
2075
|
+
}
|
|
2076
|
+
const token = typeof unit === "string" ? unit.replaceAll("'", "").trim() : "";
|
|
2077
|
+
const scale = frequencyUnitScale[token];
|
|
2078
|
+
if (scale === undefined) {
|
|
2079
|
+
return refuse(`has unit "${token}", which is not a frequency`);
|
|
2080
|
+
}
|
|
2081
|
+
hertz = value * scale;
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
return refuse("is not a number, string or structured quantity");
|
|
2085
|
+
}
|
|
2086
|
+
if (!Number.isFinite(hertz) || hertz <= 0) {
|
|
2087
|
+
return refuse("is not a positive finite frequency");
|
|
2088
|
+
}
|
|
2089
|
+
return hertz;
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* The bounds of a declared diode `EmissionCoefficient` (the SPICE `N`, the ideality factor): 1, the
|
|
2093
|
+
* Shockley limit nothing physical sits below, to 10, beyond which the part is not one junction. A
|
|
2094
|
+
* value outside them is refused by name, never clamped.
|
|
2095
|
+
*/
|
|
2096
|
+
export const EMISSION_COEFFICIENT_MIN = 1;
|
|
2097
|
+
export const EMISSION_COEFFICIENT_MAX = 10;
|
|
2098
|
+
/**
|
|
2099
|
+
* A diode's declared `EmissionCoefficient`, or `null` when the component states none.
|
|
2100
|
+
*
|
|
2101
|
+
* **The whole value is read, and a value that is not an emission coefficient is refused rather than
|
|
2102
|
+
* skipped or clamped** -- the `typedFrequencyHz` pattern for a dimensionless number. Legal forms,
|
|
2103
|
+
* each checked end to end: a bare number; a string that is exactly a number (`"1.9"`, `"1.9e0"`,
|
|
2104
|
+
* nothing after it: not `"1.9 typ"`, not `"1.9n"`); and a structured `{value, unit}` whose `value`
|
|
2105
|
+
* wins over `raw` and whose unit must be empty. The property is read under this one spelling only
|
|
2106
|
+
* (the corpus has one); nothing here reads a part number, a name or a description.
|
|
2107
|
+
*
|
|
2108
|
+
* A property that is present and is not a number in `EMISSION_COEFFICIENT_MIN` to
|
|
2109
|
+
* `EMISSION_COEFFICIENT_MAX` is a packet error, so this throws and names the component and the
|
|
2110
|
+
* offending value. It states something, and what it states is not an ideality factor.
|
|
2111
|
+
*/
|
|
2112
|
+
export function typedEmissionCoefficient(component) {
|
|
2113
|
+
const key = "EmissionCoefficient";
|
|
2114
|
+
const property = component.properties[key];
|
|
2115
|
+
if (property === undefined || property === null) {
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
const refuse = (why) => {
|
|
2119
|
+
throw new NetlistError(`${component.id}: ${key} ${JSON.stringify(property)} ${why}; a declared emission coefficient must be a number from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}, bare, as a number-only string, or as a {value, unit: ""}`, String(component.id));
|
|
2120
|
+
};
|
|
2121
|
+
let n;
|
|
2122
|
+
if (typeof property === "number") {
|
|
2123
|
+
n = property;
|
|
2124
|
+
}
|
|
2125
|
+
else if (typeof property === "string") {
|
|
2126
|
+
const text = property
|
|
2127
|
+
.trim()
|
|
2128
|
+
.replace(/^['"]+|['"]+$/gu, "")
|
|
2129
|
+
.trim();
|
|
2130
|
+
if (text === "") {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u.test(text)) {
|
|
2134
|
+
return refuse("is not a number");
|
|
2135
|
+
}
|
|
2136
|
+
n = Number(text);
|
|
2137
|
+
}
|
|
2138
|
+
else if (typeof property === "object" && !Array.isArray(property)) {
|
|
2139
|
+
const { value, unit } = property;
|
|
2140
|
+
if (typeof value !== "number") {
|
|
2141
|
+
return refuse("has no numeric value");
|
|
2142
|
+
}
|
|
2143
|
+
const token = typeof unit === "string" ? unit.replaceAll("'", "").trim() : "";
|
|
2144
|
+
if (token !== "") {
|
|
2145
|
+
return refuse(`has unit "${token}", which is not dimensionless`);
|
|
2146
|
+
}
|
|
2147
|
+
n = value;
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
return refuse("is not a number, string or structured quantity");
|
|
2151
|
+
}
|
|
2152
|
+
if (!Number.isFinite(n) || n < EMISSION_COEFFICIENT_MIN || n > EMISSION_COEFFICIENT_MAX) {
|
|
2153
|
+
return refuse(`is not a finite number from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}`);
|
|
2154
|
+
}
|
|
2155
|
+
return n;
|
|
2156
|
+
}
|
|
2001
2157
|
/**
|
|
2002
2158
|
* Parse a quantity to SI base units. A value that cannot be parsed is an error, not a
|
|
2003
2159
|
* default -- a silently defaulted component is a silently wrong circuit.
|
|
@@ -2074,6 +2230,396 @@ export function parseQuantity(raw) {
|
|
|
2074
2230
|
}
|
|
2075
2231
|
return value * multiplier;
|
|
2076
2232
|
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Stage 2: the explicit device-parameter override block.
|
|
2235
|
+
*
|
|
2236
|
+
* A `.vdsp` NAMES the part; the registry holds its parameters. A component may carry one
|
|
2237
|
+
* `DeviceOverride` object in its `properties` stating measured values for this device
|
|
2238
|
+
* (`kind: "measured"`, allowed on any part) or the full parameter set for a part the
|
|
2239
|
+
* registry does not have (`kind: "custom"`). Parameter names are the part's OWN class-schema
|
|
2240
|
+
* names (`CLASS_PARAM_SCHEMAS` for its class); values are plain SI numbers, one per parameter.
|
|
2241
|
+
*
|
|
2242
|
+
* Placement is a nested structured property -- not a new component field -- so no
|
|
2243
|
+
* `circuit-interchange` schema version, no `Component` type, no parser/serializer and no
|
|
2244
|
+
* core change was needed: nested objects round-trip through `parsePropertyValue` /
|
|
2245
|
+
* `propertiesBlock` untouched. An override changes only law numbers that already flow into
|
|
2246
|
+
* program blocks the way today's legacy typed keys do, so `Program`, the stamps and the
|
|
2247
|
+
* consoles are untouched too.
|
|
2248
|
+
*
|
|
2249
|
+
* Precedence, decided under the brief: override > legacy typed key > registry > class default.
|
|
2250
|
+
* Legacy reading is untouched (REMOVAL is stage 4, not this task); while it is still read,
|
|
2251
|
+
* every device carrying a still-read legacy key gets the additive `legacy-device-parameter`
|
|
2252
|
+
* warning, and every applied override gets the additive `registry-override-applied` notice
|
|
2253
|
+
* (emitted in `compile.ts`, where the registry is in scope).
|
|
2254
|
+
*
|
|
2255
|
+
* Every refusal names the component, the parameter and the rule (`NetlistError` carries the
|
|
2256
|
+
* component id, and `compile` turns it into an `unsupported` result at stage `netlist`).
|
|
2257
|
+
*/
|
|
2258
|
+
const DEVICE_OVERRIDE_KEY = "DeviceOverride";
|
|
2259
|
+
/**
|
|
2260
|
+
* The schema class an override block on this device is checked against, or `null` when the
|
|
2261
|
+
* device kind takes no registry-class parameters and an override block is an error.
|
|
2262
|
+
*
|
|
2263
|
+
* `diode` device kind covers three classes with the same five names: zener when the component
|
|
2264
|
+
* declares itself one (`Type: Zener`, the only two corpus spellings, or the canonical
|
|
2265
|
+
* `Circuit.ZenerDiode` source type), led when the component kind is `led`, else diode. Only
|
|
2266
|
+
* the *required set* differs between them (a zener additionally requires `breakdownVolts`,
|
|
2267
|
+
* because without it the law stamps an ordinary diode).
|
|
2268
|
+
*/
|
|
2269
|
+
function overrideTargetClass(component, deviceKind) {
|
|
2270
|
+
switch (deviceKind) {
|
|
2271
|
+
case "bjt":
|
|
2272
|
+
return "bjt";
|
|
2273
|
+
case "jfet":
|
|
2274
|
+
return "jfet";
|
|
2275
|
+
case "mosfet":
|
|
2276
|
+
return "mosfet";
|
|
2277
|
+
case "diode": {
|
|
2278
|
+
const declaredType = propertyString(component, "Type");
|
|
2279
|
+
if (declaredType === "Zener" ||
|
|
2280
|
+
declaredType === "zener" ||
|
|
2281
|
+
component.sourceTypeName === "Circuit.ZenerDiode") {
|
|
2282
|
+
return "zener";
|
|
2283
|
+
}
|
|
2284
|
+
if (String(component.kind) === "led") {
|
|
2285
|
+
return "led";
|
|
2286
|
+
}
|
|
2287
|
+
return "diode";
|
|
2288
|
+
}
|
|
2289
|
+
case "opamp":
|
|
2290
|
+
return "opamp";
|
|
2291
|
+
default:
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Legacy typed device-parameter keys this stage still reads, per device kind. Only these warn:
|
|
2297
|
+
* keys the compiler never reads (`SR`, `Rout`, `Rin`, `Vsat`, a diode's `IS`/`n`, ...) change
|
|
2298
|
+
* nothing when deleted and belong to the migration table, not to a compile warning.
|
|
2299
|
+
*/
|
|
2300
|
+
const LEGACY_DEVICE_PARAM_KEYS = {
|
|
2301
|
+
bjt: ["IS", "BF", "BR", "LeakageCurrent"],
|
|
2302
|
+
jfet: [
|
|
2303
|
+
"Vt0",
|
|
2304
|
+
"VTO",
|
|
2305
|
+
"ThresholdVoltage",
|
|
2306
|
+
"thresholdVolts",
|
|
2307
|
+
"Beta",
|
|
2308
|
+
"BETA",
|
|
2309
|
+
"Transconductance",
|
|
2310
|
+
"transconductance",
|
|
2311
|
+
],
|
|
2312
|
+
mosfet: [
|
|
2313
|
+
"Vt0",
|
|
2314
|
+
"VTO",
|
|
2315
|
+
"ThresholdVoltage",
|
|
2316
|
+
"thresholdVolts",
|
|
2317
|
+
"Beta",
|
|
2318
|
+
"BETA",
|
|
2319
|
+
"Transconductance",
|
|
2320
|
+
"transconductance",
|
|
2321
|
+
],
|
|
2322
|
+
diode: [
|
|
2323
|
+
"ReverseBreakdownVoltage",
|
|
2324
|
+
"BreakdownVoltage",
|
|
2325
|
+
"ZenerVoltage",
|
|
2326
|
+
"SaturationCurrent",
|
|
2327
|
+
"SeriesResistance",
|
|
2328
|
+
"OhmicResistance",
|
|
2329
|
+
"RS",
|
|
2330
|
+
"Rs",
|
|
2331
|
+
"EmissionCoefficient",
|
|
2332
|
+
],
|
|
2333
|
+
opamp: ["Aol", "GBP"],
|
|
2334
|
+
};
|
|
2335
|
+
/**
|
|
2336
|
+
* The engine field a legacy key feeds, for the superseded computation. A legacy key is
|
|
2337
|
+
* superseded when the override supplies the same engine field -- compared on fields, not on
|
|
2338
|
+
* spellings, so the compiler holds no legacy-name to schema-name translation table (that
|
|
2339
|
+
* table lives only in the step-3 migration script).
|
|
2340
|
+
*/
|
|
2341
|
+
const LEGACY_KEY_ENGINE_FIELD = {
|
|
2342
|
+
IS: "saturationCurrent",
|
|
2343
|
+
BF: "beta",
|
|
2344
|
+
BR: "reverseBeta",
|
|
2345
|
+
LeakageCurrent: "leakageAmps",
|
|
2346
|
+
Vt0: "thresholdVolts",
|
|
2347
|
+
VTO: "thresholdVolts",
|
|
2348
|
+
ThresholdVoltage: "thresholdVolts",
|
|
2349
|
+
thresholdVolts: "thresholdVolts",
|
|
2350
|
+
Beta: "transconductance",
|
|
2351
|
+
BETA: "transconductance",
|
|
2352
|
+
Transconductance: "transconductance",
|
|
2353
|
+
transconductance: "transconductance",
|
|
2354
|
+
ReverseBreakdownVoltage: "breakdownVolts",
|
|
2355
|
+
BreakdownVoltage: "breakdownVolts",
|
|
2356
|
+
ZenerVoltage: "breakdownVolts",
|
|
2357
|
+
SaturationCurrent: "saturationCurrent",
|
|
2358
|
+
SeriesResistance: "seriesResistance",
|
|
2359
|
+
OhmicResistance: "seriesResistance",
|
|
2360
|
+
RS: "seriesResistance",
|
|
2361
|
+
Rs: "seriesResistance",
|
|
2362
|
+
EmissionCoefficient: "emissionCoefficient",
|
|
2363
|
+
Aol: "openLoopGain",
|
|
2364
|
+
GBP: "gainBandwidthHz",
|
|
2365
|
+
};
|
|
2366
|
+
/**
|
|
2367
|
+
* The engine field an override schema name writes. Every schema name already IS the engine
|
|
2368
|
+
* field except the BJT's gain: the document-typed legacy key is `BF`, the engine field is
|
|
2369
|
+
* `beta` (read by `device-laws.ts` and the provisional-value suppression), and the schema
|
|
2370
|
+
* name is `forwardBeta`.
|
|
2371
|
+
*/
|
|
2372
|
+
function overrideEngineField(target, name) {
|
|
2373
|
+
if (target === "bjt" && name === "forwardBeta") {
|
|
2374
|
+
return "beta";
|
|
2375
|
+
}
|
|
2376
|
+
return name;
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* The class parameters an override block may actually set, by target class: the subset of
|
|
2380
|
+
* the class schema that the engine reads from the document.
|
|
2381
|
+
*
|
|
2382
|
+
* A schema name outside this set is refused even though it is a real class parameter, because
|
|
2383
|
+
* accepting it would be a silent drop: `device-laws.ts` reads a JFET's gate terms
|
|
2384
|
+
* (`gateSaturationCurrent`, `gateOnsetVolts`, `gateScaleVolts`), its
|
|
2385
|
+
* `channelLengthModulation` and `subthresholdVolts`, and an op-amp's output-swing pair ONLY
|
|
2386
|
+
* from the registry entry, never from `device.parameters`. An override value for one of those
|
|
2387
|
+
* would merge, stamp nothing, and change nothing -- the wrong-answer-that-renders this file
|
|
2388
|
+
* exists to prevent. Those numbers come from registry entries (stage 3 fills them), never
|
|
2389
|
+
* from a per-device override; polarity and channel stay structural for the same reason.
|
|
2390
|
+
*/
|
|
2391
|
+
const OVERRIDABLE_CLASS_PARAMS = {
|
|
2392
|
+
bjt: [
|
|
2393
|
+
"saturationCurrent",
|
|
2394
|
+
"forwardBeta",
|
|
2395
|
+
"reverseBeta",
|
|
2396
|
+
"thermalVoltage",
|
|
2397
|
+
"leakageAmps",
|
|
2398
|
+
],
|
|
2399
|
+
jfet: ["thresholdVolts", "transconductance"],
|
|
2400
|
+
mosfet: ["thresholdVolts", "transconductance"],
|
|
2401
|
+
diode: [
|
|
2402
|
+
"saturationCurrent",
|
|
2403
|
+
"emissionCoefficient",
|
|
2404
|
+
"thermalVoltage",
|
|
2405
|
+
"breakdownVolts",
|
|
2406
|
+
"seriesResistance",
|
|
2407
|
+
],
|
|
2408
|
+
zener: [
|
|
2409
|
+
"saturationCurrent",
|
|
2410
|
+
"emissionCoefficient",
|
|
2411
|
+
"thermalVoltage",
|
|
2412
|
+
"breakdownVolts",
|
|
2413
|
+
"seriesResistance",
|
|
2414
|
+
],
|
|
2415
|
+
led: [
|
|
2416
|
+
"saturationCurrent",
|
|
2417
|
+
"emissionCoefficient",
|
|
2418
|
+
"thermalVoltage",
|
|
2419
|
+
"breakdownVolts",
|
|
2420
|
+
"seriesResistance",
|
|
2421
|
+
],
|
|
2422
|
+
opamp: ["openLoopGain", "gainBandwidthHz"],
|
|
2423
|
+
};
|
|
2424
|
+
/** A non-empty authored string, or `null` when the value is not one. */
|
|
2425
|
+
function nonEmptyText(value) {
|
|
2426
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
return value.trim();
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Check one override value: finite, and positive (or bounded) where the parameter is
|
|
2433
|
+
* physically required to be. Names the component, the parameter and the rule.
|
|
2434
|
+
*/
|
|
2435
|
+
function checkOverrideValue(componentId, target, name, value) {
|
|
2436
|
+
const refuse = (rule) => {
|
|
2437
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" ${JSON.stringify(value)} ${rule}`, componentId);
|
|
2438
|
+
};
|
|
2439
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2440
|
+
refuse("is not a finite SI number");
|
|
2441
|
+
}
|
|
2442
|
+
const n = value;
|
|
2443
|
+
switch (name) {
|
|
2444
|
+
case "emissionCoefficient":
|
|
2445
|
+
if (n < EMISSION_COEFFICIENT_MIN || n > EMISSION_COEFFICIENT_MAX) {
|
|
2446
|
+
refuse(`is not an ideality factor from ${EMISSION_COEFFICIENT_MIN} to ${EMISSION_COEFFICIENT_MAX}`);
|
|
2447
|
+
}
|
|
2448
|
+
return n;
|
|
2449
|
+
case "leakageAmps":
|
|
2450
|
+
case "breakdownVolts":
|
|
2451
|
+
case "seriesResistance":
|
|
2452
|
+
case "channelLengthModulation":
|
|
2453
|
+
case "subthresholdVolts":
|
|
2454
|
+
case "gateSaturationCurrent":
|
|
2455
|
+
if (n < 0) {
|
|
2456
|
+
refuse(`is negative, and a negative ${name} is not physical`);
|
|
2457
|
+
}
|
|
2458
|
+
return n;
|
|
2459
|
+
case "thresholdVolts":
|
|
2460
|
+
case "gateOnsetVolts":
|
|
2461
|
+
// Sign carries the physics here (depletion negative, enhancement positive, and
|
|
2462
|
+
// p-channel JFETs pinch off positive), so any finite value -- including zero --
|
|
2463
|
+
// parses, exactly as the legacy `Vt0` reader accepts any quantity.
|
|
2464
|
+
return n;
|
|
2465
|
+
default:
|
|
2466
|
+
// saturationCurrent, forwardBeta, reverseBeta, thermalVoltage, transconductance,
|
|
2467
|
+
// gateScaleVolts, openLoopGain, gainBandwidthHz, volts, sourceOhms,
|
|
2468
|
+
// chargePumpLossVolts: zero or negative is not a value for any of them.
|
|
2469
|
+
if (!(n > 0)) {
|
|
2470
|
+
refuse(`is not a positive finite number, which ${name} must be`);
|
|
2471
|
+
}
|
|
2472
|
+
return n;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Parse and validate the `DeviceOverride` block on this component, or return null when it
|
|
2477
|
+
* declares none. Throws `NetlistError` (naming component, parameter and rule) on every
|
|
2478
|
+
* malformed block: unknown kind, missing/malformed provenance, malformed parameter list,
|
|
2479
|
+
* unknown name for the class, duplicate parameter, non-physical value, `custom` on a known
|
|
2480
|
+
* part, or `custom` missing a required parameter. Override on a device kind that takes no
|
|
2481
|
+
* registry-class parameters is an error, so a block on a resistor or an IC can never sit
|
|
2482
|
+
* silently unread.
|
|
2483
|
+
*/
|
|
2484
|
+
function readDeviceOverride(component, deviceKind, registry) {
|
|
2485
|
+
const raw = component.properties[DEVICE_OVERRIDE_KEY];
|
|
2486
|
+
if (raw === undefined || raw === null) {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
const componentId = String(component.id);
|
|
2490
|
+
const target = overrideTargetClass(component, deviceKind);
|
|
2491
|
+
if (target === null) {
|
|
2492
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride is not supported for ${deviceKind} devices (bjt, jfet, mosfet, diode and opamp only)`, componentId);
|
|
2493
|
+
}
|
|
2494
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
2495
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride must be an object with kind, provenance and parameters, not ${JSON.stringify(raw)}`, componentId);
|
|
2496
|
+
}
|
|
2497
|
+
const block = raw;
|
|
2498
|
+
const kind = block.kind;
|
|
2499
|
+
if (kind !== "measured" && kind !== "custom") {
|
|
2500
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.kind ${JSON.stringify(kind)} is not "measured" or "custom"`, componentId);
|
|
2501
|
+
}
|
|
2502
|
+
const source = nonEmptyText(block.source);
|
|
2503
|
+
if (source === null) {
|
|
2504
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.source is missing (what was measured, or which document the numbers come from)`, componentId);
|
|
2505
|
+
}
|
|
2506
|
+
const method = nonEmptyText(block.method);
|
|
2507
|
+
if (method === null) {
|
|
2508
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.method is missing (instrument/procedure for measured, why the registry lacks the part for custom)`, componentId);
|
|
2509
|
+
}
|
|
2510
|
+
const dateRaw = nonEmptyText(block.date);
|
|
2511
|
+
if (dateRaw === null ||
|
|
2512
|
+
!/^\d{4}-\d{2}-\d{2}$/u.test(dateRaw) ||
|
|
2513
|
+
Number.isNaN(Date.parse(`${dateRaw}T00:00:00Z`))) {
|
|
2514
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.date ${JSON.stringify(block.date)} is not an ISO date (YYYY-MM-DD)`, componentId);
|
|
2515
|
+
}
|
|
2516
|
+
const by = nonEmptyText(block.by);
|
|
2517
|
+
if (by === null) {
|
|
2518
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.by is missing (a named person or organisation)`, componentId);
|
|
2519
|
+
}
|
|
2520
|
+
const conditions = block.conditions;
|
|
2521
|
+
if (conditions !== undefined && typeof conditions !== "string") {
|
|
2522
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.conditions must be free text, not ${JSON.stringify(conditions)}`, componentId);
|
|
2523
|
+
}
|
|
2524
|
+
const list = block.parameters;
|
|
2525
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
2526
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.parameters must be a non-empty list of {name, value}`, componentId);
|
|
2527
|
+
}
|
|
2528
|
+
const allowed = new Set(classParamNames(target));
|
|
2529
|
+
const overridable = new Set(OVERRIDABLE_CLASS_PARAMS[target]);
|
|
2530
|
+
const seen = new Set();
|
|
2531
|
+
const parameters = [];
|
|
2532
|
+
for (const [index, entry] of list.entries()) {
|
|
2533
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2534
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride.parameters[${index}] must be {name, value}, not ${JSON.stringify(entry)}`, componentId);
|
|
2535
|
+
}
|
|
2536
|
+
const record = entry;
|
|
2537
|
+
const name = record.name;
|
|
2538
|
+
if (typeof name !== "string" || !allowed.has(name)) {
|
|
2539
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride parameter ${JSON.stringify(name)} is not a ${target} parameter (accepted: ${[...allowed].join(", ")})`, componentId);
|
|
2540
|
+
}
|
|
2541
|
+
if (!overridable.has(name)) {
|
|
2542
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" is a ${target} parameter but the engine reads it only from the registry entry, never from the document, so an override cannot set it (overridable: ${[...overridable].join(", ")})`, componentId);
|
|
2543
|
+
}
|
|
2544
|
+
if (seen.has(name)) {
|
|
2545
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride parameter "${name}" is stated twice (one value per parameter)`, componentId);
|
|
2546
|
+
}
|
|
2547
|
+
seen.add(name);
|
|
2548
|
+
const value = checkOverrideValue(componentId, target, name, record.value);
|
|
2549
|
+
parameters.push({ name, value });
|
|
2550
|
+
}
|
|
2551
|
+
if (kind === "custom") {
|
|
2552
|
+
const partNumber = propertyString(component, "PartNumber");
|
|
2553
|
+
if (partNumber !== null &&
|
|
2554
|
+
registryEntryFor(registry, { partId: partNumber, evidence: "exact-part" }) !== null) {
|
|
2555
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride kind "custom" on part "${partNumber}", which the registry already has (use kind "measured", or remove the block)`, componentId);
|
|
2556
|
+
}
|
|
2557
|
+
const missing = requiredClassParams(target).filter((name) => !seen.has(name));
|
|
2558
|
+
if (missing.length > 0) {
|
|
2559
|
+
throw new NetlistError(`component ${componentId}: DeviceOverride kind "custom" for class ${target} is missing required parameter(s): ${missing.join(", ")}`, componentId);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const provenance = {
|
|
2563
|
+
kind,
|
|
2564
|
+
source,
|
|
2565
|
+
method,
|
|
2566
|
+
date: dateRaw,
|
|
2567
|
+
by,
|
|
2568
|
+
...(conditions === undefined ? {} : { conditions }),
|
|
2569
|
+
};
|
|
2570
|
+
return { override: { provenance, parameters }, target };
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Merge an explicit override over this device's legacy typed parameters (override wins),
|
|
2574
|
+
* and emit the additive `legacy-device-parameter` warning for every legacy key still read.
|
|
2575
|
+
* No override block: legacy parameters pass through bit-identically and only the warning
|
|
2576
|
+
* is new.
|
|
2577
|
+
*/
|
|
2578
|
+
function applyDeviceOverride(component, deviceKind, legacy, context) {
|
|
2579
|
+
const read = readDeviceOverride(component, deviceKind, context.registry);
|
|
2580
|
+
const legacyKeys = (LEGACY_DEVICE_PARAM_KEYS[deviceKind] ?? []).filter((key) => component.properties[key] !== undefined);
|
|
2581
|
+
if (read === null) {
|
|
2582
|
+
if (legacyKeys.length > 0) {
|
|
2583
|
+
const warning = {
|
|
2584
|
+
code: "legacy-device-parameter",
|
|
2585
|
+
device: context.deviceId,
|
|
2586
|
+
keys: legacyKeys,
|
|
2587
|
+
overridden: [],
|
|
2588
|
+
detail: `${context.deviceId} still types device parameters under legacy keys (${legacyKeys.join(", ")}), ` +
|
|
2589
|
+
`which the compiler still reads and the stage-4 removal will stop reading: ` +
|
|
2590
|
+
`migrate them to a DeviceOverride block (measured with provenance, or custom ` +
|
|
2591
|
+
`for a part the registry lacks).`,
|
|
2592
|
+
};
|
|
2593
|
+
context.warnings.push(warning);
|
|
2594
|
+
}
|
|
2595
|
+
return { parameters: legacy, deviceOverride: undefined };
|
|
2596
|
+
}
|
|
2597
|
+
const { override, target } = read;
|
|
2598
|
+
const values = { ...legacy };
|
|
2599
|
+
const overrideFields = new Set(override.parameters.map((parameter) => overrideEngineField(target, parameter.name)));
|
|
2600
|
+
for (const parameter of override.parameters) {
|
|
2601
|
+
values[overrideEngineField(target, parameter.name)] =
|
|
2602
|
+
parameter.value;
|
|
2603
|
+
}
|
|
2604
|
+
const overridden = legacyKeys.filter((key) => {
|
|
2605
|
+
const field = LEGACY_KEY_ENGINE_FIELD[key];
|
|
2606
|
+
return field !== undefined && overrideFields.has(field);
|
|
2607
|
+
});
|
|
2608
|
+
const warning = {
|
|
2609
|
+
code: "legacy-device-parameter",
|
|
2610
|
+
device: context.deviceId,
|
|
2611
|
+
keys: legacyKeys,
|
|
2612
|
+
overridden,
|
|
2613
|
+
detail: overridden.length > 0
|
|
2614
|
+
? `${context.deviceId} applies a ${override.provenance.kind} DeviceOverride that supersedes legacy keys (${overridden.join(", ")}); the override values win. ` +
|
|
2615
|
+
`Remaining legacy keys (${legacyKeys.filter((key) => !overridden.includes(key)).join(", ") || "none"}) still read as before.`
|
|
2616
|
+
: `${context.deviceId} applies a ${override.provenance.kind} DeviceOverride alongside legacy keys (${legacyKeys.join(", ") || "none"}); no legacy key feeds an overridden parameter, so all legacy values still read.`,
|
|
2617
|
+
};
|
|
2618
|
+
if (legacyKeys.length > 0) {
|
|
2619
|
+
context.warnings.push(warning);
|
|
2620
|
+
}
|
|
2621
|
+
return { parameters: values, deviceOverride: override };
|
|
2622
|
+
}
|
|
2077
2623
|
function parametersFor(component, kind) {
|
|
2078
2624
|
// A supply rail asserts a potential at its node. Most declare one -- 84 of the
|
|
2079
2625
|
// corpus's 90 -- and the six that do not are named support shells with nothing to
|
|
@@ -2224,10 +2770,18 @@ function parametersFor(component, kind) {
|
|
|
2224
2770
|
// with the decibel conversion done -- `{raw: "92 dB", value: 39810, unit: ""}`. So the typed
|
|
2225
2771
|
// `value` is taken as-is and nothing here converts from dB, which would double-apply it.
|
|
2226
2772
|
//
|
|
2227
|
-
// `GBP
|
|
2228
|
-
// dominant pole
|
|
2229
|
-
//
|
|
2230
|
-
//
|
|
2773
|
+
// `GBP`, the gain-bandwidth product, is read as a typed frequency. It was left unread while no
|
|
2774
|
+
// dominant pole existed for it to feed; the pole landed 2026-08-21 and both consoles now take
|
|
2775
|
+
// the product from the stamp. It is stated in several forms across the corpus -- `"3 MHz"`,
|
|
2776
|
+
// `"3000000"`, `1.2 MHz`, and structured `{value, unit}` -- so it goes through
|
|
2777
|
+
// `typedFrequencyHz`, which reads the unit whole and refuses what it cannot read. Nothing here
|
|
2778
|
+
// falls back to a number: a packet that states none leaves the field absent, and the catalog's
|
|
2779
|
+
// datasheet figure for the exact part id, else `null`, is decided in `device-laws.ts`.
|
|
2780
|
+
//
|
|
2781
|
+
// `SR` stays unread. It is stated as `"1.2 V/us"`, and `parseQuantity` strips after the slash,
|
|
2782
|
+
// so it would yield 1.2 for the wrong reason -- right number, lost units -- and no consumer
|
|
2783
|
+
// exists for a slew rate yet. `typedFrequencyHz` does not inherit that trap: it compares the
|
|
2784
|
+
// whole string against a number and a frequency unit and refuses anything else.
|
|
2231
2785
|
if (kind === "opamp") {
|
|
2232
2786
|
// `typedNumber`, not `optionalQuantity`, and the difference is a 433x error.
|
|
2233
2787
|
// `optionalQuantity` reads `raw` first, so `{raw: "92 dB", value: 39810, unit: ""}` parsed as
|
|
@@ -2235,7 +2789,11 @@ function parametersFor(component, kind) {
|
|
|
2235
2789
|
// the conversion, so it has to win. Caught by measuring the stamped gains after the change
|
|
2236
2790
|
// rather than by reading the code.
|
|
2237
2791
|
const gain = typedNumber(component, "Aol", [""]);
|
|
2238
|
-
|
|
2792
|
+
const bandwidth = typedFrequencyHz(component, "GBP");
|
|
2793
|
+
return {
|
|
2794
|
+
...(gain !== null && gain > 0 ? { openLoopGain: gain } : {}),
|
|
2795
|
+
...(bandwidth === null ? {} : { gainBandwidthHz: bandwidth }),
|
|
2796
|
+
};
|
|
2239
2797
|
}
|
|
2240
2798
|
// A delay-memory shell's own delay time, which is a **packet** fact rather than a part fact.
|
|
2241
2799
|
//
|
|
@@ -2374,6 +2932,12 @@ function parametersFor(component, kind) {
|
|
|
2374
2932
|
optionalQuantity(component, "OhmicResistance") ??
|
|
2375
2933
|
optionalQuantity(component, "RS") ??
|
|
2376
2934
|
optionalQuantity(component, "Rs");
|
|
2935
|
+
// `EmissionCoefficient` -- the junction's ideality, the slope of its knee. One packet declares
|
|
2936
|
+
// it (`boss-sd-1`, 1.9 on three clipping diodes) and until this read it was discarded: the law
|
|
2937
|
+
// stamped 1.752 whatever the source said, and nothing said so, so a knee that a packet states
|
|
2938
|
+
// was a knee the engine never had. Typed and refused when unreadable, like `GBP`; absent stays
|
|
2939
|
+
// absent, so `deviceLaw` keeps ownership of what an undeclared coefficient means.
|
|
2940
|
+
const emission = typedEmissionCoefficient(component);
|
|
2377
2941
|
return {
|
|
2378
2942
|
...(breakdown !== null && breakdown > 0
|
|
2379
2943
|
? { breakdownVolts: breakdown }
|
|
@@ -2384,6 +2948,7 @@ function parametersFor(component, kind) {
|
|
|
2384
2948
|
...(seriesResistance !== null && seriesResistance > 0
|
|
2385
2949
|
? { seriesResistance }
|
|
2386
2950
|
: {}),
|
|
2951
|
+
...(emission === null ? {} : { emissionCoefficient: emission }),
|
|
2387
2952
|
};
|
|
2388
2953
|
}
|
|
2389
2954
|
// A bipolar transistor's device type and SPICE parameters, which this stage used to
|
|
@@ -3302,6 +3867,11 @@ function selectedOption(component, stated) {
|
|
|
3302
3867
|
* Deliberately absent: `W`, `W20`, `G`, `D`, `L`, `BH`, `Stepped`, `Trim`,
|
|
3303
3868
|
* `Boss-G-taper` and the other manufacturer-specific curves. We do not know their
|
|
3304
3869
|
* shapes, and guessing one would be inventing a device law from a letter.
|
|
3870
|
+
*
|
|
3871
|
+
* `s-curve` is the one S-shaped law present, and it is the *law's name*, not Alpha's letter: a
|
|
3872
|
+
* pot marked `W` or `W20` stays absent, because makers disagree on W and a letter selects a
|
|
3873
|
+
* curve only with its maker. A document that has established its pot's S-curve is Alpha W(4B)
|
|
3874
|
+
* says so by writing `s-curve`.
|
|
3305
3875
|
*/
|
|
3306
3876
|
const taperSpellings = new Map([
|
|
3307
3877
|
["linear", "linear"],
|
|
@@ -3319,6 +3889,9 @@ const taperSpellings = new Map([
|
|
|
3319
3889
|
["negativelog", "reverse-logarithmic"],
|
|
3320
3890
|
["c", "reverse-logarithmic"],
|
|
3321
3891
|
["reverselinear", "reverse-linear"],
|
|
3892
|
+
// Alpha W(4B). `foldTaper` strips `-`, `_` and whitespace, so this one key is `s-curve`,
|
|
3893
|
+
// `S-Curve`, `S Curve` and `scurve`; a hyphenated key would be unreachable.
|
|
3894
|
+
["scurve", "s-curve"],
|
|
3322
3895
|
]);
|
|
3323
3896
|
/**
|
|
3324
3897
|
* Values whose meaning is "the source does not state a taper".
|
|
@@ -3336,12 +3909,11 @@ const taperUnknownSpellings = new Set([
|
|
|
3336
3909
|
/**
|
|
3337
3910
|
* Tapers `@vessel-dsp/core`'s format supports and this runtime has no curve for.
|
|
3338
3911
|
*
|
|
3339
|
-
* `PotentiometerTaper` carries seven values; `TaperKind` -- the executable law -- carries
|
|
3340
|
-
* A document declaring one of these
|
|
3912
|
+
* `PotentiometerTaper` carries seven values; `TaperKind` -- the executable law -- carries five.
|
|
3913
|
+
* A document declaring one of these two has stated a real taper that the render cannot produce,
|
|
3341
3914
|
* which is a different failure from an unreadable marking and is worth saying differently.
|
|
3342
3915
|
*/
|
|
3343
3916
|
const taperWithoutLaw = new Set([
|
|
3344
|
-
"scurve",
|
|
3345
3917
|
"stepped",
|
|
3346
3918
|
"custom",
|
|
3347
3919
|
]);
|
|
@@ -3370,7 +3942,7 @@ function taperKind(value) {
|
|
|
3370
3942
|
* have read;
|
|
3371
3943
|
* - the value states anything else -> linear, and **name it**, whether it is a part marking whose
|
|
3372
3944
|
* curve is not derivable here (`W20`, `Boss-G-taper`) or a taper the format supports and this
|
|
3373
|
-
* runtime has no law for (`
|
|
3945
|
+
* runtime has no law for (`stepped`, `custom`).
|
|
3374
3946
|
*/
|
|
3375
3947
|
function declaredTaper(component) {
|
|
3376
3948
|
// `Sweep` is what a variable-resistor calls its taper. Both are closed vocabularies
|