From 6d5a910c503df242457b2e83f4918d422c0a68ab Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 22 Jul 2026 13:06:35 +0200 Subject: [PATCH 01/21] common: infer the speculative type from the draft repo sidecars (#25989) With -hfd pointing to a repo that ships mtp-/dflash-/eagle3- sidecars and no --spec-type given, the draft resolved to a full model while the sidecar was the intended draft. When the speculative types are still at their default, discover the sidecars of the draft repo, pick the first available following the existing mtp > dflash > eagle3 priority, and set the corresponding type, so this now works without any extra flag: llama-server -hf repo:Q3_K_M -hfd repo:Q8_0 An explicit --spec-type disables the inference, and a draft repo without sidecars keeps resolving to a full model as before. --- common/arg.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/common/arg.cpp b/common/arg.cpp index 48e274774..1f5b5a8ab 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -351,6 +351,10 @@ static std::string get_default_local_path(const std::string & url) { return fs_get_cache_file(string_split(f, '/').back()); } +static bool spec_types_is_default(const common_params & params) { + return params.speculative.types == std::vector{COMMON_SPECULATIVE_TYPE_NONE}; +} + common_models_handler common_models_handler_init(const common_params & params, llama_example curr_ex) { common_download_hf_plan plan; common_download_hf_plan plan_spec; @@ -391,7 +395,14 @@ common_models_handler common_models_handler_init(const common_params & params, l } if (!params.speculative.draft.mparams.hf_repo.empty()) { - plan_spec = common_download_get_hf_plan(params.speculative.draft.mparams, opts); + // without a requested type, discover every sidecar the draft repo ships to infer the type later + auto opts_spec = opts; + if (spec_types_is_default(params)) { + opts_spec.download_mtp = true; + opts_spec.download_dflash = true; + opts_spec.download_eagle3 = true; + } + plan_spec = common_download_get_hf_plan(params.speculative.draft.mparams, opts_spec); } if (!params.vocoder.model.hf_repo.empty()) { @@ -527,6 +538,20 @@ void common_models_handler_apply(common_models_handler & handler, common_params } }; + // infer the speculative type from the sidecar shipped by the draft repo when none is requested + if (spec_types_is_default(params)) { + if (!plan_spec.mtp.local_path.empty()) { + params.speculative.types = { COMMON_SPECULATIVE_TYPE_DRAFT_MTP }; + plan_spec.dflash = {}; + plan_spec.eagle3 = {}; + } else if (!plan_spec.dflash.local_path.empty()) { + params.speculative.types = { COMMON_SPECULATIVE_TYPE_DRAFT_DFLASH }; + plan_spec.eagle3 = {}; + } else if (!plan_spec.eagle3.local_path.empty()) { + params.speculative.types = { COMMON_SPECULATIVE_TYPE_DRAFT_EAGLE3 }; + } + } + // when a sidecar type is requested, the draft repo resolves to its sidecar instead of a full model const bool spec_sidecar_found = !plan_spec.mtp.local_path.empty() || !plan_spec.dflash.local_path.empty() || From e8e6c7af2456fd50bb62f7a2bbd642e6fb14ae77 Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Wed, 22 Jul 2026 14:32:54 +0200 Subject: [PATCH 02/21] minor: fix reasoning preserve var for DS4 [no ci] (#25999) --- common/jinja/caps.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 0d9336c0d..2b9e27ba1 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -23,7 +23,7 @@ void caps_apply_preserve_reasoning(jinja::context & ctx, bool enabled) { ctx.set_val("preserve_thinking", mk_val(enabled)); ctx.set_val("clear_thinking", mk_val(!enabled)); ctx.set_val("truncate_history_thinking", mk_val(!enabled)); - ctx.set_val("drop_reasoning", mk_val(!enabled)); + ctx.set_val("drop_thinking", mk_val(!enabled)); } static void caps_try_execute(jinja::program & prog, From 61328e6a9150b5d83f70416c93d2dfa607b442d0 Mon Sep 17 00:00:00 2001 From: rankaiyx Date: Wed, 22 Jul 2026 23:52:55 +0800 Subject: [PATCH 03/21] feat(ui): add symbolic math support to JS sandbox via nerdamer (#25948) * feat(ui): add symbolic math support to JS sandbox via nerdamer Preload nerdamer (with decimal.js) in the sandboxed worker, exposing the `nerdamer` global for symbolic computation: simplify, expand, factor, diff, integrate, solve, laplace, ilt, limit, partfrac, gcd/lcm, roots, coefficients, and more. Mirrors the math.js integration pattern from the feature/sandbox-symbolic-math branch, but uses nerdamer for a lighter, more focused symbolic math engine. * Update sandbox-harness.ts * docs(ui): update sandbox tool description with detailed nerdamer usage guide * Clarify nerdamer usage in sandbox tool description Updated the description of the sandbox tool to clarify usage of nerdamer. * ui: build nerdamer sandbox prelude from vendored source Replace the vendored all.min.js with the readable nerdamer-prime source and its two bundled deps (big-integer, decimal.js), licenses included. A vite plugin bundles and minifies them at build time with the upstream esbuild flags, exposed as virtual:nerdamer and imported lazily on first sandbox use. The vendors package.json pins commonjs so the project level type: module does not break esbuild format detection. The harness gains a CSP removing network egress from the worker, and browser tests cover the prelude, exact arithmetic, the fetch block and the timeout. Upstream snapshot: together-science/nerdamer-prime@1936145 * feat(ui): make symbolic math (nerdamer) a user-toggleable setting - Add SYMBOLIC_MATH_ENABLED setting key and registry entry (checkbox, default false) - Convert SANDBOX_TOOL_DEFINITION to buildSandboxToolDefinition(includeSymbolicMath) so the tool description includes/excludes nerdamer API docs dynamically - Cache sandbox harness per variant ('nerdamer' / 'plain') for instant toggle - Deprecate SANDBOX_TOOL_DEFINITION constant alias for backward compatibility - Update tools store to pass symbolic math config into tool definition * docs(ui): tell LLM to list nerdamer functions first, do not guess * test(ui): enable symbolic math in sandbox tests via settingsStore config * style(ui): fix formatting for tools.svelte.ts --------- Co-authored-by: Pascal --- tools/ui/.gitignore | 4 + tools/ui/.prettierignore | 3 + tools/ui/eslint.config.js | 3 +- tools/ui/scripts/vite-plugin-nerdamer.ts | 49 + tools/ui/src/lib/constants/sandbox.ts | 61 +- tools/ui/src/lib/constants/settings-keys.ts | 1 + .../ui/src/lib/constants/settings-registry.ts | 9 + tools/ui/src/lib/services/index.ts | 2 +- tools/ui/src/lib/services/sandbox-harness.ts | 18 +- tools/ui/src/lib/services/sandbox-worker.js | 4 +- tools/ui/src/lib/services/sandbox.service.ts | 35 +- tools/ui/src/lib/stores/tools.svelte.ts | 6 +- .../src/lib/vendors/big-integer/BigInteger.js | 1453 ++ tools/ui/src/lib/vendors/big-integer/LICENSE | 24 + .../ui/src/lib/vendors/decimal.js/LICENCE.md | 23 + .../ui/src/lib/vendors/decimal.js/decimal.js | 4951 +++++ .../src/lib/vendors/nerdamer-prime/Algebra.js | 6213 ++++++ .../lib/vendors/nerdamer-prime/Calculus.js | 3487 +++ .../src/lib/vendors/nerdamer-prime/Extra.js | 926 + .../ui/src/lib/vendors/nerdamer-prime/LICENSE | 21 + .../src/lib/vendors/nerdamer-prime/Solve.js | 2370 ++ .../ui/src/lib/vendors/nerdamer-prime/all.js | 16 + .../lib/vendors/nerdamer-prime/constants.js | 261 + .../vendors/nerdamer-prime/nerdamer.core.js | 17838 ++++++++++++++++ tools/ui/src/lib/vendors/package.json | 3 + tools/ui/src/virtual-nerdamer.d.ts | 4 + .../client/sandbox.service.svelte.test.ts | 59 + tools/ui/tsconfig.json | 2 +- tools/ui/vite.config.ts | 2 + 29 files changed, 37812 insertions(+), 36 deletions(-) create mode 100644 tools/ui/scripts/vite-plugin-nerdamer.ts create mode 100644 tools/ui/src/lib/vendors/big-integer/BigInteger.js create mode 100644 tools/ui/src/lib/vendors/big-integer/LICENSE create mode 100644 tools/ui/src/lib/vendors/decimal.js/LICENCE.md create mode 100644 tools/ui/src/lib/vendors/decimal.js/decimal.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/Algebra.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/Calculus.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/Extra.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/LICENSE create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/Solve.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/all.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/constants.js create mode 100644 tools/ui/src/lib/vendors/nerdamer-prime/nerdamer.core.js create mode 100644 tools/ui/src/lib/vendors/package.json create mode 100644 tools/ui/src/virtual-nerdamer.d.ts create mode 100644 tools/ui/tests/client/sandbox.service.svelte.test.ts diff --git a/tools/ui/.gitignore b/tools/ui/.gitignore index 0bb8c9b3c..7cd35376e 100644 --- a/tools/ui/.gitignore +++ b/tools/ui/.gitignore @@ -36,3 +36,7 @@ static/favicon* *storybook.log storybook-static *.code-workspace + +# Vitest browser mode failure artifacts +.vitest-attachments/ +tests/**/__screenshots__/ diff --git a/tools/ui/.prettierignore b/tools/ui/.prettierignore index 7bbdcf6a0..635cf99c7 100644 --- a/tools/ui/.prettierignore +++ b/tools/ui/.prettierignore @@ -16,3 +16,6 @@ build/ /build/ /.svelte-kit/ test-results + +# Vendored third party sources, kept byte identical to upstream +src/lib/vendors/ diff --git a/tools/ui/eslint.config.js b/tools/ui/eslint.config.js index 54679a05b..fcbf7ee95 100644 --- a/tools/ui/eslint.config.js +++ b/tools/ui/eslint.config.js @@ -59,7 +59,8 @@ export default ts.config( '.svelte-kit/**', 'test-results/**', '.storybook/**/*', - 'src/lib/services/sandbox-worker.js' + 'src/lib/services/sandbox-worker.js', + 'src/lib/vendors/**' ] }, storybook.configs['flat/recommended'] diff --git a/tools/ui/scripts/vite-plugin-nerdamer.ts b/tools/ui/scripts/vite-plugin-nerdamer.ts new file mode 100644 index 000000000..218c2fa23 --- /dev/null +++ b/tools/ui/scripts/vite-plugin-nerdamer.ts @@ -0,0 +1,49 @@ +import { build } from 'esbuild'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; +import type { Plugin } from 'vite'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const VENDORS_DIR = resolve(__dirname, '../src/lib/vendors'); +const VIRTUAL_ID = 'virtual:nerdamer'; +const RESOLVED_ID = '\0' + VIRTUAL_ID; + +/** + * Bundle the vendored nerdamer-prime source into a minified IIFE string, + * exposed as the `virtual:nerdamer` module. Flags mirror the upstream + * build (esbuild --bundle --minify --format=iife --global-name=nerdamer), + * so only human readable source lives in the repo and minification is a + * build artifact. Vendored under src/lib/vendors/, upstream snapshot: + * https://github.com/together-science/nerdamer-prime/commit/1936145f8af306ec0d883b9bfd7730aedd175c24 + */ +export function nerdamerPlugin(): Plugin { + let bundled: string | null = null; + + return { + name: 'llamacpp:nerdamer', + resolveId(id) { + return id === VIRTUAL_ID ? RESOLVED_ID : undefined; + }, + async load(id) { + if (id !== RESOLVED_ID) return undefined; + if (bundled === null) { + const result = await build({ + entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')], + bundle: true, + minify: true, + format: 'iife', + globalName: 'nerdamer', + alias: { + 'big-integer': resolve(VENDORS_DIR, 'big-integer/BigInteger.js'), + 'decimal.js': resolve(VENDORS_DIR, 'decimal.js/decimal.js') + }, + write: false, + logLevel: 'silent' + }); + bundled = result.outputFiles[0].text; + } + return `export default ${JSON.stringify(bundled)};`; + } + }; +} diff --git a/tools/ui/src/lib/constants/sandbox.ts b/tools/ui/src/lib/constants/sandbox.ts index de49f0584..58242678d 100644 --- a/tools/ui/src/lib/constants/sandbox.ts +++ b/tools/ui/src/lib/constants/sandbox.ts @@ -13,27 +13,44 @@ export const SANDBOX_EMPTY_OUTPUT = '(no output)'; export const SANDBOX_TRUNCATION_NOTICE = '[output truncated]'; -export const SANDBOX_TOOL_DEFINITION: OpenAIToolDefinition = { - type: ToolCallType.FUNCTION, - function: { - name: SANDBOX_TOOL_NAME, - description: - 'Execute JavaScript in a sandboxed browser worker (no DOM, no page access). ' + - 'Top level await is supported. Use console.log to print intermediate values; ' + - 'a top level return statement is captured as the result.', - parameters: { - type: JsonSchemaType.OBJECT, - properties: { - code: { - type: JsonSchemaType.STRING, - description: 'JavaScript source to execute' +const NERDAMER_DESCRIPTION = ` +Symbolic/numeric math via \`nerdamer\` (pre-loaded, do not require, use it directly). +nerdamer('diff(sin(x)/x,x)') or nerdamer.diff('sin(x)/x','x') → Expression; convert with .toString()/.text()/.toTeX(), or .evaluate() (→ still Expression, then .toString()). +nerdamer(expr,{x:2}) substitutes only; chain .evaluate() or pass 'numer' for numeric result. +solve(expr,var)→Symbol[]; solveEquations([eq1,..])→[[var,val],..] pairs. +Functions: simplify/expand/factor(expr), diff(expr,var[,n]), integrate(expr,var), defint(expr,from,to,var), limit(expr,var,to), laplace(expr,t,s), ilt(expr,s,t), gcd/lcm(a,b), roots/coeffs/partfrac(expr,var), pfactor(n), numer/decimals/erf(expr), product/sum(expr,var,from,to), mean/median/stdev/variance(...vals). +Object.keys(nerdamer).filter(k=>typeof nerdamer[k]==='function') lists all available functions. If you need a function not documented above, list them first — do not guess function names.`; + +/** + * Build the sandbox tool definition. When `includeSymbolicMath` is true, + * the description includes nerdamer API documentation; otherwise it + * describes a plain JavaScript sandbox. + */ +export function buildSandboxToolDefinition(includeSymbolicMath: boolean): OpenAIToolDefinition { + return { + type: ToolCallType.FUNCTION, + function: { + name: SANDBOX_TOOL_NAME, + description: includeSymbolicMath + ? `Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.${NERDAMER_DESCRIPTION}` + : 'Execute JS in a sandboxed browser worker (no DOM/page access). Top-level await ok; console.log for intermediates; top-level return is captured as result.', + parameters: { + type: JsonSchemaType.OBJECT, + properties: { + code: { + type: JsonSchemaType.STRING, + description: 'JavaScript source to execute' + }, + timeout_ms: { + type: JsonSchemaType.NUMBER, + description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}` + } }, - timeout_ms: { - type: JsonSchemaType.NUMBER, - description: `Execution timeout in milliseconds, default ${SANDBOX_TIMEOUT_MS_DEFAULT}, max ${SANDBOX_TIMEOUT_MS_MAX}` - } - }, - required: ['code'] + required: ['code'] + } } - } -}; + }; +} + +/** @deprecated Use {@link buildSandboxToolDefinition} instead. Kept for backward compatibility. */ +export const SANDBOX_TOOL_DEFINITION = buildSandboxToolDefinition(true); diff --git a/tools/ui/src/lib/constants/settings-keys.ts b/tools/ui/src/lib/constants/settings-keys.ts index fab343659..265507a5b 100644 --- a/tools/ui/src/lib/constants/settings-keys.ts +++ b/tools/ui/src/lib/constants/settings-keys.ts @@ -67,6 +67,7 @@ export const SETTINGS_KEYS = { EXCLUDE_REASONING_FROM_CONTEXT: 'excludeReasoningFromContext', SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch', JS_SANDBOX_ENABLED: 'jsSandboxEnabled', + SYMBOLIC_MATH_ENABLED: 'symbolicMathEnabled', // PY_INTERPRETER_ENABLED: 'pyInterpreterEnabled', CUSTOM_JSON: 'customJson', CUSTOM_CSS: 'customCss' diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index f511b751d..742b5f5c6 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -724,6 +724,15 @@ const SETTINGS_REGISTRY: Record = { paramType: SyncableParameterType.BOOLEAN } }, + { + key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED, + label: 'Symbolic math (nerdamer)', + help: 'Pre-load nerdamer in the sandbox for symbolic computation: simplify, diff, integrate, solve, and more. Requires "JavaScript sandbox tool" to be enabled.', + defaultValue: false, + type: SettingsFieldType.CHECKBOX, + section: SETTINGS_SECTION_SLUGS.DEVELOPER, + dependsOn: SETTINGS_KEYS.JS_SANDBOX_ENABLED + }, { key: SETTINGS_KEYS.CUSTOM_JSON, label: 'Custom JSON', diff --git a/tools/ui/src/lib/services/index.ts b/tools/ui/src/lib/services/index.ts index 386f740b8..8704b1bd6 100644 --- a/tools/ui/src/lib/services/index.ts +++ b/tools/ui/src/lib/services/index.ts @@ -276,7 +276,7 @@ export { MCPService } from './mcp.service'; * - **toolsStore**: Exposes the tool definition when the sandbox is enabled * - **agenticStore**: Dispatches ToolSource.FRONTEND calls here * - * @see SANDBOX_TOOL_DEFINITION in constants/sandbox.ts - tool schema sent to the LLM + * @see buildSandboxToolDefinition in constants/sandbox.ts - tool schema sent to the LLM * @see agenticStore in stores/agentic.svelte.ts - tool dispatch */ export { SandboxService } from './sandbox.service'; diff --git a/tools/ui/src/lib/services/sandbox-harness.ts b/tools/ui/src/lib/services/sandbox-harness.ts index 27b05e24b..40502121f 100644 --- a/tools/ui/src/lib/services/sandbox-harness.ts +++ b/tools/ui/src/lib/services/sandbox-harness.ts @@ -1,14 +1,25 @@ +import { NEWLINE } from '$lib/constants'; import WORKER_SHIM from './sandbox-worker.js?raw'; +/** + * CSP for the harness document, inherited by the blob worker. connect-src + * falls back to default-src, removing network egress for model and vendored + * code. 'unsafe-eval' is required by the worker's AsyncFunction constructor, + * 'unsafe-inline' by the inline script below, worker-src by the blob worker. + */ +const HARNESS_CSP = `default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; worker-src blob:`; + /** * Harness loaded as srcdoc into a sandboxed iframe (allow-scripts only). * The opaque origin is the security boundary: no access to the app origin, * its storage or its API. The harness spawns a worker so model code never * runs on a main thread, which makes the parent timeout enforceable by - * removing the iframe. + * removing the iframe. The prelude runs in the worker before the shim, + * exposing globals such as `nerdamer` to model code. */ -export const SANDBOX_HARNESS_HTML = ``; +} diff --git a/tools/ui/src/lib/services/sandbox-worker.js b/tools/ui/src/lib/services/sandbox-worker.js index 689b9211e..838a97db1 100644 --- a/tools/ui/src/lib/services/sandbox-worker.js +++ b/tools/ui/src/lib/services/sandbox-worker.js @@ -21,7 +21,9 @@ self.onmessage = async (event) => { const reply = { logs, result: null, error: null }; try { const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; - const value = await new AsyncFunction(event.data.code)(); + // The prelude bundled ahead of this shim defines self.nerdamer, + // passed into the execution scope as the `nerdamer` parameter. + const value = await new AsyncFunction('nerdamer', event.data.code)(self.nerdamer); if (value !== undefined) reply.result = fmt(value); } catch (err) { reply.error = err instanceof Error ? err.stack || err.message : String(err); diff --git a/tools/ui/src/lib/services/sandbox.service.ts b/tools/ui/src/lib/services/sandbox.service.ts index 36bf2d020..f49a774a0 100644 --- a/tools/ui/src/lib/services/sandbox.service.ts +++ b/tools/ui/src/lib/services/sandbox.service.ts @@ -7,9 +7,32 @@ import { SANDBOX_TOOL_NAME, SANDBOX_TRUNCATION_NOTICE } from '$lib/constants'; -import { SANDBOX_HARNESS_HTML } from './sandbox-harness'; +import { buildSandboxHarness } from './sandbox-harness'; +import { config } from '$lib/stores/settings.svelte'; import type { ToolExecutionResult } from '$lib/types'; +/** Cached harnesses keyed by whether nerdamer is included. */ +const harnessCache: Record = {}; + +/** + * Build the sandbox harness. When symbolic math is enabled, loads the + * nerdamer prelude lazily; otherwise builds a plain harness with an empty + * prelude. Cached per variant so toggling the setting is instant. + */ +async function getHarness(): Promise { + const enabled = !!config().symbolicMathEnabled; + const key = enabled ? 'nerdamer' : 'plain'; + if (!harnessCache[key]) { + if (enabled) { + const { default: nerdamerJs } = await import('virtual:nerdamer'); + harnessCache[key] = buildSandboxHarness(nerdamerJs); + } else { + harnessCache[key] = buildSandboxHarness(''); + } + } + return harnessCache[key]; +} + interface SandboxReply { logs?: unknown; result?: unknown; @@ -45,20 +68,22 @@ export class SandboxService { * timeout or abort. Removing the iframe terminates the worker * at the browser level, so runaway code cannot outlive it. */ - static executeTool( + static async executeTool( toolName: string, params: Record, signal?: AbortSignal ): Promise { if (toolName !== SANDBOX_TOOL_NAME) { - return Promise.resolve({ content: `Unknown frontend tool: ${toolName}`, isError: true }); + return { content: `Unknown frontend tool: ${toolName}`, isError: true }; } const code = typeof params.code === 'string' ? params.code : ''; if (!code) { - return Promise.resolve({ content: 'Missing required parameter: code', isError: true }); + return { content: 'Missing required parameter: code', isError: true }; } + const harness = await getHarness(); + const requested = Number(params.timeout_ms); const timeoutMs = Number.isFinite(requested) && requested > 0 @@ -69,7 +94,7 @@ export class SandboxService { const iframe = document.createElement('iframe'); iframe.setAttribute('sandbox', 'allow-scripts'); iframe.style.display = 'none'; - iframe.srcdoc = SANDBOX_HARNESS_HTML; + iframe.srcdoc = harness; let settled = false; diff --git a/tools/ui/src/lib/stores/tools.svelte.ts b/tools/ui/src/lib/stores/tools.svelte.ts index 53b6ad9ee..bd69c4d30 100644 --- a/tools/ui/src/lib/stores/tools.svelte.ts +++ b/tools/ui/src/lib/stores/tools.svelte.ts @@ -5,7 +5,7 @@ import { HealthCheckStatus, JsonSchemaType, ToolCallType, ToolSource } from '$li import { config } from '$lib/stores/settings.svelte'; import { DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY, - SANDBOX_TOOL_DEFINITION, + buildSandboxToolDefinition, TOOL_GROUP_LABELS, TOOL_SERVER_LABELS } from '$lib/constants'; @@ -143,7 +143,9 @@ class ToolsStore { } get frontendTools(): OpenAIToolDefinition[] { - return config().jsSandboxEnabled ? [SANDBOX_TOOL_DEFINITION] : []; + return config().jsSandboxEnabled + ? [buildSandboxToolDefinition(!!config().symbolicMathEnabled)] + : []; } get customTools(): OpenAIToolDefinition[] { diff --git a/tools/ui/src/lib/vendors/big-integer/BigInteger.js b/tools/ui/src/lib/vendors/big-integer/BigInteger.js new file mode 100644 index 000000000..87e43dfff --- /dev/null +++ b/tools/ui/src/lib/vendors/big-integer/BigInteger.js @@ -0,0 +1,1453 @@ +var bigInt = (function (undefined) { + "use strict"; + + var BASE = 1e7, + LOG_BASE = 7, + MAX_INT = 9007199254740992, + MAX_INT_ARR = smallToArray(MAX_INT), + DEFAULT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"; + + var supportsNativeBigInt = typeof BigInt === "function"; + + function Integer(v, radix, alphabet, caseSensitive) { + if (typeof v === "undefined") return Integer[0]; + if (typeof radix !== "undefined") return +radix === 10 && !alphabet ? parseValue(v) : parseBase(v, radix, alphabet, caseSensitive); + return parseValue(v); + } + + function BigInteger(value, sign) { + this.value = value; + this.sign = sign; + this.isSmall = false; + } + BigInteger.prototype = Object.create(Integer.prototype); + + function SmallInteger(value) { + this.value = value; + this.sign = value < 0; + this.isSmall = true; + } + SmallInteger.prototype = Object.create(Integer.prototype); + + function NativeBigInt(value) { + this.value = value; + } + NativeBigInt.prototype = Object.create(Integer.prototype); + + function isPrecise(n) { + return -MAX_INT < n && n < MAX_INT; + } + + function smallToArray(n) { // For performance reasons doesn't reference BASE, need to change this function if BASE changes + if (n < 1e7) + return [n]; + if (n < 1e14) + return [n % 1e7, Math.floor(n / 1e7)]; + return [n % 1e7, Math.floor(n / 1e7) % 1e7, Math.floor(n / 1e14)]; + } + + function arrayToSmall(arr) { // If BASE changes this function may need to change + trim(arr); + var length = arr.length; + if (length < 4 && compareAbs(arr, MAX_INT_ARR) < 0) { + switch (length) { + case 0: return 0; + case 1: return arr[0]; + case 2: return arr[0] + arr[1] * BASE; + default: return arr[0] + (arr[1] + arr[2] * BASE) * BASE; + } + } + return arr; + } + + function trim(v) { + var i = v.length; + while (v[--i] === 0); + v.length = i + 1; + } + + function createArray(length) { // function shamelessly stolen from Yaffle's library https://github.com/Yaffle/BigInteger + var x = new Array(length); + var i = -1; + while (++i < length) { + x[i] = 0; + } + return x; + } + + function truncate(n) { + if (n > 0) return Math.floor(n); + return Math.ceil(n); + } + + function add(a, b) { // assumes a and b are arrays with a.length >= b.length + var l_a = a.length, + l_b = b.length, + r = new Array(l_a), + carry = 0, + base = BASE, + sum, i; + for (i = 0; i < l_b; i++) { + sum = a[i] + b[i] + carry; + carry = sum >= base ? 1 : 0; + r[i] = sum - carry * base; + } + while (i < l_a) { + sum = a[i] + carry; + carry = sum === base ? 1 : 0; + r[i++] = sum - carry * base; + } + if (carry > 0) r.push(carry); + return r; + } + + function addAny(a, b) { + if (a.length >= b.length) return add(a, b); + return add(b, a); + } + + function addSmall(a, carry) { // assumes a is array, carry is number with 0 <= carry < MAX_INT + var l = a.length, + r = new Array(l), + base = BASE, + sum, i; + for (i = 0; i < l; i++) { + sum = a[i] - base + carry; + carry = Math.floor(sum / base); + r[i] = sum - carry * base; + carry += 1; + } + while (carry > 0) { + r[i++] = carry % base; + carry = Math.floor(carry / base); + } + return r; + } + + BigInteger.prototype.add = function (v) { + var n = parseValue(v); + if (this.sign !== n.sign) { + return this.subtract(n.negate()); + } + var a = this.value, b = n.value; + if (n.isSmall) { + return new BigInteger(addSmall(a, Math.abs(b)), this.sign); + } + return new BigInteger(addAny(a, b), this.sign); + }; + BigInteger.prototype.plus = BigInteger.prototype.add; + + SmallInteger.prototype.add = function (v) { + var n = parseValue(v); + var a = this.value; + if (a < 0 !== n.sign) { + return this.subtract(n.negate()); + } + var b = n.value; + if (n.isSmall) { + if (isPrecise(a + b)) return new SmallInteger(a + b); + b = smallToArray(Math.abs(b)); + } + return new BigInteger(addSmall(b, Math.abs(a)), a < 0); + }; + SmallInteger.prototype.plus = SmallInteger.prototype.add; + + NativeBigInt.prototype.add = function (v) { + return new NativeBigInt(this.value + parseValue(v).value); + } + NativeBigInt.prototype.plus = NativeBigInt.prototype.add; + + function subtract(a, b) { // assumes a and b are arrays with a >= b + var a_l = a.length, + b_l = b.length, + r = new Array(a_l), + borrow = 0, + base = BASE, + i, difference; + for (i = 0; i < b_l; i++) { + difference = a[i] - borrow - b[i]; + if (difference < 0) { + difference += base; + borrow = 1; + } else borrow = 0; + r[i] = difference; + } + for (i = b_l; i < a_l; i++) { + difference = a[i] - borrow; + if (difference < 0) difference += base; + else { + r[i++] = difference; + break; + } + r[i] = difference; + } + for (; i < a_l; i++) { + r[i] = a[i]; + } + trim(r); + return r; + } + + function subtractAny(a, b, sign) { + var value; + if (compareAbs(a, b) >= 0) { + value = subtract(a, b); + } else { + value = subtract(b, a); + sign = !sign; + } + value = arrayToSmall(value); + if (typeof value === "number") { + if (sign) value = -value; + return new SmallInteger(value); + } + return new BigInteger(value, sign); + } + + function subtractSmall(a, b, sign) { // assumes a is array, b is number with 0 <= b < MAX_INT + var l = a.length, + r = new Array(l), + carry = -b, + base = BASE, + i, difference; + for (i = 0; i < l; i++) { + difference = a[i] + carry; + carry = Math.floor(difference / base); + difference %= base; + r[i] = difference < 0 ? difference + base : difference; + } + r = arrayToSmall(r); + if (typeof r === "number") { + if (sign) r = -r; + return new SmallInteger(r); + } return new BigInteger(r, sign); + } + + BigInteger.prototype.subtract = function (v) { + var n = parseValue(v); + if (this.sign !== n.sign) { + return this.add(n.negate()); + } + var a = this.value, b = n.value; + if (n.isSmall) + return subtractSmall(a, Math.abs(b), this.sign); + return subtractAny(a, b, this.sign); + }; + BigInteger.prototype.minus = BigInteger.prototype.subtract; + + SmallInteger.prototype.subtract = function (v) { + var n = parseValue(v); + var a = this.value; + if (a < 0 !== n.sign) { + return this.add(n.negate()); + } + var b = n.value; + if (n.isSmall) { + return new SmallInteger(a - b); + } + return subtractSmall(b, Math.abs(a), a >= 0); + }; + SmallInteger.prototype.minus = SmallInteger.prototype.subtract; + + NativeBigInt.prototype.subtract = function (v) { + return new NativeBigInt(this.value - parseValue(v).value); + } + NativeBigInt.prototype.minus = NativeBigInt.prototype.subtract; + + BigInteger.prototype.negate = function () { + return new BigInteger(this.value, !this.sign); + }; + SmallInteger.prototype.negate = function () { + var sign = this.sign; + var small = new SmallInteger(-this.value); + small.sign = !sign; + return small; + }; + NativeBigInt.prototype.negate = function () { + return new NativeBigInt(-this.value); + } + + BigInteger.prototype.abs = function () { + return new BigInteger(this.value, false); + }; + SmallInteger.prototype.abs = function () { + return new SmallInteger(Math.abs(this.value)); + }; + NativeBigInt.prototype.abs = function () { + return new NativeBigInt(this.value >= 0 ? this.value : -this.value); + } + + + function multiplyLong(a, b) { + var a_l = a.length, + b_l = b.length, + l = a_l + b_l, + r = createArray(l), + base = BASE, + product, carry, i, a_i, b_j; + for (i = 0; i < a_l; ++i) { + a_i = a[i]; + for (var j = 0; j < b_l; ++j) { + b_j = b[j]; + product = a_i * b_j + r[i + j]; + carry = Math.floor(product / base); + r[i + j] = product - carry * base; + r[i + j + 1] += carry; + } + } + trim(r); + return r; + } + + function multiplySmall(a, b) { // assumes a is array, b is number with |b| < BASE + var l = a.length, + r = new Array(l), + base = BASE, + carry = 0, + product, i; + for (i = 0; i < l; i++) { + product = a[i] * b + carry; + carry = Math.floor(product / base); + r[i] = product - carry * base; + } + while (carry > 0) { + r[i++] = carry % base; + carry = Math.floor(carry / base); + } + return r; + } + + function shiftLeft(x, n) { + var r = []; + while (n-- > 0) r.push(0); + return r.concat(x); + } + + function multiplyKaratsuba(x, y) { + var n = Math.max(x.length, y.length); + + if (n <= 30) return multiplyLong(x, y); + n = Math.ceil(n / 2); + + var b = x.slice(n), + a = x.slice(0, n), + d = y.slice(n), + c = y.slice(0, n); + + var ac = multiplyKaratsuba(a, c), + bd = multiplyKaratsuba(b, d), + abcd = multiplyKaratsuba(addAny(a, b), addAny(c, d)); + + var product = addAny(addAny(ac, shiftLeft(subtract(subtract(abcd, ac), bd), n)), shiftLeft(bd, 2 * n)); + trim(product); + return product; + } + + // The following function is derived from a surface fit of a graph plotting the performance difference + // between long multiplication and karatsuba multiplication versus the lengths of the two arrays. + function useKaratsuba(l1, l2) { + return -0.012 * l1 - 0.012 * l2 + 0.000015 * l1 * l2 > 0; + } + + BigInteger.prototype.multiply = function (v) { + var n = parseValue(v), + a = this.value, b = n.value, + sign = this.sign !== n.sign, + abs; + if (n.isSmall) { + if (b === 0) return Integer[0]; + if (b === 1) return this; + if (b === -1) return this.negate(); + abs = Math.abs(b); + if (abs < BASE) { + return new BigInteger(multiplySmall(a, abs), sign); + } + b = smallToArray(abs); + } + if (useKaratsuba(a.length, b.length)) // Karatsuba is only faster for certain array sizes + return new BigInteger(multiplyKaratsuba(a, b), sign); + return new BigInteger(multiplyLong(a, b), sign); + }; + + BigInteger.prototype.times = BigInteger.prototype.multiply; + + function multiplySmallAndArray(a, b, sign) { // a >= 0 + if (a < BASE) { + return new BigInteger(multiplySmall(b, a), sign); + } + return new BigInteger(multiplyLong(b, smallToArray(a)), sign); + } + SmallInteger.prototype._multiplyBySmall = function (a) { + if (isPrecise(a.value * this.value)) { + return new SmallInteger(a.value * this.value); + } + return multiplySmallAndArray(Math.abs(a.value), smallToArray(Math.abs(this.value)), this.sign !== a.sign); + }; + BigInteger.prototype._multiplyBySmall = function (a) { + if (a.value === 0) return Integer[0]; + if (a.value === 1) return this; + if (a.value === -1) return this.negate(); + return multiplySmallAndArray(Math.abs(a.value), this.value, this.sign !== a.sign); + }; + SmallInteger.prototype.multiply = function (v) { + return parseValue(v)._multiplyBySmall(this); + }; + SmallInteger.prototype.times = SmallInteger.prototype.multiply; + + NativeBigInt.prototype.multiply = function (v) { + return new NativeBigInt(this.value * parseValue(v).value); + } + NativeBigInt.prototype.times = NativeBigInt.prototype.multiply; + + function square(a) { + //console.assert(2 * BASE * BASE < MAX_INT); + var l = a.length, + r = createArray(l + l), + base = BASE, + product, carry, i, a_i, a_j; + for (i = 0; i < l; i++) { + a_i = a[i]; + carry = 0 - a_i * a_i; + for (var j = i; j < l; j++) { + a_j = a[j]; + product = 2 * (a_i * a_j) + r[i + j] + carry; + carry = Math.floor(product / base); + r[i + j] = product - carry * base; + } + r[i + l] = carry; + } + trim(r); + return r; + } + + BigInteger.prototype.square = function () { + return new BigInteger(square(this.value), false); + }; + + SmallInteger.prototype.square = function () { + var value = this.value * this.value; + if (isPrecise(value)) return new SmallInteger(value); + return new BigInteger(square(smallToArray(Math.abs(this.value))), false); + }; + + NativeBigInt.prototype.square = function (v) { + return new NativeBigInt(this.value * this.value); + } + + function divMod1(a, b) { // Left over from previous version. Performs faster than divMod2 on smaller input sizes. + var a_l = a.length, + b_l = b.length, + base = BASE, + result = createArray(b.length), + divisorMostSignificantDigit = b[b_l - 1], + // normalization + lambda = Math.ceil(base / (2 * divisorMostSignificantDigit)), + remainder = multiplySmall(a, lambda), + divisor = multiplySmall(b, lambda), + quotientDigit, shift, carry, borrow, i, l, q; + if (remainder.length <= a_l) remainder.push(0); + divisor.push(0); + divisorMostSignificantDigit = divisor[b_l - 1]; + for (shift = a_l - b_l; shift >= 0; shift--) { + quotientDigit = base - 1; + if (remainder[shift + b_l] !== divisorMostSignificantDigit) { + quotientDigit = Math.floor((remainder[shift + b_l] * base + remainder[shift + b_l - 1]) / divisorMostSignificantDigit); + } + // quotientDigit <= base - 1 + carry = 0; + borrow = 0; + l = divisor.length; + for (i = 0; i < l; i++) { + carry += quotientDigit * divisor[i]; + q = Math.floor(carry / base); + borrow += remainder[shift + i] - (carry - q * base); + carry = q; + if (borrow < 0) { + remainder[shift + i] = borrow + base; + borrow = -1; + } else { + remainder[shift + i] = borrow; + borrow = 0; + } + } + while (borrow !== 0) { + quotientDigit -= 1; + carry = 0; + for (i = 0; i < l; i++) { + carry += remainder[shift + i] - base + divisor[i]; + if (carry < 0) { + remainder[shift + i] = carry + base; + carry = 0; + } else { + remainder[shift + i] = carry; + carry = 1; + } + } + borrow += carry; + } + result[shift] = quotientDigit; + } + // denormalization + remainder = divModSmall(remainder, lambda)[0]; + return [arrayToSmall(result), arrayToSmall(remainder)]; + } + + function divMod2(a, b) { // Implementation idea shamelessly stolen from Silent Matt's library http://silentmatt.com/biginteger/ + // Performs faster than divMod1 on larger input sizes. + var a_l = a.length, + b_l = b.length, + result = [], + part = [], + base = BASE, + guess, xlen, highx, highy, check; + while (a_l) { + part.unshift(a[--a_l]); + trim(part); + if (compareAbs(part, b) < 0) { + result.push(0); + continue; + } + xlen = part.length; + highx = part[xlen - 1] * base + part[xlen - 2]; + highy = b[b_l - 1] * base + b[b_l - 2]; + if (xlen > b_l) { + highx = (highx + 1) * base; + } + guess = Math.ceil(highx / highy); + do { + check = multiplySmall(b, guess); + if (compareAbs(check, part) <= 0) break; + guess--; + } while (guess); + result.push(guess); + part = subtract(part, check); + } + result.reverse(); + return [arrayToSmall(result), arrayToSmall(part)]; + } + + function divModSmall(value, lambda) { + var length = value.length, + quotient = createArray(length), + base = BASE, + i, q, remainder, divisor; + remainder = 0; + for (i = length - 1; i >= 0; --i) { + divisor = remainder * base + value[i]; + q = truncate(divisor / lambda); + remainder = divisor - q * lambda; + quotient[i] = q | 0; + } + return [quotient, remainder | 0]; + } + + function divModAny(self, v) { + var value, n = parseValue(v); + if (supportsNativeBigInt) { + return [new NativeBigInt(self.value / n.value), new NativeBigInt(self.value % n.value)]; + } + var a = self.value, b = n.value; + var quotient; + if (b === 0) throw new Error("Cannot divide by zero"); + if (self.isSmall) { + if (n.isSmall) { + return [new SmallInteger(truncate(a / b)), new SmallInteger(a % b)]; + } + return [Integer[0], self]; + } + if (n.isSmall) { + if (b === 1) return [self, Integer[0]]; + if (b == -1) return [self.negate(), Integer[0]]; + var abs = Math.abs(b); + if (abs < BASE) { + value = divModSmall(a, abs); + quotient = arrayToSmall(value[0]); + var remainder = value[1]; + if (self.sign) remainder = -remainder; + if (typeof quotient === "number") { + if (self.sign !== n.sign) quotient = -quotient; + return [new SmallInteger(quotient), new SmallInteger(remainder)]; + } + return [new BigInteger(quotient, self.sign !== n.sign), new SmallInteger(remainder)]; + } + b = smallToArray(abs); + } + var comparison = compareAbs(a, b); + if (comparison === -1) return [Integer[0], self]; + if (comparison === 0) return [Integer[self.sign === n.sign ? 1 : -1], Integer[0]]; + + // divMod1 is faster on smaller input sizes + if (a.length + b.length <= 200) + value = divMod1(a, b); + else value = divMod2(a, b); + + quotient = value[0]; + var qSign = self.sign !== n.sign, + mod = value[1], + mSign = self.sign; + if (typeof quotient === "number") { + if (qSign) quotient = -quotient; + quotient = new SmallInteger(quotient); + } else quotient = new BigInteger(quotient, qSign); + if (typeof mod === "number") { + if (mSign) mod = -mod; + mod = new SmallInteger(mod); + } else mod = new BigInteger(mod, mSign); + return [quotient, mod]; + } + + BigInteger.prototype.divmod = function (v) { + var result = divModAny(this, v); + return { + quotient: result[0], + remainder: result[1] + }; + }; + NativeBigInt.prototype.divmod = SmallInteger.prototype.divmod = BigInteger.prototype.divmod; + + + BigInteger.prototype.divide = function (v) { + return divModAny(this, v)[0]; + }; + NativeBigInt.prototype.over = NativeBigInt.prototype.divide = function (v) { + return new NativeBigInt(this.value / parseValue(v).value); + }; + SmallInteger.prototype.over = SmallInteger.prototype.divide = BigInteger.prototype.over = BigInteger.prototype.divide; + + BigInteger.prototype.mod = function (v) { + return divModAny(this, v)[1]; + }; + NativeBigInt.prototype.mod = NativeBigInt.prototype.remainder = function (v) { + return new NativeBigInt(this.value % parseValue(v).value); + }; + SmallInteger.prototype.remainder = SmallInteger.prototype.mod = BigInteger.prototype.remainder = BigInteger.prototype.mod; + + BigInteger.prototype.pow = function (v) { + var n = parseValue(v), + a = this.value, + b = n.value, + value, x, y; + if (b === 0) return Integer[1]; + if (a === 0) return Integer[0]; + if (a === 1) return Integer[1]; + if (a === -1) return n.isEven() ? Integer[1] : Integer[-1]; + if (n.sign) { + return Integer[0]; + } + if (!n.isSmall) throw new Error("The exponent " + n.toString() + " is too large."); + if (this.isSmall) { + if (isPrecise(value = Math.pow(a, b))) + return new SmallInteger(truncate(value)); + } + x = this; + y = Integer[1]; + while (true) { + if (b & 1 === 1) { + y = y.times(x); + --b; + } + if (b === 0) break; + b /= 2; + x = x.square(); + } + return y; + }; + SmallInteger.prototype.pow = BigInteger.prototype.pow; + + NativeBigInt.prototype.pow = function (v) { + var n = parseValue(v); + var a = this.value, b = n.value; + var _0 = BigInt(0), _1 = BigInt(1), _2 = BigInt(2); + if (b === _0) return Integer[1]; + if (a === _0) return Integer[0]; + if (a === _1) return Integer[1]; + if (a === BigInt(-1)) return n.isEven() ? Integer[1] : Integer[-1]; + if (n.isNegative()) return new NativeBigInt(_0); + var x = this; + var y = Integer[1]; + while (true) { + if ((b & _1) === _1) { + y = y.times(x); + --b; + } + if (b === _0) break; + b /= _2; + x = x.square(); + } + return y; + } + + BigInteger.prototype.modPow = function (exp, mod) { + exp = parseValue(exp); + mod = parseValue(mod); + if (mod.isZero()) throw new Error("Cannot take modPow with modulus 0"); + var r = Integer[1], + base = this.mod(mod); + if (exp.isNegative()) { + exp = exp.multiply(Integer[-1]); + base = base.modInv(mod); + } + while (exp.isPositive()) { + if (base.isZero()) return Integer[0]; + if (exp.isOdd()) r = r.multiply(base).mod(mod); + exp = exp.divide(2); + base = base.square().mod(mod); + } + return r; + }; + NativeBigInt.prototype.modPow = SmallInteger.prototype.modPow = BigInteger.prototype.modPow; + + function compareAbs(a, b) { + if (a.length !== b.length) { + return a.length > b.length ? 1 : -1; + } + for (var i = a.length - 1; i >= 0; i--) { + if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1; + } + return 0; + } + + BigInteger.prototype.compareAbs = function (v) { + var n = parseValue(v), + a = this.value, + b = n.value; + if (n.isSmall) return 1; + return compareAbs(a, b); + }; + SmallInteger.prototype.compareAbs = function (v) { + var n = parseValue(v), + a = Math.abs(this.value), + b = n.value; + if (n.isSmall) { + b = Math.abs(b); + return a === b ? 0 : a > b ? 1 : -1; + } + return -1; + }; + NativeBigInt.prototype.compareAbs = function (v) { + var a = this.value; + var b = parseValue(v).value; + a = a >= 0 ? a : -a; + b = b >= 0 ? b : -b; + return a === b ? 0 : a > b ? 1 : -1; + } + + BigInteger.prototype.compare = function (v) { + // See discussion about comparison with Infinity: + // https://github.com/peterolson/BigInteger.js/issues/61 + if (v === Infinity) { + return -1; + } + if (v === -Infinity) { + return 1; + } + + var n = parseValue(v), + a = this.value, + b = n.value; + if (this.sign !== n.sign) { + return n.sign ? 1 : -1; + } + if (n.isSmall) { + return this.sign ? -1 : 1; + } + return compareAbs(a, b) * (this.sign ? -1 : 1); + }; + BigInteger.prototype.compareTo = BigInteger.prototype.compare; + + SmallInteger.prototype.compare = function (v) { + if (v === Infinity) { + return -1; + } + if (v === -Infinity) { + return 1; + } + + var n = parseValue(v), + a = this.value, + b = n.value; + if (n.isSmall) { + return a == b ? 0 : a > b ? 1 : -1; + } + if (a < 0 !== n.sign) { + return a < 0 ? -1 : 1; + } + return a < 0 ? 1 : -1; + }; + SmallInteger.prototype.compareTo = SmallInteger.prototype.compare; + + NativeBigInt.prototype.compare = function (v) { + if (v === Infinity) { + return -1; + } + if (v === -Infinity) { + return 1; + } + var a = this.value; + var b = parseValue(v).value; + return a === b ? 0 : a > b ? 1 : -1; + } + NativeBigInt.prototype.compareTo = NativeBigInt.prototype.compare; + + BigInteger.prototype.equals = function (v) { + return this.compare(v) === 0; + }; + NativeBigInt.prototype.eq = NativeBigInt.prototype.equals = SmallInteger.prototype.eq = SmallInteger.prototype.equals = BigInteger.prototype.eq = BigInteger.prototype.equals; + + BigInteger.prototype.notEquals = function (v) { + return this.compare(v) !== 0; + }; + NativeBigInt.prototype.neq = NativeBigInt.prototype.notEquals = SmallInteger.prototype.neq = SmallInteger.prototype.notEquals = BigInteger.prototype.neq = BigInteger.prototype.notEquals; + + BigInteger.prototype.greater = function (v) { + return this.compare(v) > 0; + }; + NativeBigInt.prototype.gt = NativeBigInt.prototype.greater = SmallInteger.prototype.gt = SmallInteger.prototype.greater = BigInteger.prototype.gt = BigInteger.prototype.greater; + + BigInteger.prototype.lesser = function (v) { + return this.compare(v) < 0; + }; + NativeBigInt.prototype.lt = NativeBigInt.prototype.lesser = SmallInteger.prototype.lt = SmallInteger.prototype.lesser = BigInteger.prototype.lt = BigInteger.prototype.lesser; + + BigInteger.prototype.greaterOrEquals = function (v) { + return this.compare(v) >= 0; + }; + NativeBigInt.prototype.geq = NativeBigInt.prototype.greaterOrEquals = SmallInteger.prototype.geq = SmallInteger.prototype.greaterOrEquals = BigInteger.prototype.geq = BigInteger.prototype.greaterOrEquals; + + BigInteger.prototype.lesserOrEquals = function (v) { + return this.compare(v) <= 0; + }; + NativeBigInt.prototype.leq = NativeBigInt.prototype.lesserOrEquals = SmallInteger.prototype.leq = SmallInteger.prototype.lesserOrEquals = BigInteger.prototype.leq = BigInteger.prototype.lesserOrEquals; + + BigInteger.prototype.isEven = function () { + return (this.value[0] & 1) === 0; + }; + SmallInteger.prototype.isEven = function () { + return (this.value & 1) === 0; + }; + NativeBigInt.prototype.isEven = function () { + return (this.value & BigInt(1)) === BigInt(0); + } + + BigInteger.prototype.isOdd = function () { + return (this.value[0] & 1) === 1; + }; + SmallInteger.prototype.isOdd = function () { + return (this.value & 1) === 1; + }; + NativeBigInt.prototype.isOdd = function () { + return (this.value & BigInt(1)) === BigInt(1); + } + + BigInteger.prototype.isPositive = function () { + return !this.sign; + }; + SmallInteger.prototype.isPositive = function () { + return this.value > 0; + }; + NativeBigInt.prototype.isPositive = SmallInteger.prototype.isPositive; + + BigInteger.prototype.isNegative = function () { + return this.sign; + }; + SmallInteger.prototype.isNegative = function () { + return this.value < 0; + }; + NativeBigInt.prototype.isNegative = SmallInteger.prototype.isNegative; + + BigInteger.prototype.isUnit = function () { + return false; + }; + SmallInteger.prototype.isUnit = function () { + return Math.abs(this.value) === 1; + }; + NativeBigInt.prototype.isUnit = function () { + return this.abs().value === BigInt(1); + } + + BigInteger.prototype.isZero = function () { + return false; + }; + SmallInteger.prototype.isZero = function () { + return this.value === 0; + }; + NativeBigInt.prototype.isZero = function () { + return this.value === BigInt(0); + } + + BigInteger.prototype.isDivisibleBy = function (v) { + var n = parseValue(v); + if (n.isZero()) return false; + if (n.isUnit()) return true; + if (n.compareAbs(2) === 0) return this.isEven(); + return this.mod(n).isZero(); + }; + NativeBigInt.prototype.isDivisibleBy = SmallInteger.prototype.isDivisibleBy = BigInteger.prototype.isDivisibleBy; + + function isBasicPrime(v) { + var n = v.abs(); + if (n.isUnit()) return false; + if (n.equals(2) || n.equals(3) || n.equals(5)) return true; + if (n.isEven() || n.isDivisibleBy(3) || n.isDivisibleBy(5)) return false; + if (n.lesser(49)) return true; + // we don't know if it's prime: let the other functions figure it out + } + + function millerRabinTest(n, a) { + var nPrev = n.prev(), + b = nPrev, + r = 0, + d, t, i, x; + while (b.isEven()) b = b.divide(2), r++; + next: for (i = 0; i < a.length; i++) { + if (n.lesser(a[i])) continue; + x = bigInt(a[i]).modPow(b, n); + if (x.isUnit() || x.equals(nPrev)) continue; + for (d = r - 1; d != 0; d--) { + x = x.square().mod(n); + if (x.isUnit()) return false; + if (x.equals(nPrev)) continue next; + } + return false; + } + return true; + } + + // Set "strict" to true to force GRH-supported lower bound of 2*log(N)^2 + BigInteger.prototype.isPrime = function (strict) { + var isPrime = isBasicPrime(this); + if (isPrime !== undefined) return isPrime; + var n = this.abs(); + var bits = n.bitLength(); + if (bits <= 64) + return millerRabinTest(n, [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]); + var logN = Math.log(2) * bits.toJSNumber(); + var t = Math.ceil((strict === true) ? (2 * Math.pow(logN, 2)) : logN); + for (var a = [], i = 0; i < t; i++) { + a.push(bigInt(i + 2)); + } + return millerRabinTest(n, a); + }; + NativeBigInt.prototype.isPrime = SmallInteger.prototype.isPrime = BigInteger.prototype.isPrime; + + BigInteger.prototype.isProbablePrime = function (iterations, rng) { + var isPrime = isBasicPrime(this); + if (isPrime !== undefined) return isPrime; + var n = this.abs(); + var t = iterations === undefined ? 5 : iterations; + for (var a = [], i = 0; i < t; i++) { + a.push(bigInt.randBetween(2, n.minus(2), rng)); + } + return millerRabinTest(n, a); + }; + NativeBigInt.prototype.isProbablePrime = SmallInteger.prototype.isProbablePrime = BigInteger.prototype.isProbablePrime; + + BigInteger.prototype.modInv = function (n) { + var t = bigInt.zero, newT = bigInt.one, r = parseValue(n), newR = this.abs(), q, lastT, lastR; + while (!newR.isZero()) { + q = r.divide(newR); + lastT = t; + lastR = r; + t = newT; + r = newR; + newT = lastT.subtract(q.multiply(newT)); + newR = lastR.subtract(q.multiply(newR)); + } + if (!r.isUnit()) throw new Error(this.toString() + " and " + n.toString() + " are not co-prime"); + if (t.compare(0) === -1) { + t = t.add(n); + } + if (this.isNegative()) { + return t.negate(); + } + return t; + }; + + NativeBigInt.prototype.modInv = SmallInteger.prototype.modInv = BigInteger.prototype.modInv; + + BigInteger.prototype.next = function () { + var value = this.value; + if (this.sign) { + return subtractSmall(value, 1, this.sign); + } + return new BigInteger(addSmall(value, 1), this.sign); + }; + SmallInteger.prototype.next = function () { + var value = this.value; + if (value + 1 < MAX_INT) return new SmallInteger(value + 1); + return new BigInteger(MAX_INT_ARR, false); + }; + NativeBigInt.prototype.next = function () { + return new NativeBigInt(this.value + BigInt(1)); + } + + BigInteger.prototype.prev = function () { + var value = this.value; + if (this.sign) { + return new BigInteger(addSmall(value, 1), true); + } + return subtractSmall(value, 1, this.sign); + }; + SmallInteger.prototype.prev = function () { + var value = this.value; + if (value - 1 > -MAX_INT) return new SmallInteger(value - 1); + return new BigInteger(MAX_INT_ARR, true); + }; + NativeBigInt.prototype.prev = function () { + return new NativeBigInt(this.value - BigInt(1)); + } + + var powersOfTwo = [1]; + while (2 * powersOfTwo[powersOfTwo.length - 1] <= BASE) powersOfTwo.push(2 * powersOfTwo[powersOfTwo.length - 1]); + var powers2Length = powersOfTwo.length, highestPower2 = powersOfTwo[powers2Length - 1]; + + function shift_isSmall(n) { + return Math.abs(n) <= BASE; + } + + BigInteger.prototype.shiftLeft = function (v) { + var n = parseValue(v).toJSNumber(); + if (!shift_isSmall(n)) { + throw new Error(String(n) + " is too large for shifting."); + } + if (n < 0) return this.shiftRight(-n); + var result = this; + if (result.isZero()) return result; + while (n >= powers2Length) { + result = result.multiply(highestPower2); + n -= powers2Length - 1; + } + return result.multiply(powersOfTwo[n]); + }; + NativeBigInt.prototype.shiftLeft = SmallInteger.prototype.shiftLeft = BigInteger.prototype.shiftLeft; + + BigInteger.prototype.shiftRight = function (v) { + var remQuo; + var n = parseValue(v).toJSNumber(); + if (!shift_isSmall(n)) { + throw new Error(String(n) + " is too large for shifting."); + } + if (n < 0) return this.shiftLeft(-n); + var result = this; + while (n >= powers2Length) { + if (result.isZero() || (result.isNegative() && result.isUnit())) return result; + remQuo = divModAny(result, highestPower2); + result = remQuo[1].isNegative() ? remQuo[0].prev() : remQuo[0]; + n -= powers2Length - 1; + } + remQuo = divModAny(result, powersOfTwo[n]); + return remQuo[1].isNegative() ? remQuo[0].prev() : remQuo[0]; + }; + NativeBigInt.prototype.shiftRight = SmallInteger.prototype.shiftRight = BigInteger.prototype.shiftRight; + + function bitwise(x, y, fn) { + y = parseValue(y); + var xSign = x.isNegative(), ySign = y.isNegative(); + var xRem = xSign ? x.not() : x, + yRem = ySign ? y.not() : y; + var xDigit = 0, yDigit = 0; + var xDivMod = null, yDivMod = null; + var result = []; + while (!xRem.isZero() || !yRem.isZero()) { + xDivMod = divModAny(xRem, highestPower2); + xDigit = xDivMod[1].toJSNumber(); + if (xSign) { + xDigit = highestPower2 - 1 - xDigit; // two's complement for negative numbers + } + + yDivMod = divModAny(yRem, highestPower2); + yDigit = yDivMod[1].toJSNumber(); + if (ySign) { + yDigit = highestPower2 - 1 - yDigit; // two's complement for negative numbers + } + + xRem = xDivMod[0]; + yRem = yDivMod[0]; + result.push(fn(xDigit, yDigit)); + } + var sum = fn(xSign ? 1 : 0, ySign ? 1 : 0) !== 0 ? bigInt(-1) : bigInt(0); + for (var i = result.length - 1; i >= 0; i -= 1) { + sum = sum.multiply(highestPower2).add(bigInt(result[i])); + } + return sum; + } + + BigInteger.prototype.not = function () { + return this.negate().prev(); + }; + NativeBigInt.prototype.not = SmallInteger.prototype.not = BigInteger.prototype.not; + + BigInteger.prototype.and = function (n) { + return bitwise(this, n, function (a, b) { return a & b; }); + }; + NativeBigInt.prototype.and = SmallInteger.prototype.and = BigInteger.prototype.and; + + BigInteger.prototype.or = function (n) { + return bitwise(this, n, function (a, b) { return a | b; }); + }; + NativeBigInt.prototype.or = SmallInteger.prototype.or = BigInteger.prototype.or; + + BigInteger.prototype.xor = function (n) { + return bitwise(this, n, function (a, b) { return a ^ b; }); + }; + NativeBigInt.prototype.xor = SmallInteger.prototype.xor = BigInteger.prototype.xor; + + var LOBMASK_I = 1 << 30, LOBMASK_BI = (BASE & -BASE) * (BASE & -BASE) | LOBMASK_I; + function roughLOB(n) { // get lowestOneBit (rough) + // SmallInteger: return Min(lowestOneBit(n), 1 << 30) + // BigInteger: return Min(lowestOneBit(n), 1 << 14) [BASE=1e7] + var v = n.value, + x = typeof v === "number" ? v | LOBMASK_I : + typeof v === "bigint" ? v | BigInt(LOBMASK_I) : + v[0] + v[1] * BASE | LOBMASK_BI; + return x & -x; + } + + function integerLogarithm(value, base) { + if (base.compareTo(value) <= 0) { + var tmp = integerLogarithm(value, base.square(base)); + var p = tmp.p; + var e = tmp.e; + var t = p.multiply(base); + return t.compareTo(value) <= 0 ? { p: t, e: e * 2 + 1 } : { p: p, e: e * 2 }; + } + return { p: bigInt(1), e: 0 }; + } + + BigInteger.prototype.bitLength = function () { + var n = this; + if (n.compareTo(bigInt(0)) < 0) { + n = n.negate().subtract(bigInt(1)); + } + if (n.compareTo(bigInt(0)) === 0) { + return bigInt(0); + } + return bigInt(integerLogarithm(n, bigInt(2)).e).add(bigInt(1)); + } + NativeBigInt.prototype.bitLength = SmallInteger.prototype.bitLength = BigInteger.prototype.bitLength; + + function max(a, b) { + a = parseValue(a); + b = parseValue(b); + return a.greater(b) ? a : b; + } + function min(a, b) { + a = parseValue(a); + b = parseValue(b); + return a.lesser(b) ? a : b; + } + function gcd(a, b) { + a = parseValue(a).abs(); + b = parseValue(b).abs(); + if (a.equals(b)) return a; + if (a.isZero()) return b; + if (b.isZero()) return a; + var c = Integer[1], d, t; + while (a.isEven() && b.isEven()) { + d = min(roughLOB(a), roughLOB(b)); + a = a.divide(d); + b = b.divide(d); + c = c.multiply(d); + } + while (a.isEven()) { + a = a.divide(roughLOB(a)); + } + do { + while (b.isEven()) { + b = b.divide(roughLOB(b)); + } + if (a.greater(b)) { + t = b; b = a; a = t; + } + b = b.subtract(a); + } while (!b.isZero()); + return c.isUnit() ? a : a.multiply(c); + } + function lcm(a, b) { + a = parseValue(a).abs(); + b = parseValue(b).abs(); + return a.divide(gcd(a, b)).multiply(b); + } + function randBetween(a, b, rng) { + a = parseValue(a); + b = parseValue(b); + var usedRNG = rng || Math.random; + var low = min(a, b), high = max(a, b); + var range = high.subtract(low).add(1); + if (range.isSmall) return low.add(Math.floor(usedRNG() * range)); + var digits = toBase(range, BASE).value; + var result = [], restricted = true; + for (var i = 0; i < digits.length; i++) { + var top = restricted ? digits[i] + (i + 1 < digits.length ? digits[i + 1] / BASE : 0) : BASE; + var digit = truncate(usedRNG() * top); + result.push(digit); + if (digit < digits[i]) restricted = false; + } + return low.add(Integer.fromArray(result, BASE, false)); + } + + var parseBase = function (text, base, alphabet, caseSensitive) { + alphabet = alphabet || DEFAULT_ALPHABET; + text = String(text); + if (!caseSensitive) { + text = text.toLowerCase(); + alphabet = alphabet.toLowerCase(); + } + var length = text.length; + var i; + var absBase = Math.abs(base); + var alphabetValues = {}; + for (i = 0; i < alphabet.length; i++) { + alphabetValues[alphabet[i]] = i; + } + for (i = 0; i < length; i++) { + var c = text[i]; + if (c === "-") continue; + if (c in alphabetValues) { + if (alphabetValues[c] >= absBase) { + if (c === "1" && absBase === 1) continue; + throw new Error(c + " is not a valid digit in base " + base + "."); + } + } + } + base = parseValue(base); + var digits = []; + var isNegative = text[0] === "-"; + for (i = isNegative ? 1 : 0; i < text.length; i++) { + var c = text[i]; + if (c in alphabetValues) digits.push(parseValue(alphabetValues[c])); + else if (c === "<") { + var start = i; + do { i++; } while (text[i] !== ">" && i < text.length); + digits.push(parseValue(text.slice(start + 1, i))); + } + else throw new Error(c + " is not a valid character"); + } + return parseBaseFromArray(digits, base, isNegative); + }; + + function parseBaseFromArray(digits, base, isNegative) { + var val = Integer[0], pow = Integer[1], i; + for (i = digits.length - 1; i >= 0; i--) { + val = val.add(digits[i].times(pow)); + pow = pow.times(base); + } + return isNegative ? val.negate() : val; + } + + function stringify(digit, alphabet) { + alphabet = alphabet || DEFAULT_ALPHABET; + if (digit < alphabet.length) { + return alphabet[digit]; + } + return "<" + digit + ">"; + } + + function toBase(n, base) { + base = bigInt(base); + if (base.isZero()) { + if (n.isZero()) return { value: [0], isNegative: false }; + throw new Error("Cannot convert nonzero numbers to base 0."); + } + if (base.equals(-1)) { + if (n.isZero()) return { value: [0], isNegative: false }; + if (n.isNegative()) + return { + value: [].concat.apply([], Array.apply(null, Array(-n.toJSNumber())) + .map(Array.prototype.valueOf, [1, 0]) + ), + isNegative: false + }; + + var arr = Array.apply(null, Array(n.toJSNumber() - 1)) + .map(Array.prototype.valueOf, [0, 1]); + arr.unshift([1]); + return { + value: [].concat.apply([], arr), + isNegative: false + }; + } + + var neg = false; + if (n.isNegative() && base.isPositive()) { + neg = true; + n = n.abs(); + } + if (base.isUnit()) { + if (n.isZero()) return { value: [0], isNegative: false }; + + return { + value: Array.apply(null, Array(n.toJSNumber())) + .map(Number.prototype.valueOf, 1), + isNegative: neg + }; + } + var out = []; + var left = n, divmod; + while (left.isNegative() || left.compareAbs(base) >= 0) { + divmod = left.divmod(base); + left = divmod.quotient; + var digit = divmod.remainder; + if (digit.isNegative()) { + digit = base.minus(digit).abs(); + left = left.next(); + } + out.push(digit.toJSNumber()); + } + out.push(left.toJSNumber()); + return { value: out.reverse(), isNegative: neg }; + } + + function toBaseString(n, base, alphabet) { + var arr = toBase(n, base); + return (arr.isNegative ? "-" : "") + arr.value.map(function (x) { + return stringify(x, alphabet); + }).join(''); + } + + BigInteger.prototype.toArray = function (radix) { + return toBase(this, radix); + }; + + SmallInteger.prototype.toArray = function (radix) { + return toBase(this, radix); + }; + + NativeBigInt.prototype.toArray = function (radix) { + return toBase(this, radix); + }; + + BigInteger.prototype.toString = function (radix, alphabet) { + if (radix === undefined) radix = 10; + if (radix !== 10 || alphabet) return toBaseString(this, radix, alphabet); + var v = this.value, l = v.length, str = String(v[--l]), zeros = "0000000", digit; + while (--l >= 0) { + digit = String(v[l]); + str += zeros.slice(digit.length) + digit; + } + var sign = this.sign ? "-" : ""; + return sign + str; + }; + + SmallInteger.prototype.toString = function (radix, alphabet) { + if (radix === undefined) radix = 10; + if (radix != 10 || alphabet) return toBaseString(this, radix, alphabet); + return String(this.value); + }; + + NativeBigInt.prototype.toString = SmallInteger.prototype.toString; + + NativeBigInt.prototype.toJSON = BigInteger.prototype.toJSON = SmallInteger.prototype.toJSON = function () { return this.toString(); } + + BigInteger.prototype.valueOf = function () { + return parseInt(this.toString(), 10); + }; + BigInteger.prototype.toJSNumber = BigInteger.prototype.valueOf; + + SmallInteger.prototype.valueOf = function () { + return this.value; + }; + SmallInteger.prototype.toJSNumber = SmallInteger.prototype.valueOf; + NativeBigInt.prototype.valueOf = NativeBigInt.prototype.toJSNumber = function () { + return parseInt(this.toString(), 10); + } + + function parseStringValue(v) { + if (isPrecise(+v)) { + var x = +v; + if (x === truncate(x)) + return supportsNativeBigInt ? new NativeBigInt(BigInt(x)) : new SmallInteger(x); + throw new Error("Invalid integer: " + v); + } + var sign = v[0] === "-"; + if (sign) v = v.slice(1); + var split = v.split(/e/i); + if (split.length > 2) throw new Error("Invalid integer: " + split.join("e")); + if (split.length === 2) { + var exp = split[1]; + if (exp[0] === "+") exp = exp.slice(1); + exp = +exp; + if (exp !== truncate(exp) || !isPrecise(exp)) throw new Error("Invalid integer: " + exp + " is not a valid exponent."); + var text = split[0]; + var decimalPlace = text.indexOf("."); + if (decimalPlace >= 0) { + exp -= text.length - decimalPlace - 1; + text = text.slice(0, decimalPlace) + text.slice(decimalPlace + 1); + } + if (exp < 0) throw new Error("Cannot include negative exponent part for integers"); + text += (new Array(exp + 1)).join("0"); + v = text; + } + var isValid = /^([0-9][0-9]*)$/.test(v); + if (!isValid) throw new Error("Invalid integer: " + v); + if (supportsNativeBigInt) { + return new NativeBigInt(BigInt(sign ? "-" + v : v)); + } + var r = [], max = v.length, l = LOG_BASE, min = max - l; + while (max > 0) { + r.push(+v.slice(min, max)); + min -= l; + if (min < 0) min = 0; + max -= l; + } + trim(r); + return new BigInteger(r, sign); + } + + function parseNumberValue(v) { + if (supportsNativeBigInt) { + return new NativeBigInt(BigInt(v)); + } + if (isPrecise(v)) { + if (v !== truncate(v)) throw new Error(v + " is not an integer."); + return new SmallInteger(v); + } + return parseStringValue(v.toString()); + } + + function parseValue(v) { + if (typeof v === "number") { + return parseNumberValue(v); + } + if (typeof v === "string") { + return parseStringValue(v); + } + if (typeof v === "bigint") { + return new NativeBigInt(v); + } + return v; + } + // Pre-define numbers in range [-999,999] + for (var i = 0; i < 1000; i++) { + Integer[i] = parseValue(i); + if (i > 0) Integer[-i] = parseValue(-i); + } + // Backwards compatibility + Integer.one = Integer[1]; + Integer.zero = Integer[0]; + Integer.minusOne = Integer[-1]; + Integer.max = max; + Integer.min = min; + Integer.gcd = gcd; + Integer.lcm = lcm; + Integer.isInstance = function (x) { return x instanceof BigInteger || x instanceof SmallInteger || x instanceof NativeBigInt; }; + Integer.randBetween = randBetween; + + Integer.fromArray = function (digits, base, isNegative) { + return parseBaseFromArray(digits.map(parseValue), parseValue(base || 10), isNegative); + }; + + return Integer; +})(); + +// Node.js check +if (typeof module !== "undefined" && module.hasOwnProperty("exports")) { + module.exports = bigInt; +} + +//amd check +if (typeof define === "function" && define.amd) { + define( function () { + return bigInt; + }); +} diff --git a/tools/ui/src/lib/vendors/big-integer/LICENSE b/tools/ui/src/lib/vendors/big-integer/LICENSE new file mode 100644 index 000000000..3ce22da38 --- /dev/null +++ b/tools/ui/src/lib/vendors/big-integer/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/tools/ui/src/lib/vendors/decimal.js/LICENCE.md b/tools/ui/src/lib/vendors/decimal.js/LICENCE.md new file mode 100644 index 000000000..57740b9d4 --- /dev/null +++ b/tools/ui/src/lib/vendors/decimal.js/LICENCE.md @@ -0,0 +1,23 @@ +The MIT Licence. + +Copyright (c) 2025 Michael Mclaughlin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/tools/ui/src/lib/vendors/decimal.js/decimal.js b/tools/ui/src/lib/vendors/decimal.js/decimal.js new file mode 100644 index 000000000..23295c6ac --- /dev/null +++ b/tools/ui/src/lib/vendors/decimal.js/decimal.js @@ -0,0 +1,4951 @@ +;(function (globalScope) { + 'use strict'; + + + /*! + * decimal.js v10.6.0 + * An arbitrary-precision Decimal type for JavaScript. + * https://github.com/MikeMcl/decimal.js + * Copyright (c) 2025 Michael Mclaughlin + * MIT Licence + */ + + + // ----------------------------------- EDITABLE DEFAULTS ------------------------------------ // + + + // The maximum exponent magnitude. + // The limit on the value of `toExpNeg`, `toExpPos`, `minE` and `maxE`. + var EXP_LIMIT = 9e15, // 0 to 9e15 + + // The limit on the value of `precision`, and on the value of the first argument to + // `toDecimalPlaces`, `toExponential`, `toFixed`, `toPrecision` and `toSignificantDigits`. + MAX_DIGITS = 1e9, // 0 to 1e9 + + // Base conversion alphabet. + NUMERALS = '0123456789abcdef', + + // The natural logarithm of 10 (1025 digits). + LN10 = '2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058', + + // Pi (1025 digits). + PI = '3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789', + + + // The initial configuration properties of the Decimal constructor. + DEFAULTS = { + + // These values must be integers within the stated ranges (inclusive). + // Most of these values can be changed at run-time using the `Decimal.config` method. + + // The maximum number of significant digits of the result of a calculation or base conversion. + // E.g. `Decimal.config({ precision: 20 });` + precision: 20, // 1 to MAX_DIGITS + + // The rounding mode used when rounding to `precision`. + // + // ROUND_UP 0 Away from zero. + // ROUND_DOWN 1 Towards zero. + // ROUND_CEIL 2 Towards +Infinity. + // ROUND_FLOOR 3 Towards -Infinity. + // ROUND_HALF_UP 4 Towards nearest neighbour. If equidistant, up. + // ROUND_HALF_DOWN 5 Towards nearest neighbour. If equidistant, down. + // ROUND_HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour. + // ROUND_HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity. + // ROUND_HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity. + // + // E.g. + // `Decimal.rounding = 4;` + // `Decimal.rounding = Decimal.ROUND_HALF_UP;` + rounding: 4, // 0 to 8 + + // The modulo mode used when calculating the modulus: a mod n. + // The quotient (q = a / n) is calculated according to the corresponding rounding mode. + // The remainder (r) is calculated as: r = a - n * q. + // + // UP 0 The remainder is positive if the dividend is negative, else is negative. + // DOWN 1 The remainder has the same sign as the dividend (JavaScript %). + // FLOOR 3 The remainder has the same sign as the divisor (Python %). + // HALF_EVEN 6 The IEEE 754 remainder function. + // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)). Always positive. + // + // Truncated division (1), floored division (3), the IEEE 754 remainder (6), and Euclidian + // division (9) are commonly used for the modulus operation. The other rounding modes can also + // be used, but they may not give useful results. + modulo: 1, // 0 to 9 + + // The exponent value at and beneath which `toString` returns exponential notation. + // JavaScript numbers: -7 + toExpNeg: -7, // 0 to -EXP_LIMIT + + // The exponent value at and above which `toString` returns exponential notation. + // JavaScript numbers: 21 + toExpPos: 21, // 0 to EXP_LIMIT + + // The minimum exponent value, beneath which underflow to zero occurs. + // JavaScript numbers: -324 (5e-324) + minE: -EXP_LIMIT, // -1 to -EXP_LIMIT + + // The maximum exponent value, above which overflow to Infinity occurs. + // JavaScript numbers: 308 (1.7976931348623157e+308) + maxE: EXP_LIMIT, // 1 to EXP_LIMIT + + // Whether to use cryptographically-secure random number generation, if available. + crypto: false // true/false + }, + + + // ----------------------------------- END OF EDITABLE DEFAULTS ------------------------------- // + + + Decimal, inexact, noConflict, quadrant, + external = true, + + decimalError = '[DecimalError] ', + invalidArgument = decimalError + 'Invalid argument: ', + precisionLimitExceeded = decimalError + 'Precision limit exceeded', + cryptoUnavailable = decimalError + 'crypto unavailable', + tag = '[object Decimal]', + + mathfloor = Math.floor, + mathpow = Math.pow, + + isBinary = /^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i, + isHex = /^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i, + isOctal = /^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i, + isDecimal = /^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i, + + BASE = 1e7, + LOG_BASE = 7, + MAX_SAFE_INTEGER = 9007199254740991, + + LN10_PRECISION = LN10.length - 1, + PI_PRECISION = PI.length - 1, + + // Decimal.prototype object + P = { toStringTag: tag }; + + + // Decimal prototype methods + + + /* + * absoluteValue abs + * ceil + * clampedTo clamp + * comparedTo cmp + * cosine cos + * cubeRoot cbrt + * decimalPlaces dp + * dividedBy div + * dividedToIntegerBy divToInt + * equals eq + * floor + * greaterThan gt + * greaterThanOrEqualTo gte + * hyperbolicCosine cosh + * hyperbolicSine sinh + * hyperbolicTangent tanh + * inverseCosine acos + * inverseHyperbolicCosine acosh + * inverseHyperbolicSine asinh + * inverseHyperbolicTangent atanh + * inverseSine asin + * inverseTangent atan + * isFinite + * isInteger isInt + * isNaN + * isNegative isNeg + * isPositive isPos + * isZero + * lessThan lt + * lessThanOrEqualTo lte + * logarithm log + * [maximum] [max] + * [minimum] [min] + * minus sub + * modulo mod + * naturalExponential exp + * naturalLogarithm ln + * negated neg + * plus add + * precision sd + * round + * sine sin + * squareRoot sqrt + * tangent tan + * times mul + * toBinary + * toDecimalPlaces toDP + * toExponential + * toFixed + * toFraction + * toHexadecimal toHex + * toNearest + * toNumber + * toOctal + * toPower pow + * toPrecision + * toSignificantDigits toSD + * toString + * truncated trunc + * valueOf toJSON + */ + + + /* + * Return a new Decimal whose value is the absolute value of this Decimal. + * + */ + P.absoluteValue = P.abs = function () { + var x = new this.constructor(this); + if (x.s < 0) x.s = 1; + return finalise(x); + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the + * direction of positive Infinity. + * + */ + P.ceil = function () { + return finalise(new this.constructor(this), this.e + 1, 2); + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal clamped to the range + * delineated by `min` and `max`. + * + * min {number|string|bigint|Decimal} + * max {number|string|bigint|Decimal} + * + */ + P.clampedTo = P.clamp = function (min, max) { + var k, + x = this, + Ctor = x.constructor; + min = new Ctor(min); + max = new Ctor(max); + if (!min.s || !max.s) return new Ctor(NaN); + if (min.gt(max)) throw Error(invalidArgument + max); + k = x.cmp(min); + return k < 0 ? min : x.cmp(max) > 0 ? max : new Ctor(x); + }; + + + /* + * Return + * 1 if the value of this Decimal is greater than the value of `y`, + * -1 if the value of this Decimal is less than the value of `y`, + * 0 if they have the same value, + * NaN if the value of either Decimal is NaN. + * + */ + P.comparedTo = P.cmp = function (y) { + var i, j, xdL, ydL, + x = this, + xd = x.d, + yd = (y = new x.constructor(y)).d, + xs = x.s, + ys = y.s; + + // Either NaN or ±Infinity? + if (!xd || !yd) { + return !xs || !ys ? NaN : xs !== ys ? xs : xd === yd ? 0 : !xd ^ xs < 0 ? 1 : -1; + } + + // Either zero? + if (!xd[0] || !yd[0]) return xd[0] ? xs : yd[0] ? -ys : 0; + + // Signs differ? + if (xs !== ys) return xs; + + // Compare exponents. + if (x.e !== y.e) return x.e > y.e ^ xs < 0 ? 1 : -1; + + xdL = xd.length; + ydL = yd.length; + + // Compare digit by digit. + for (i = 0, j = xdL < ydL ? xdL : ydL; i < j; ++i) { + if (xd[i] !== yd[i]) return xd[i] > yd[i] ^ xs < 0 ? 1 : -1; + } + + // Compare lengths. + return xdL === ydL ? 0 : xdL > ydL ^ xs < 0 ? 1 : -1; + }; + + + /* + * Return a new Decimal whose value is the cosine of the value in radians of this Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-1, 1] + * + * cos(0) = 1 + * cos(-0) = 1 + * cos(Infinity) = NaN + * cos(-Infinity) = NaN + * cos(NaN) = NaN + * + */ + P.cosine = P.cos = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (!x.d) return new Ctor(NaN); + + // cos(0) = cos(-0) = 1 + if (!x.d[0]) return new Ctor(1); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE; + Ctor.rounding = 1; + + x = cosine(Ctor, toLessThanHalfPi(Ctor, x)); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return finalise(quadrant == 2 || quadrant == 3 ? x.neg() : x, pr, rm, true); + }; + + + /* + * + * Return a new Decimal whose value is the cube root of the value of this Decimal, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + * cbrt(0) = 0 + * cbrt(-0) = -0 + * cbrt(1) = 1 + * cbrt(-1) = -1 + * cbrt(N) = N + * cbrt(-I) = -I + * cbrt(I) = I + * + * Math.cbrt(x) = (x < 0 ? -Math.pow(-x, 1/3) : Math.pow(x, 1/3)) + * + */ + P.cubeRoot = P.cbrt = function () { + var e, m, n, r, rep, s, sd, t, t3, t3plusx, + x = this, + Ctor = x.constructor; + + if (!x.isFinite() || x.isZero()) return new Ctor(x); + external = false; + + // Initial estimate. + s = x.s * mathpow(x.s * x, 1 / 3); + + // Math.cbrt underflow/overflow? + // Pass x to Math.pow as integer, then adjust the exponent of the result. + if (!s || Math.abs(s) == 1 / 0) { + n = digitsToString(x.d); + e = x.e; + + // Adjust n exponent so it is a multiple of 3 away from x exponent. + if (s = (e - n.length + 1) % 3) n += (s == 1 || s == -2 ? '0' : '00'); + s = mathpow(n, 1 / 3); + + // Rarely, e may be one less than the result exponent value. + e = mathfloor((e + 1) / 3) - (e % 3 == (e < 0 ? -1 : 2)); + + if (s == 1 / 0) { + n = '5e' + e; + } else { + n = s.toExponential(); + n = n.slice(0, n.indexOf('e') + 1) + e; + } + + r = new Ctor(n); + r.s = x.s; + } else { + r = new Ctor(s.toString()); + } + + sd = (e = Ctor.precision) + 3; + + // Halley's method. + // TODO? Compare Newton's method. + for (;;) { + t = r; + t3 = t.times(t).times(t); + t3plusx = t3.plus(x); + r = divide(t3plusx.plus(x).times(t), t3plusx.plus(t3), sd + 2, 1); + + // TODO? Replace with for-loop and checkRoundingDigits. + if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) { + n = n.slice(sd - 3, sd + 1); + + // The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or 4999 + // , i.e. approaching a rounding boundary, continue the iteration. + if (n == '9999' || !rep && n == '4999') { + + // On the first iteration only, check to see if rounding up gives the exact result as the + // nines may infinitely repeat. + if (!rep) { + finalise(t, e + 1, 0); + + if (t.times(t).times(t).eq(x)) { + r = t; + break; + } + } + + sd += 4; + rep = 1; + } else { + + // If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result. + // If not, then there are further digits and m will be truthy. + if (!+n || !+n.slice(1) && n.charAt(0) == '5') { + + // Truncate to the first rounding digit. + finalise(r, e + 1, 1); + m = !r.times(r).times(r).eq(x); + } + + break; + } + } + } + + external = true; + + return finalise(r, e, Ctor.rounding, m); + }; + + + /* + * Return the number of decimal places of the value of this Decimal. + * + */ + P.decimalPlaces = P.dp = function () { + var w, + d = this.d, + n = NaN; + + if (d) { + w = d.length - 1; + n = (w - mathfloor(this.e / LOG_BASE)) * LOG_BASE; + + // Subtract the number of trailing zeros of the last word. + w = d[w]; + if (w) for (; w % 10 == 0; w /= 10) n--; + if (n < 0) n = 0; + } + + return n; + }; + + + /* + * n / 0 = I + * n / N = N + * n / I = 0 + * 0 / n = 0 + * 0 / 0 = N + * 0 / N = N + * 0 / I = 0 + * N / n = N + * N / 0 = N + * N / N = N + * N / I = N + * I / n = I + * I / 0 = I + * I / N = N + * I / I = N + * + * Return a new Decimal whose value is the value of this Decimal divided by `y`, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + */ + P.dividedBy = P.div = function (y) { + return divide(this, new this.constructor(y)); + }; + + + /* + * Return a new Decimal whose value is the integer part of dividing the value of this Decimal + * by the value of `y`, rounded to `precision` significant digits using rounding mode `rounding`. + * + */ + P.dividedToIntegerBy = P.divToInt = function (y) { + var x = this, + Ctor = x.constructor; + return finalise(divide(x, new Ctor(y), 0, 1, 1), Ctor.precision, Ctor.rounding); + }; + + + /* + * Return true if the value of this Decimal is equal to the value of `y`, otherwise return false. + * + */ + P.equals = P.eq = function (y) { + return this.cmp(y) === 0; + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the + * direction of negative Infinity. + * + */ + P.floor = function () { + return finalise(new this.constructor(this), this.e + 1, 3); + }; + + + /* + * Return true if the value of this Decimal is greater than the value of `y`, otherwise return + * false. + * + */ + P.greaterThan = P.gt = function (y) { + return this.cmp(y) > 0; + }; + + + /* + * Return true if the value of this Decimal is greater than or equal to the value of `y`, + * otherwise return false. + * + */ + P.greaterThanOrEqualTo = P.gte = function (y) { + var k = this.cmp(y); + return k == 1 || k === 0; + }; + + + /* + * Return a new Decimal whose value is the hyperbolic cosine of the value in radians of this + * Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [1, Infinity] + * + * cosh(x) = 1 + x^2/2! + x^4/4! + x^6/6! + ... + * + * cosh(0) = 1 + * cosh(-0) = 1 + * cosh(Infinity) = Infinity + * cosh(-Infinity) = Infinity + * cosh(NaN) = NaN + * + * x time taken (ms) result + * 1000 9 9.8503555700852349694e+433 + * 10000 25 4.4034091128314607936e+4342 + * 100000 171 1.4033316802130615897e+43429 + * 1000000 3817 1.5166076984010437725e+434294 + * 10000000 abandoned after 2 minute wait + * + * TODO? Compare performance of cosh(x) = 0.5 * (exp(x) + exp(-x)) + * + */ + P.hyperbolicCosine = P.cosh = function () { + var k, n, pr, rm, len, + x = this, + Ctor = x.constructor, + one = new Ctor(1); + + if (!x.isFinite()) return new Ctor(x.s ? 1 / 0 : NaN); + if (x.isZero()) return one; + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + Math.max(x.e, x.sd()) + 4; + Ctor.rounding = 1; + len = x.d.length; + + // Argument reduction: cos(4x) = 1 - 8cos^2(x) + 8cos^4(x) + 1 + // i.e. cos(x) = 1 - cos^2(x/4)(8 - 8cos^2(x/4)) + + // Estimate the optimum number of times to use the argument reduction. + // TODO? Estimation reused from cosine() and may not be optimal here. + if (len < 32) { + k = Math.ceil(len / 3); + n = (1 / tinyPow(4, k)).toString(); + } else { + k = 16; + n = '2.3283064365386962890625e-10'; + } + + x = taylorSeries(Ctor, 1, x.times(n), new Ctor(1), true); + + // Reverse argument reduction + var cosh2_x, + i = k, + d8 = new Ctor(8); + for (; i--;) { + cosh2_x = x.times(x); + x = one.minus(cosh2_x.times(d8.minus(cosh2_x.times(d8)))); + } + + return finalise(x, Ctor.precision = pr, Ctor.rounding = rm, true); + }; + + + /* + * Return a new Decimal whose value is the hyperbolic sine of the value in radians of this + * Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-Infinity, Infinity] + * + * sinh(x) = x + x^3/3! + x^5/5! + x^7/7! + ... + * + * sinh(0) = 0 + * sinh(-0) = -0 + * sinh(Infinity) = Infinity + * sinh(-Infinity) = -Infinity + * sinh(NaN) = NaN + * + * x time taken (ms) + * 10 2 ms + * 100 5 ms + * 1000 14 ms + * 10000 82 ms + * 100000 886 ms 1.4033316802130615897e+43429 + * 200000 2613 ms + * 300000 5407 ms + * 400000 8824 ms + * 500000 13026 ms 8.7080643612718084129e+217146 + * 1000000 48543 ms + * + * TODO? Compare performance of sinh(x) = 0.5 * (exp(x) - exp(-x)) + * + */ + P.hyperbolicSine = P.sinh = function () { + var k, pr, rm, len, + x = this, + Ctor = x.constructor; + + if (!x.isFinite() || x.isZero()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + Math.max(x.e, x.sd()) + 4; + Ctor.rounding = 1; + len = x.d.length; + + if (len < 3) { + x = taylorSeries(Ctor, 2, x, x, true); + } else { + + // Alternative argument reduction: sinh(3x) = sinh(x)(3 + 4sinh^2(x)) + // i.e. sinh(x) = sinh(x/3)(3 + 4sinh^2(x/3)) + // 3 multiplications and 1 addition + + // Argument reduction: sinh(5x) = sinh(x)(5 + sinh^2(x)(20 + 16sinh^2(x))) + // i.e. sinh(x) = sinh(x/5)(5 + sinh^2(x/5)(20 + 16sinh^2(x/5))) + // 4 multiplications and 2 additions + + // Estimate the optimum number of times to use the argument reduction. + k = 1.4 * Math.sqrt(len); + k = k > 16 ? 16 : k | 0; + + x = x.times(1 / tinyPow(5, k)); + x = taylorSeries(Ctor, 2, x, x, true); + + // Reverse argument reduction + var sinh2_x, + d5 = new Ctor(5), + d16 = new Ctor(16), + d20 = new Ctor(20); + for (; k--;) { + sinh2_x = x.times(x); + x = x.times(d5.plus(sinh2_x.times(d16.times(sinh2_x).plus(d20)))); + } + } + + Ctor.precision = pr; + Ctor.rounding = rm; + + return finalise(x, pr, rm, true); + }; + + + /* + * Return a new Decimal whose value is the hyperbolic tangent of the value in radians of this + * Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-1, 1] + * + * tanh(x) = sinh(x) / cosh(x) + * + * tanh(0) = 0 + * tanh(-0) = -0 + * tanh(Infinity) = 1 + * tanh(-Infinity) = -1 + * tanh(NaN) = NaN + * + */ + P.hyperbolicTangent = P.tanh = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (!x.isFinite()) return new Ctor(x.s); + if (x.isZero()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + 7; + Ctor.rounding = 1; + + return divide(x.sinh(), x.cosh(), Ctor.precision = pr, Ctor.rounding = rm); + }; + + + /* + * Return a new Decimal whose value is the arccosine (inverse cosine) in radians of the value of + * this Decimal. + * + * Domain: [-1, 1] + * Range: [0, pi] + * + * acos(x) = pi/2 - asin(x) + * + * acos(0) = pi/2 + * acos(-0) = pi/2 + * acos(1) = 0 + * acos(-1) = pi + * acos(1/2) = pi/3 + * acos(-1/2) = 2*pi/3 + * acos(|x| > 1) = NaN + * acos(NaN) = NaN + * + */ + P.inverseCosine = P.acos = function () { + var x = this, + Ctor = x.constructor, + k = x.abs().cmp(1), + pr = Ctor.precision, + rm = Ctor.rounding; + + if (k !== -1) { + return k === 0 + // |x| is 1 + ? x.isNeg() ? getPi(Ctor, pr, rm) : new Ctor(0) + // |x| > 1 or x is NaN + : new Ctor(NaN); + } + + if (x.isZero()) return getPi(Ctor, pr + 4, rm).times(0.5); + + // TODO? Special case acos(0.5) = pi/3 and acos(-0.5) = 2*pi/3 + + Ctor.precision = pr + 6; + Ctor.rounding = 1; + + // See https://github.com/MikeMcl/decimal.js/pull/217 + x = new Ctor(1).minus(x).div(x.plus(1)).sqrt().atan(); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return x.times(2); + }; + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic cosine in radians of the + * value of this Decimal. + * + * Domain: [1, Infinity] + * Range: [0, Infinity] + * + * acosh(x) = ln(x + sqrt(x^2 - 1)) + * + * acosh(x < 1) = NaN + * acosh(NaN) = NaN + * acosh(Infinity) = Infinity + * acosh(-Infinity) = NaN + * acosh(0) = NaN + * acosh(-0) = NaN + * acosh(1) = 0 + * acosh(-1) = NaN + * + */ + P.inverseHyperbolicCosine = P.acosh = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (x.lte(1)) return new Ctor(x.eq(1) ? 0 : NaN); + if (!x.isFinite()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + Math.max(Math.abs(x.e), x.sd()) + 4; + Ctor.rounding = 1; + external = false; + + x = x.times(x).minus(1).sqrt().plus(x); + + external = true; + Ctor.precision = pr; + Ctor.rounding = rm; + + return x.ln(); + }; + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic sine in radians of the value + * of this Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-Infinity, Infinity] + * + * asinh(x) = ln(x + sqrt(x^2 + 1)) + * + * asinh(NaN) = NaN + * asinh(Infinity) = Infinity + * asinh(-Infinity) = -Infinity + * asinh(0) = 0 + * asinh(-0) = -0 + * + */ + P.inverseHyperbolicSine = P.asinh = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (!x.isFinite() || x.isZero()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + 2 * Math.max(Math.abs(x.e), x.sd()) + 6; + Ctor.rounding = 1; + external = false; + + x = x.times(x).plus(1).sqrt().plus(x); + + external = true; + Ctor.precision = pr; + Ctor.rounding = rm; + + return x.ln(); + }; + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic tangent in radians of the + * value of this Decimal. + * + * Domain: [-1, 1] + * Range: [-Infinity, Infinity] + * + * atanh(x) = 0.5 * ln((1 + x) / (1 - x)) + * + * atanh(|x| > 1) = NaN + * atanh(NaN) = NaN + * atanh(Infinity) = NaN + * atanh(-Infinity) = NaN + * atanh(0) = 0 + * atanh(-0) = -0 + * atanh(1) = Infinity + * atanh(-1) = -Infinity + * + */ + P.inverseHyperbolicTangent = P.atanh = function () { + var pr, rm, wpr, xsd, + x = this, + Ctor = x.constructor; + + if (!x.isFinite()) return new Ctor(NaN); + if (x.e >= 0) return new Ctor(x.abs().eq(1) ? x.s / 0 : x.isZero() ? x : NaN); + + pr = Ctor.precision; + rm = Ctor.rounding; + xsd = x.sd(); + + if (Math.max(xsd, pr) < 2 * -x.e - 1) return finalise(new Ctor(x), pr, rm, true); + + Ctor.precision = wpr = xsd - x.e; + + x = divide(x.plus(1), new Ctor(1).minus(x), wpr + pr, 1); + + Ctor.precision = pr + 4; + Ctor.rounding = 1; + + x = x.ln(); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return x.times(0.5); + }; + + + /* + * Return a new Decimal whose value is the arcsine (inverse sine) in radians of the value of this + * Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-pi/2, pi/2] + * + * asin(x) = 2*atan(x/(1 + sqrt(1 - x^2))) + * + * asin(0) = 0 + * asin(-0) = -0 + * asin(1/2) = pi/6 + * asin(-1/2) = -pi/6 + * asin(1) = pi/2 + * asin(-1) = -pi/2 + * asin(|x| > 1) = NaN + * asin(NaN) = NaN + * + * TODO? Compare performance of Taylor series. + * + */ + P.inverseSine = P.asin = function () { + var halfPi, k, + pr, rm, + x = this, + Ctor = x.constructor; + + if (x.isZero()) return new Ctor(x); + + k = x.abs().cmp(1); + pr = Ctor.precision; + rm = Ctor.rounding; + + if (k !== -1) { + + // |x| is 1 + if (k === 0) { + halfPi = getPi(Ctor, pr + 4, rm).times(0.5); + halfPi.s = x.s; + return halfPi; + } + + // |x| > 1 or x is NaN + return new Ctor(NaN); + } + + // TODO? Special case asin(1/2) = pi/6 and asin(-1/2) = -pi/6 + + Ctor.precision = pr + 6; + Ctor.rounding = 1; + + x = x.div(new Ctor(1).minus(x.times(x)).sqrt().plus(1)).atan(); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return x.times(2); + }; + + + /* + * Return a new Decimal whose value is the arctangent (inverse tangent) in radians of the value + * of this Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-pi/2, pi/2] + * + * atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ... + * + * atan(0) = 0 + * atan(-0) = -0 + * atan(1) = pi/4 + * atan(-1) = -pi/4 + * atan(Infinity) = pi/2 + * atan(-Infinity) = -pi/2 + * atan(NaN) = NaN + * + */ + P.inverseTangent = P.atan = function () { + var i, j, k, n, px, t, r, wpr, x2, + x = this, + Ctor = x.constructor, + pr = Ctor.precision, + rm = Ctor.rounding; + + if (!x.isFinite()) { + if (!x.s) return new Ctor(NaN); + if (pr + 4 <= PI_PRECISION) { + r = getPi(Ctor, pr + 4, rm).times(0.5); + r.s = x.s; + return r; + } + } else if (x.isZero()) { + return new Ctor(x); + } else if (x.abs().eq(1) && pr + 4 <= PI_PRECISION) { + r = getPi(Ctor, pr + 4, rm).times(0.25); + r.s = x.s; + return r; + } + + Ctor.precision = wpr = pr + 10; + Ctor.rounding = 1; + + // TODO? if (x >= 1 && pr <= PI_PRECISION) atan(x) = halfPi * x.s - atan(1 / x); + + // Argument reduction + // Ensure |x| < 0.42 + // atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2))) + + k = Math.min(28, wpr / LOG_BASE + 2 | 0); + + for (i = k; i; --i) x = x.div(x.times(x).plus(1).sqrt().plus(1)); + + external = false; + + j = Math.ceil(wpr / LOG_BASE); + n = 1; + x2 = x.times(x); + r = new Ctor(x); + px = x; + + // atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ... + for (; i !== -1;) { + px = px.times(x2); + t = r.minus(px.div(n += 2)); + + px = px.times(x2); + r = t.plus(px.div(n += 2)); + + if (r.d[j] !== void 0) for (i = j; r.d[i] === t.d[i] && i--;); + } + + if (k) r = r.times(2 << (k - 1)); + + external = true; + + return finalise(r, Ctor.precision = pr, Ctor.rounding = rm, true); + }; + + + /* + * Return true if the value of this Decimal is a finite number, otherwise return false. + * + */ + P.isFinite = function () { + return !!this.d; + }; + + + /* + * Return true if the value of this Decimal is an integer, otherwise return false. + * + */ + P.isInteger = P.isInt = function () { + return !!this.d && mathfloor(this.e / LOG_BASE) > this.d.length - 2; + }; + + + /* + * Return true if the value of this Decimal is NaN, otherwise return false. + * + */ + P.isNaN = function () { + return !this.s; + }; + + + /* + * Return true if the value of this Decimal is negative, otherwise return false. + * + */ + P.isNegative = P.isNeg = function () { + return this.s < 0; + }; + + + /* + * Return true if the value of this Decimal is positive, otherwise return false. + * + */ + P.isPositive = P.isPos = function () { + return this.s > 0; + }; + + + /* + * Return true if the value of this Decimal is 0 or -0, otherwise return false. + * + */ + P.isZero = function () { + return !!this.d && this.d[0] === 0; + }; + + + /* + * Return true if the value of this Decimal is less than `y`, otherwise return false. + * + */ + P.lessThan = P.lt = function (y) { + return this.cmp(y) < 0; + }; + + + /* + * Return true if the value of this Decimal is less than or equal to `y`, otherwise return false. + * + */ + P.lessThanOrEqualTo = P.lte = function (y) { + return this.cmp(y) < 1; + }; + + + /* + * Return the logarithm of the value of this Decimal to the specified base, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * If no base is specified, return log[10](arg). + * + * log[base](arg) = ln(arg) / ln(base) + * + * The result will always be correctly rounded if the base of the log is 10, and 'almost always' + * otherwise: + * + * Depending on the rounding mode, the result may be incorrectly rounded if the first fifteen + * rounding digits are [49]99999999999999 or [50]00000000000000. In that case, the maximum error + * between the result and the correctly rounded result will be one ulp (unit in the last place). + * + * log[-b](a) = NaN + * log[0](a) = NaN + * log[1](a) = NaN + * log[NaN](a) = NaN + * log[Infinity](a) = NaN + * log[b](0) = -Infinity + * log[b](-0) = -Infinity + * log[b](-a) = NaN + * log[b](1) = 0 + * log[b](Infinity) = Infinity + * log[b](NaN) = NaN + * + * [base] {number|string|bigint|Decimal} The base of the logarithm. + * + */ + P.logarithm = P.log = function (base) { + var isBase10, d, denominator, k, inf, num, sd, r, + arg = this, + Ctor = arg.constructor, + pr = Ctor.precision, + rm = Ctor.rounding, + guard = 5; + + // Default base is 10. + if (base == null) { + base = new Ctor(10); + isBase10 = true; + } else { + base = new Ctor(base); + d = base.d; + + // Return NaN if base is negative, or non-finite, or is 0 or 1. + if (base.s < 0 || !d || !d[0] || base.eq(1)) return new Ctor(NaN); + + isBase10 = base.eq(10); + } + + d = arg.d; + + // Is arg negative, non-finite, 0 or 1? + if (arg.s < 0 || !d || !d[0] || arg.eq(1)) { + return new Ctor(d && !d[0] ? -1 / 0 : arg.s != 1 ? NaN : d ? 0 : 1 / 0); + } + + // The result will have a non-terminating decimal expansion if base is 10 and arg is not an + // integer power of 10. + if (isBase10) { + if (d.length > 1) { + inf = true; + } else { + for (k = d[0]; k % 10 === 0;) k /= 10; + inf = k !== 1; + } + } + + external = false; + sd = pr + guard; + num = naturalLogarithm(arg, sd); + denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd); + + // The result will have 5 rounding digits. + r = divide(num, denominator, sd, 1); + + // If at a rounding boundary, i.e. the result's rounding digits are [49]9999 or [50]0000, + // calculate 10 further digits. + // + // If the result is known to have an infinite decimal expansion, repeat this until it is clear + // that the result is above or below the boundary. Otherwise, if after calculating the 10 + // further digits, the last 14 are nines, round up and assume the result is exact. + // Also assume the result is exact if the last 14 are zero. + // + // Example of a result that will be incorrectly rounded: + // log[1048576](4503599627370502) = 2.60000000000000009610279511444746... + // The above result correctly rounded using ROUND_CEIL to 1 decimal place should be 2.7, but it + // will be given as 2.6 as there are 15 zeros immediately after the requested decimal place, so + // the exact result would be assumed to be 2.6, which rounded using ROUND_CEIL to 1 decimal + // place is still 2.6. + if (checkRoundingDigits(r.d, k = pr, rm)) { + + do { + sd += 10; + num = naturalLogarithm(arg, sd); + denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd); + r = divide(num, denominator, sd, 1); + + if (!inf) { + + // Check for 14 nines from the 2nd rounding digit, as the first may be 4. + if (+digitsToString(r.d).slice(k + 1, k + 15) + 1 == 1e14) { + r = finalise(r, pr + 1, 0); + } + + break; + } + } while (checkRoundingDigits(r.d, k += 10, rm)); + } + + external = true; + + return finalise(r, pr, rm); + }; + + + /* + * Return a new Decimal whose value is the maximum of the arguments and the value of this Decimal. + * + * arguments {number|string|bigint|Decimal} + * + P.max = function () { + Array.prototype.push.call(arguments, this); + return maxOrMin(this.constructor, arguments, -1); + }; + */ + + + /* + * Return a new Decimal whose value is the minimum of the arguments and the value of this Decimal. + * + * arguments {number|string|bigint|Decimal} + * + P.min = function () { + Array.prototype.push.call(arguments, this); + return maxOrMin(this.constructor, arguments, 1); + }; + */ + + + /* + * n - 0 = n + * n - N = N + * n - I = -I + * 0 - n = -n + * 0 - 0 = 0 + * 0 - N = N + * 0 - I = -I + * N - n = N + * N - 0 = N + * N - N = N + * N - I = N + * I - n = I + * I - 0 = I + * I - N = N + * I - I = N + * + * Return a new Decimal whose value is the value of this Decimal minus `y`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + */ + P.minus = P.sub = function (y) { + var d, e, i, j, k, len, pr, rm, xd, xe, xLTy, yd, + x = this, + Ctor = x.constructor; + + y = new Ctor(y); + + // If either is not finite... + if (!x.d || !y.d) { + + // Return NaN if either is NaN. + if (!x.s || !y.s) y = new Ctor(NaN); + + // Return y negated if x is finite and y is ±Infinity. + else if (x.d) y.s = -y.s; + + // Return x if y is finite and x is ±Infinity. + // Return x if both are ±Infinity with different signs. + // Return NaN if both are ±Infinity with the same sign. + else y = new Ctor(y.d || x.s !== y.s ? x : NaN); + + return y; + } + + // If signs differ... + if (x.s != y.s) { + y.s = -y.s; + return x.plus(y); + } + + xd = x.d; + yd = y.d; + pr = Ctor.precision; + rm = Ctor.rounding; + + // If either is zero... + if (!xd[0] || !yd[0]) { + + // Return y negated if x is zero and y is non-zero. + if (yd[0]) y.s = -y.s; + + // Return x if y is zero and x is non-zero. + else if (xd[0]) y = new Ctor(x); + + // Return zero if both are zero. + // From IEEE 754 (2008) 6.3: 0 - 0 = -0 - -0 = -0 when rounding to -Infinity. + else return new Ctor(rm === 3 ? -0 : 0); + + return external ? finalise(y, pr, rm) : y; + } + + // x and y are finite, non-zero numbers with the same sign. + + // Calculate base 1e7 exponents. + e = mathfloor(y.e / LOG_BASE); + xe = mathfloor(x.e / LOG_BASE); + + xd = xd.slice(); + k = xe - e; + + // If base 1e7 exponents differ... + if (k) { + xLTy = k < 0; + + if (xLTy) { + d = xd; + k = -k; + len = yd.length; + } else { + d = yd; + e = xe; + len = xd.length; + } + + // Numbers with massively different exponents would result in a very high number of + // zeros needing to be prepended, but this can be avoided while still ensuring correct + // rounding by limiting the number of zeros to `Math.ceil(pr / LOG_BASE) + 2`. + i = Math.max(Math.ceil(pr / LOG_BASE), len) + 2; + + if (k > i) { + k = i; + d.length = 1; + } + + // Prepend zeros to equalise exponents. + d.reverse(); + for (i = k; i--;) d.push(0); + d.reverse(); + + // Base 1e7 exponents equal. + } else { + + // Check digits to determine which is the bigger number. + + i = xd.length; + len = yd.length; + xLTy = i < len; + if (xLTy) len = i; + + for (i = 0; i < len; i++) { + if (xd[i] != yd[i]) { + xLTy = xd[i] < yd[i]; + break; + } + } + + k = 0; + } + + if (xLTy) { + d = xd; + xd = yd; + yd = d; + y.s = -y.s; + } + + len = xd.length; + + // Append zeros to `xd` if shorter. + // Don't add zeros to `yd` if shorter as subtraction only needs to start at `yd` length. + for (i = yd.length - len; i > 0; --i) xd[len++] = 0; + + // Subtract yd from xd. + for (i = yd.length; i > k;) { + + if (xd[--i] < yd[i]) { + for (j = i; j && xd[--j] === 0;) xd[j] = BASE - 1; + --xd[j]; + xd[i] += BASE; + } + + xd[i] -= yd[i]; + } + + // Remove trailing zeros. + for (; xd[--len] === 0;) xd.pop(); + + // Remove leading zeros and adjust exponent accordingly. + for (; xd[0] === 0; xd.shift()) --e; + + // Zero? + if (!xd[0]) return new Ctor(rm === 3 ? -0 : 0); + + y.d = xd; + y.e = getBase10Exponent(xd, e); + + return external ? finalise(y, pr, rm) : y; + }; + + + /* + * n % 0 = N + * n % N = N + * n % I = n + * 0 % n = 0 + * -0 % n = -0 + * 0 % 0 = N + * 0 % N = N + * 0 % I = 0 + * N % n = N + * N % 0 = N + * N % N = N + * N % I = N + * I % n = N + * I % 0 = N + * I % N = N + * I % I = N + * + * Return a new Decimal whose value is the value of this Decimal modulo `y`, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + * The result depends on the modulo mode. + * + */ + P.modulo = P.mod = function (y) { + var q, + x = this, + Ctor = x.constructor; + + y = new Ctor(y); + + // Return NaN if x is ±Infinity or NaN, or y is NaN or ±0. + if (!x.d || !y.s || y.d && !y.d[0]) return new Ctor(NaN); + + // Return x if y is ±Infinity or x is ±0. + if (!y.d || x.d && !x.d[0]) { + return finalise(new Ctor(x), Ctor.precision, Ctor.rounding); + } + + // Prevent rounding of intermediate calculations. + external = false; + + if (Ctor.modulo == 9) { + + // Euclidian division: q = sign(y) * floor(x / abs(y)) + // result = x - q * y where 0 <= result < abs(y) + q = divide(x, y.abs(), 0, 3, 1); + q.s *= y.s; + } else { + q = divide(x, y, 0, Ctor.modulo, 1); + } + + q = q.times(y); + + external = true; + + return x.minus(q); + }; + + + /* + * Return a new Decimal whose value is the natural exponential of the value of this Decimal, + * i.e. the base e raised to the power the value of this Decimal, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + */ + P.naturalExponential = P.exp = function () { + return naturalExponential(this); + }; + + + /* + * Return a new Decimal whose value is the natural logarithm of the value of this Decimal, + * rounded to `precision` significant digits using rounding mode `rounding`. + * + */ + P.naturalLogarithm = P.ln = function () { + return naturalLogarithm(this); + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal negated, i.e. as if multiplied by + * -1. + * + */ + P.negated = P.neg = function () { + var x = new this.constructor(this); + x.s = -x.s; + return finalise(x); + }; + + + /* + * n + 0 = n + * n + N = N + * n + I = I + * 0 + n = n + * 0 + 0 = 0 + * 0 + N = N + * 0 + I = I + * N + n = N + * N + 0 = N + * N + N = N + * N + I = N + * I + n = I + * I + 0 = I + * I + N = N + * I + I = I + * + * Return a new Decimal whose value is the value of this Decimal plus `y`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + */ + P.plus = P.add = function (y) { + var carry, d, e, i, k, len, pr, rm, xd, yd, + x = this, + Ctor = x.constructor; + + y = new Ctor(y); + + // If either is not finite... + if (!x.d || !y.d) { + + // Return NaN if either is NaN. + if (!x.s || !y.s) y = new Ctor(NaN); + + // Return x if y is finite and x is ±Infinity. + // Return x if both are ±Infinity with the same sign. + // Return NaN if both are ±Infinity with different signs. + // Return y if x is finite and y is ±Infinity. + else if (!x.d) y = new Ctor(y.d || x.s === y.s ? x : NaN); + + return y; + } + + // If signs differ... + if (x.s != y.s) { + y.s = -y.s; + return x.minus(y); + } + + xd = x.d; + yd = y.d; + pr = Ctor.precision; + rm = Ctor.rounding; + + // If either is zero... + if (!xd[0] || !yd[0]) { + + // Return x if y is zero. + // Return y if y is non-zero. + if (!yd[0]) y = new Ctor(x); + + return external ? finalise(y, pr, rm) : y; + } + + // x and y are finite, non-zero numbers with the same sign. + + // Calculate base 1e7 exponents. + k = mathfloor(x.e / LOG_BASE); + e = mathfloor(y.e / LOG_BASE); + + xd = xd.slice(); + i = k - e; + + // If base 1e7 exponents differ... + if (i) { + + if (i < 0) { + d = xd; + i = -i; + len = yd.length; + } else { + d = yd; + e = k; + len = xd.length; + } + + // Limit number of zeros prepended to max(ceil(pr / LOG_BASE), len) + 1. + k = Math.ceil(pr / LOG_BASE); + len = k > len ? k + 1 : len + 1; + + if (i > len) { + i = len; + d.length = 1; + } + + // Prepend zeros to equalise exponents. Note: Faster to use reverse then do unshifts. + d.reverse(); + for (; i--;) d.push(0); + d.reverse(); + } + + len = xd.length; + i = yd.length; + + // If yd is longer than xd, swap xd and yd so xd points to the longer array. + if (len - i < 0) { + i = len; + d = yd; + yd = xd; + xd = d; + } + + // Only start adding at yd.length - 1 as the further digits of xd can be left as they are. + for (carry = 0; i;) { + carry = (xd[--i] = xd[i] + yd[i] + carry) / BASE | 0; + xd[i] %= BASE; + } + + if (carry) { + xd.unshift(carry); + ++e; + } + + // Remove trailing zeros. + // No need to check for zero, as +x + +y != 0 && -x + -y != 0 + for (len = xd.length; xd[--len] == 0;) xd.pop(); + + y.d = xd; + y.e = getBase10Exponent(xd, e); + + return external ? finalise(y, pr, rm) : y; + }; + + + /* + * Return the number of significant digits of the value of this Decimal. + * + * [z] {boolean|number} Whether to count integer-part trailing zeros: true, false, 1 or 0. + * + */ + P.precision = P.sd = function (z) { + var k, + x = this; + + if (z !== void 0 && z !== !!z && z !== 1 && z !== 0) throw Error(invalidArgument + z); + + if (x.d) { + k = getPrecision(x.d); + if (z && x.e + 1 > k) k = x.e + 1; + } else { + k = NaN; + } + + return k; + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal rounded to a whole number using + * rounding mode `rounding`. + * + */ + P.round = function () { + var x = this, + Ctor = x.constructor; + + return finalise(new Ctor(x), x.e + 1, Ctor.rounding); + }; + + + /* + * Return a new Decimal whose value is the sine of the value in radians of this Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-1, 1] + * + * sin(x) = x - x^3/3! + x^5/5! - ... + * + * sin(0) = 0 + * sin(-0) = -0 + * sin(Infinity) = NaN + * sin(-Infinity) = NaN + * sin(NaN) = NaN + * + */ + P.sine = P.sin = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (!x.isFinite()) return new Ctor(NaN); + if (x.isZero()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE; + Ctor.rounding = 1; + + x = sine(Ctor, toLessThanHalfPi(Ctor, x)); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return finalise(quadrant > 2 ? x.neg() : x, pr, rm, true); + }; + + + /* + * Return a new Decimal whose value is the square root of this Decimal, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * sqrt(-n) = N + * sqrt(N) = N + * sqrt(-I) = N + * sqrt(I) = I + * sqrt(0) = 0 + * sqrt(-0) = -0 + * + */ + P.squareRoot = P.sqrt = function () { + var m, n, sd, r, rep, t, + x = this, + d = x.d, + e = x.e, + s = x.s, + Ctor = x.constructor; + + // Negative/NaN/Infinity/zero? + if (s !== 1 || !d || !d[0]) { + return new Ctor(!s || s < 0 && (!d || d[0]) ? NaN : d ? x : 1 / 0); + } + + external = false; + + // Initial estimate. + s = Math.sqrt(+x); + + // Math.sqrt underflow/overflow? + // Pass x to Math.sqrt as integer, then adjust the exponent of the result. + if (s == 0 || s == 1 / 0) { + n = digitsToString(d); + + if ((n.length + e) % 2 == 0) n += '0'; + s = Math.sqrt(n); + e = mathfloor((e + 1) / 2) - (e < 0 || e % 2); + + if (s == 1 / 0) { + n = '5e' + e; + } else { + n = s.toExponential(); + n = n.slice(0, n.indexOf('e') + 1) + e; + } + + r = new Ctor(n); + } else { + r = new Ctor(s.toString()); + } + + sd = (e = Ctor.precision) + 3; + + // Newton-Raphson iteration. + for (;;) { + t = r; + r = t.plus(divide(x, t, sd + 2, 1)).times(0.5); + + // TODO? Replace with for-loop and checkRoundingDigits. + if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) { + n = n.slice(sd - 3, sd + 1); + + // The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or + // 4999, i.e. approaching a rounding boundary, continue the iteration. + if (n == '9999' || !rep && n == '4999') { + + // On the first iteration only, check to see if rounding up gives the exact result as the + // nines may infinitely repeat. + if (!rep) { + finalise(t, e + 1, 0); + + if (t.times(t).eq(x)) { + r = t; + break; + } + } + + sd += 4; + rep = 1; + } else { + + // If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result. + // If not, then there are further digits and m will be truthy. + if (!+n || !+n.slice(1) && n.charAt(0) == '5') { + + // Truncate to the first rounding digit. + finalise(r, e + 1, 1); + m = !r.times(r).eq(x); + } + + break; + } + } + } + + external = true; + + return finalise(r, e, Ctor.rounding, m); + }; + + + /* + * Return a new Decimal whose value is the tangent of the value in radians of this Decimal. + * + * Domain: [-Infinity, Infinity] + * Range: [-Infinity, Infinity] + * + * tan(0) = 0 + * tan(-0) = -0 + * tan(Infinity) = NaN + * tan(-Infinity) = NaN + * tan(NaN) = NaN + * + */ + P.tangent = P.tan = function () { + var pr, rm, + x = this, + Ctor = x.constructor; + + if (!x.isFinite()) return new Ctor(NaN); + if (x.isZero()) return new Ctor(x); + + pr = Ctor.precision; + rm = Ctor.rounding; + Ctor.precision = pr + 10; + Ctor.rounding = 1; + + x = x.sin(); + x.s = 1; + x = divide(x, new Ctor(1).minus(x.times(x)).sqrt(), pr + 10, 0); + + Ctor.precision = pr; + Ctor.rounding = rm; + + return finalise(quadrant == 2 || quadrant == 4 ? x.neg() : x, pr, rm, true); + }; + + + /* + * n * 0 = 0 + * n * N = N + * n * I = I + * 0 * n = 0 + * 0 * 0 = 0 + * 0 * N = N + * 0 * I = N + * N * n = N + * N * 0 = N + * N * N = N + * N * I = N + * I * n = I + * I * 0 = N + * I * N = N + * I * I = I + * + * Return a new Decimal whose value is this Decimal times `y`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + */ + P.times = P.mul = function (y) { + var carry, e, i, k, r, rL, t, xdL, ydL, + x = this, + Ctor = x.constructor, + xd = x.d, + yd = (y = new Ctor(y)).d; + + y.s *= x.s; + + // If either is NaN, ±Infinity or ±0... + if (!xd || !xd[0] || !yd || !yd[0]) { + + return new Ctor(!y.s || xd && !xd[0] && !yd || yd && !yd[0] && !xd + + // Return NaN if either is NaN. + // Return NaN if x is ±0 and y is ±Infinity, or y is ±0 and x is ±Infinity. + ? NaN + + // Return ±Infinity if either is ±Infinity. + // Return ±0 if either is ±0. + : !xd || !yd ? y.s / 0 : y.s * 0); + } + + e = mathfloor(x.e / LOG_BASE) + mathfloor(y.e / LOG_BASE); + xdL = xd.length; + ydL = yd.length; + + // Ensure xd points to the longer array. + if (xdL < ydL) { + r = xd; + xd = yd; + yd = r; + rL = xdL; + xdL = ydL; + ydL = rL; + } + + // Initialise the result array with zeros. + r = []; + rL = xdL + ydL; + for (i = rL; i--;) r.push(0); + + // Multiply! + for (i = ydL; --i >= 0;) { + carry = 0; + for (k = xdL + i; k > i;) { + t = r[k] + yd[i] * xd[k - i - 1] + carry; + r[k--] = t % BASE | 0; + carry = t / BASE | 0; + } + + r[k] = (r[k] + carry) % BASE | 0; + } + + // Remove trailing zeros. + for (; !r[--rL];) r.pop(); + + if (carry) ++e; + else r.shift(); + + y.d = r; + y.e = getBase10Exponent(r, e); + + return external ? finalise(y, Ctor.precision, Ctor.rounding) : y; + }; + + + /* + * Return a string representing the value of this Decimal in base 2, round to `sd` significant + * digits using rounding mode `rm`. + * + * If the optional `sd` argument is present then return binary exponential notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toBinary = function (sd, rm) { + return toStringBinary(this, 2, sd, rm); + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `dp` + * decimal places using rounding mode `rm` or `rounding` if `rm` is omitted. + * + * If `dp` is omitted, return a new Decimal whose value is the value of this Decimal. + * + * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toDecimalPlaces = P.toDP = function (dp, rm) { + var x = this, + Ctor = x.constructor; + + x = new Ctor(x); + if (dp === void 0) return x; + + checkInt32(dp, 0, MAX_DIGITS); + + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + + return finalise(x, dp + x.e + 1, rm); + }; + + + /* + * Return a string representing the value of this Decimal in exponential notation rounded to + * `dp` fixed decimal places using rounding mode `rounding`. + * + * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toExponential = function (dp, rm) { + var str, + x = this, + Ctor = x.constructor; + + if (dp === void 0) { + str = finiteToString(x, true); + } else { + checkInt32(dp, 0, MAX_DIGITS); + + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + + x = finalise(new Ctor(x), dp + 1, rm); + str = finiteToString(x, true, dp + 1); + } + + return x.isNeg() && !x.isZero() ? '-' + str : str; + }; + + + /* + * Return a string representing the value of this Decimal in normal (fixed-point) notation to + * `dp` fixed decimal places and rounded using rounding mode `rm` or `rounding` if `rm` is + * omitted. + * + * As with JavaScript numbers, (-0).toFixed(0) is '0', but e.g. (-0.00001).toFixed(0) is '-0'. + * + * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'. + * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'. + * (-0).toFixed(3) is '0.000'. + * (-0.5).toFixed(0) is '-0'. + * + */ + P.toFixed = function (dp, rm) { + var str, y, + x = this, + Ctor = x.constructor; + + if (dp === void 0) { + str = finiteToString(x); + } else { + checkInt32(dp, 0, MAX_DIGITS); + + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + + y = finalise(new Ctor(x), dp + x.e + 1, rm); + str = finiteToString(y, false, dp + y.e + 1); + } + + // To determine whether to add the minus sign look at the value before it was rounded, + // i.e. look at `x` rather than `y`. + return x.isNeg() && !x.isZero() ? '-' + str : str; + }; + + + /* + * Return an array representing the value of this Decimal as a simple fraction with an integer + * numerator and an integer denominator. + * + * The denominator will be a positive non-zero value less than or equal to the specified maximum + * denominator. If a maximum denominator is not specified, the denominator will be the lowest + * value necessary to represent the number exactly. + * + * [maxD] {number|string|bigint|Decimal} Maximum denominator. Integer >= 1 and < Infinity. + * + */ + P.toFraction = function (maxD) { + var d, d0, d1, d2, e, k, n, n0, n1, pr, q, r, + x = this, + xd = x.d, + Ctor = x.constructor; + + if (!xd) return new Ctor(x); + + n1 = d0 = new Ctor(1); + d1 = n0 = new Ctor(0); + + d = new Ctor(d1); + e = d.e = getPrecision(xd) - x.e - 1; + k = e % LOG_BASE; + d.d[0] = mathpow(10, k < 0 ? LOG_BASE + k : k); + + if (maxD == null) { + + // d is 10**e, the minimum max-denominator needed. + maxD = e > 0 ? d : n1; + } else { + n = new Ctor(maxD); + if (!n.isInt() || n.lt(n1)) throw Error(invalidArgument + n); + maxD = n.gt(d) ? (e > 0 ? d : n1) : n; + } + + external = false; + n = new Ctor(digitsToString(xd)); + pr = Ctor.precision; + Ctor.precision = e = xd.length * LOG_BASE * 2; + + for (;;) { + q = divide(n, d, 0, 1, 1); + d2 = d0.plus(q.times(d1)); + if (d2.cmp(maxD) == 1) break; + d0 = d1; + d1 = d2; + d2 = n1; + n1 = n0.plus(q.times(d2)); + n0 = d2; + d2 = d; + d = n.minus(q.times(d2)); + n = d2; + } + + d2 = divide(maxD.minus(d0), d1, 0, 1, 1); + n0 = n0.plus(d2.times(n1)); + d0 = d0.plus(d2.times(d1)); + n0.s = n1.s = x.s; + + // Determine which fraction is closer to x, n0/d0 or n1/d1? + r = divide(n1, d1, e, 1).minus(x).abs().cmp(divide(n0, d0, e, 1).minus(x).abs()) < 1 + ? [n1, d1] : [n0, d0]; + + Ctor.precision = pr; + external = true; + + return r; + }; + + + /* + * Return a string representing the value of this Decimal in base 16, round to `sd` significant + * digits using rounding mode `rm`. + * + * If the optional `sd` argument is present then return binary exponential notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toHexadecimal = P.toHex = function (sd, rm) { + return toStringBinary(this, 16, sd, rm); + }; + + + /* + * Returns a new Decimal whose value is the nearest multiple of `y` in the direction of rounding + * mode `rm`, or `Decimal.rounding` if `rm` is omitted, to the value of this Decimal. + * + * The return value will always have the same sign as this Decimal, unless either this Decimal + * or `y` is NaN, in which case the return value will be also be NaN. + * + * The return value is not affected by the value of `precision`. + * + * y {number|string|bigint|Decimal} The magnitude to round to a multiple of. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * 'toNearest() rounding mode not an integer: {rm}' + * 'toNearest() rounding mode out of range: {rm}' + * + */ + P.toNearest = function (y, rm) { + var x = this, + Ctor = x.constructor; + + x = new Ctor(x); + + if (y == null) { + + // If x is not finite, return x. + if (!x.d) return x; + + y = new Ctor(1); + rm = Ctor.rounding; + } else { + y = new Ctor(y); + if (rm === void 0) { + rm = Ctor.rounding; + } else { + checkInt32(rm, 0, 8); + } + + // If x is not finite, return x if y is not NaN, else NaN. + if (!x.d) return y.s ? x : y; + + // If y is not finite, return Infinity with the sign of x if y is Infinity, else NaN. + if (!y.d) { + if (y.s) y.s = x.s; + return y; + } + } + + // If y is not zero, calculate the nearest multiple of y to x. + if (y.d[0]) { + external = false; + x = divide(x, y, 0, rm, 1).times(y); + external = true; + finalise(x); + + // If y is zero, return zero with the sign of x. + } else { + y.s = x.s; + x = y; + } + + return x; + }; + + + /* + * Return the value of this Decimal converted to a number primitive. + * Zero keeps its sign. + * + */ + P.toNumber = function () { + return +this; + }; + + + /* + * Return a string representing the value of this Decimal in base 8, round to `sd` significant + * digits using rounding mode `rm`. + * + * If the optional `sd` argument is present then return binary exponential notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toOctal = function (sd, rm) { + return toStringBinary(this, 8, sd, rm); + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal raised to the power `y`, rounded + * to `precision` significant digits using rounding mode `rounding`. + * + * ECMAScript compliant. + * + * pow(x, NaN) = NaN + * pow(x, ±0) = 1 + + * pow(NaN, non-zero) = NaN + * pow(abs(x) > 1, +Infinity) = +Infinity + * pow(abs(x) > 1, -Infinity) = +0 + * pow(abs(x) == 1, ±Infinity) = NaN + * pow(abs(x) < 1, +Infinity) = +0 + * pow(abs(x) < 1, -Infinity) = +Infinity + * pow(+Infinity, y > 0) = +Infinity + * pow(+Infinity, y < 0) = +0 + * pow(-Infinity, odd integer > 0) = -Infinity + * pow(-Infinity, even integer > 0) = +Infinity + * pow(-Infinity, odd integer < 0) = -0 + * pow(-Infinity, even integer < 0) = +0 + * pow(+0, y > 0) = +0 + * pow(+0, y < 0) = +Infinity + * pow(-0, odd integer > 0) = -0 + * pow(-0, even integer > 0) = +0 + * pow(-0, odd integer < 0) = -Infinity + * pow(-0, even integer < 0) = +Infinity + * pow(finite x < 0, finite non-integer) = NaN + * + * For non-integer or very large exponents pow(x, y) is calculated using + * + * x^y = exp(y*ln(x)) + * + * Assuming the first 15 rounding digits are each equally likely to be any digit 0-9, the + * probability of an incorrectly rounded result + * P([49]9{14} | [50]0{14}) = 2 * 0.2 * 10^-14 = 4e-15 = 1/2.5e+14 + * i.e. 1 in 250,000,000,000,000 + * + * If a result is incorrectly rounded the maximum error will be 1 ulp (unit in last place). + * + * y {number|string|bigint|Decimal} The power to which to raise this Decimal. + * + */ + P.toPower = P.pow = function (y) { + var e, k, pr, r, rm, s, + x = this, + Ctor = x.constructor, + yn = +(y = new Ctor(y)); + + // Either ±Infinity, NaN or ±0? + if (!x.d || !y.d || !x.d[0] || !y.d[0]) return new Ctor(mathpow(+x, yn)); + + x = new Ctor(x); + + if (x.eq(1)) return x; + + pr = Ctor.precision; + rm = Ctor.rounding; + + if (y.eq(1)) return finalise(x, pr, rm); + + // y exponent + e = mathfloor(y.e / LOG_BASE); + + // If y is a small integer use the 'exponentiation by squaring' algorithm. + if (e >= y.d.length - 1 && (k = yn < 0 ? -yn : yn) <= MAX_SAFE_INTEGER) { + r = intPow(Ctor, x, k, pr); + return y.s < 0 ? new Ctor(1).div(r) : finalise(r, pr, rm); + } + + s = x.s; + + // if x is negative + if (s < 0) { + + // if y is not an integer + if (e < y.d.length - 1) return new Ctor(NaN); + + // Result is positive if x is negative and the last digit of integer y is even. + if ((y.d[e] & 1) == 0) s = 1; + + // if x.eq(-1) + if (x.e == 0 && x.d[0] == 1 && x.d.length == 1) { + x.s = s; + return x; + } + } + + // Estimate result exponent. + // x^y = 10^e, where e = y * log10(x) + // log10(x) = log10(x_significand) + x_exponent + // log10(x_significand) = ln(x_significand) / ln(10) + k = mathpow(+x, yn); + e = k == 0 || !isFinite(k) + ? mathfloor(yn * (Math.log('0.' + digitsToString(x.d)) / Math.LN10 + x.e + 1)) + : new Ctor(k + '').e; + + // Exponent estimate may be incorrect e.g. x: 0.999999999999999999, y: 2.29, e: 0, r.e: -1. + + // Overflow/underflow? + if (e > Ctor.maxE + 1 || e < Ctor.minE - 1) return new Ctor(e > 0 ? s / 0 : 0); + + external = false; + Ctor.rounding = x.s = 1; + + // Estimate the extra guard digits needed to ensure five correct rounding digits from + // naturalLogarithm(x). Example of failure without these extra digits (precision: 10): + // new Decimal(2.32456).pow('2087987436534566.46411') + // should be 1.162377823e+764914905173815, but is 1.162355823e+764914905173815 + k = Math.min(12, (e + '').length); + + // r = x^y = exp(y*ln(x)) + r = naturalExponential(y.times(naturalLogarithm(x, pr + k)), pr); + + // r may be Infinity, e.g. (0.9999999999999999).pow(-1e+40) + if (r.d) { + + // Truncate to the required precision plus five rounding digits. + r = finalise(r, pr + 5, 1); + + // If the rounding digits are [49]9999 or [50]0000 increase the precision by 10 and recalculate + // the result. + if (checkRoundingDigits(r.d, pr, rm)) { + e = pr + 10; + + // Truncate to the increased precision plus five rounding digits. + r = finalise(naturalExponential(y.times(naturalLogarithm(x, e + k)), e), e + 5, 1); + + // Check for 14 nines from the 2nd rounding digit (the first rounding digit may be 4 or 9). + if (+digitsToString(r.d).slice(pr + 1, pr + 15) + 1 == 1e14) { + r = finalise(r, pr + 1, 0); + } + } + } + + r.s = s; + external = true; + Ctor.rounding = rm; + + return finalise(r, pr, rm); + }; + + + /* + * Return a string representing the value of this Decimal rounded to `sd` significant digits + * using rounding mode `rounding`. + * + * Return exponential notation if `sd` is less than the number of digits necessary to represent + * the integer part of the value in normal notation. + * + * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + */ + P.toPrecision = function (sd, rm) { + var str, + x = this, + Ctor = x.constructor; + + if (sd === void 0) { + str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos); + } else { + checkInt32(sd, 1, MAX_DIGITS); + + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + + x = finalise(new Ctor(x), sd, rm); + str = finiteToString(x, sd <= x.e || x.e <= Ctor.toExpNeg, sd); + } + + return x.isNeg() && !x.isZero() ? '-' + str : str; + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `sd` + * significant digits using rounding mode `rm`, or to `precision` and `rounding` respectively if + * omitted. + * + * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive. + * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive. + * + * 'toSD() digits out of range: {sd}' + * 'toSD() digits not an integer: {sd}' + * 'toSD() rounding mode not an integer: {rm}' + * 'toSD() rounding mode out of range: {rm}' + * + */ + P.toSignificantDigits = P.toSD = function (sd, rm) { + var x = this, + Ctor = x.constructor; + + if (sd === void 0) { + sd = Ctor.precision; + rm = Ctor.rounding; + } else { + checkInt32(sd, 1, MAX_DIGITS); + + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + } + + return finalise(new Ctor(x), sd, rm); + }; + + + /* + * Return a string representing the value of this Decimal. + * + * Return exponential notation if this Decimal has a positive exponent equal to or greater than + * `toExpPos`, or a negative exponent equal to or less than `toExpNeg`. + * + */ + P.toString = function () { + var x = this, + Ctor = x.constructor, + str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos); + + return x.isNeg() && !x.isZero() ? '-' + str : str; + }; + + + /* + * Return a new Decimal whose value is the value of this Decimal truncated to a whole number. + * + */ + P.truncated = P.trunc = function () { + return finalise(new this.constructor(this), this.e + 1, 1); + }; + + + /* + * Return a string representing the value of this Decimal. + * Unlike `toString`, negative zero will include the minus sign. + * + */ + P.valueOf = P.toJSON = function () { + var x = this, + Ctor = x.constructor, + str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos); + + return x.isNeg() ? '-' + str : str; + }; + + + // Helper functions for Decimal.prototype (P) and/or Decimal methods, and their callers. + + + /* + * digitsToString P.cubeRoot, P.logarithm, P.squareRoot, P.toFraction, P.toPower, + * finiteToString, naturalExponential, naturalLogarithm + * checkInt32 P.toDecimalPlaces, P.toExponential, P.toFixed, P.toNearest, + * P.toPrecision, P.toSignificantDigits, toStringBinary, random + * checkRoundingDigits P.logarithm, P.toPower, naturalExponential, naturalLogarithm + * convertBase toStringBinary, parseOther + * cos P.cos + * divide P.atanh, P.cubeRoot, P.dividedBy, P.dividedToIntegerBy, + * P.logarithm, P.modulo, P.squareRoot, P.tan, P.tanh, P.toFraction, + * P.toNearest, toStringBinary, naturalExponential, naturalLogarithm, + * taylorSeries, atan2, parseOther + * finalise P.absoluteValue, P.atan, P.atanh, P.ceil, P.cos, P.cosh, + * P.cubeRoot, P.dividedToIntegerBy, P.floor, P.logarithm, P.minus, + * P.modulo, P.negated, P.plus, P.round, P.sin, P.sinh, P.squareRoot, + * P.tan, P.times, P.toDecimalPlaces, P.toExponential, P.toFixed, + * P.toNearest, P.toPower, P.toPrecision, P.toSignificantDigits, + * P.truncated, divide, getLn10, getPi, naturalExponential, + * naturalLogarithm, ceil, floor, round, trunc + * finiteToString P.toExponential, P.toFixed, P.toPrecision, P.toString, P.valueOf, + * toStringBinary + * getBase10Exponent P.minus, P.plus, P.times, parseOther + * getLn10 P.logarithm, naturalLogarithm + * getPi P.acos, P.asin, P.atan, toLessThanHalfPi, atan2 + * getPrecision P.precision, P.toFraction + * getZeroString digitsToString, finiteToString + * intPow P.toPower, parseOther + * isOdd toLessThanHalfPi + * maxOrMin max, min + * naturalExponential P.naturalExponential, P.toPower + * naturalLogarithm P.acosh, P.asinh, P.atanh, P.logarithm, P.naturalLogarithm, + * P.toPower, naturalExponential + * nonFiniteToString finiteToString, toStringBinary + * parseDecimal Decimal + * parseOther Decimal + * sin P.sin + * taylorSeries P.cosh, P.sinh, cos, sin + * toLessThanHalfPi P.cos, P.sin + * toStringBinary P.toBinary, P.toHexadecimal, P.toOctal + * truncate intPow + * + * Throws: P.logarithm, P.precision, P.toFraction, checkInt32, getLn10, getPi, + * naturalLogarithm, config, parseOther, random, Decimal + */ + + + function digitsToString(d) { + var i, k, ws, + indexOfLastWord = d.length - 1, + str = '', + w = d[0]; + + if (indexOfLastWord > 0) { + str += w; + for (i = 1; i < indexOfLastWord; i++) { + ws = d[i] + ''; + k = LOG_BASE - ws.length; + if (k) str += getZeroString(k); + str += ws; + } + + w = d[i]; + ws = w + ''; + k = LOG_BASE - ws.length; + if (k) str += getZeroString(k); + } else if (w === 0) { + return '0'; + } + + // Remove trailing zeros of last w. + for (; w % 10 === 0;) w /= 10; + + return str + w; + } + + + function checkInt32(i, min, max) { + if (i !== ~~i || i < min || i > max) { + throw Error(invalidArgument + i); + } + } + + + /* + * Check 5 rounding digits if `repeating` is null, 4 otherwise. + * `repeating == null` if caller is `log` or `pow`, + * `repeating != null` if caller is `naturalLogarithm` or `naturalExponential`. + */ + function checkRoundingDigits(d, i, rm, repeating) { + var di, k, r, rd; + + // Get the length of the first word of the array d. + for (k = d[0]; k >= 10; k /= 10) --i; + + // Is the rounding digit in the first word of d? + if (--i < 0) { + i += LOG_BASE; + di = 0; + } else { + di = Math.ceil((i + 1) / LOG_BASE); + i %= LOG_BASE; + } + + // i is the index (0 - 6) of the rounding digit. + // E.g. if within the word 3487563 the first rounding digit is 5, + // then i = 4, k = 1000, rd = 3487563 % 1000 = 563 + k = mathpow(10, LOG_BASE - i); + rd = d[di] % k | 0; + + if (repeating == null) { + if (i < 3) { + if (i == 0) rd = rd / 100 | 0; + else if (i == 1) rd = rd / 10 | 0; + r = rm < 4 && rd == 99999 || rm > 3 && rd == 49999 || rd == 50000 || rd == 0; + } else { + r = (rm < 4 && rd + 1 == k || rm > 3 && rd + 1 == k / 2) && + (d[di + 1] / k / 100 | 0) == mathpow(10, i - 2) - 1 || + (rd == k / 2 || rd == 0) && (d[di + 1] / k / 100 | 0) == 0; + } + } else { + if (i < 4) { + if (i == 0) rd = rd / 1000 | 0; + else if (i == 1) rd = rd / 100 | 0; + else if (i == 2) rd = rd / 10 | 0; + r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999; + } else { + r = ((repeating || rm < 4) && rd + 1 == k || + (!repeating && rm > 3) && rd + 1 == k / 2) && + (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1; + } + } + + return r; + } + + + // Convert string of `baseIn` to an array of numbers of `baseOut`. + // Eg. convertBase('255', 10, 16) returns [15, 15]. + // Eg. convertBase('ff', 16, 10) returns [2, 5, 5]. + function convertBase(str, baseIn, baseOut) { + var j, + arr = [0], + arrL, + i = 0, + strL = str.length; + + for (; i < strL;) { + for (arrL = arr.length; arrL--;) arr[arrL] *= baseIn; + arr[0] += NUMERALS.indexOf(str.charAt(i++)); + for (j = 0; j < arr.length; j++) { + if (arr[j] > baseOut - 1) { + if (arr[j + 1] === void 0) arr[j + 1] = 0; + arr[j + 1] += arr[j] / baseOut | 0; + arr[j] %= baseOut; + } + } + } + + return arr.reverse(); + } + + + /* + * cos(x) = 1 - x^2/2! + x^4/4! - ... + * |x| < pi/2 + * + */ + function cosine(Ctor, x) { + var k, len, y; + + if (x.isZero()) return x; + + // Argument reduction: cos(4x) = 8*(cos^4(x) - cos^2(x)) + 1 + // i.e. cos(x) = 8*(cos^4(x/4) - cos^2(x/4)) + 1 + + // Estimate the optimum number of times to use the argument reduction. + len = x.d.length; + if (len < 32) { + k = Math.ceil(len / 3); + y = (1 / tinyPow(4, k)).toString(); + } else { + k = 16; + y = '2.3283064365386962890625e-10'; + } + + Ctor.precision += k; + + x = taylorSeries(Ctor, 1, x.times(y), new Ctor(1)); + + // Reverse argument reduction + for (var i = k; i--;) { + var cos2x = x.times(x); + x = cos2x.times(cos2x).minus(cos2x).times(8).plus(1); + } + + Ctor.precision -= k; + + return x; + } + + + /* + * Perform division in the specified base. + */ + var divide = (function () { + + // Assumes non-zero x and k, and hence non-zero result. + function multiplyInteger(x, k, base) { + var temp, + carry = 0, + i = x.length; + + for (x = x.slice(); i--;) { + temp = x[i] * k + carry; + x[i] = temp % base | 0; + carry = temp / base | 0; + } + + if (carry) x.unshift(carry); + + return x; + } + + function compare(a, b, aL, bL) { + var i, r; + + if (aL != bL) { + r = aL > bL ? 1 : -1; + } else { + for (i = r = 0; i < aL; i++) { + if (a[i] != b[i]) { + r = a[i] > b[i] ? 1 : -1; + break; + } + } + } + + return r; + } + + function subtract(a, b, aL, base) { + var i = 0; + + // Subtract b from a. + for (; aL--;) { + a[aL] -= i; + i = a[aL] < b[aL] ? 1 : 0; + a[aL] = i * base + a[aL] - b[aL]; + } + + // Remove leading zeros. + for (; !a[0] && a.length > 1;) a.shift(); + } + + return function (x, y, pr, rm, dp, base) { + var cmp, e, i, k, logBase, more, prod, prodL, q, qd, rem, remL, rem0, sd, t, xi, xL, yd0, + yL, yz, + Ctor = x.constructor, + sign = x.s == y.s ? 1 : -1, + xd = x.d, + yd = y.d; + + // Either NaN, Infinity or 0? + if (!xd || !xd[0] || !yd || !yd[0]) { + + return new Ctor(// Return NaN if either NaN, or both Infinity or 0. + !x.s || !y.s || (xd ? yd && xd[0] == yd[0] : !yd) ? NaN : + + // Return ±0 if x is 0 or y is ±Infinity, or return ±Infinity as y is 0. + xd && xd[0] == 0 || !yd ? sign * 0 : sign / 0); + } + + if (base) { + logBase = 1; + e = x.e - y.e; + } else { + base = BASE; + logBase = LOG_BASE; + e = mathfloor(x.e / logBase) - mathfloor(y.e / logBase); + } + + yL = yd.length; + xL = xd.length; + q = new Ctor(sign); + qd = q.d = []; + + // Result exponent may be one less than e. + // The digit array of a Decimal from toStringBinary may have trailing zeros. + for (i = 0; yd[i] == (xd[i] || 0); i++); + + if (yd[i] > (xd[i] || 0)) e--; + + if (pr == null) { + sd = pr = Ctor.precision; + rm = Ctor.rounding; + } else if (dp) { + sd = pr + (x.e - y.e) + 1; + } else { + sd = pr; + } + + if (sd < 0) { + qd.push(1); + more = true; + } else { + + // Convert precision in number of base 10 digits to base 1e7 digits. + sd = sd / logBase + 2 | 0; + i = 0; + + // divisor < 1e7 + if (yL == 1) { + k = 0; + yd = yd[0]; + sd++; + + // k is the carry. + for (; (i < xL || k) && sd--; i++) { + t = k * base + (xd[i] || 0); + qd[i] = t / yd | 0; + k = t % yd | 0; + } + + more = k || i < xL; + + // divisor >= 1e7 + } else { + + // Normalise xd and yd so highest order digit of yd is >= base/2 + k = base / (yd[0] + 1) | 0; + + if (k > 1) { + yd = multiplyInteger(yd, k, base); + xd = multiplyInteger(xd, k, base); + yL = yd.length; + xL = xd.length; + } + + xi = yL; + rem = xd.slice(0, yL); + remL = rem.length; + + // Add zeros to make remainder as long as divisor. + for (; remL < yL;) rem[remL++] = 0; + + yz = yd.slice(); + yz.unshift(0); + yd0 = yd[0]; + + if (yd[1] >= base / 2) ++yd0; + + do { + k = 0; + + // Compare divisor and remainder. + cmp = compare(yd, rem, yL, remL); + + // If divisor < remainder. + if (cmp < 0) { + + // Calculate trial digit, k. + rem0 = rem[0]; + if (yL != remL) rem0 = rem0 * base + (rem[1] || 0); + + // k will be how many times the divisor goes into the current remainder. + k = rem0 / yd0 | 0; + + // Algorithm: + // 1. product = divisor * trial digit (k) + // 2. if product > remainder: product -= divisor, k-- + // 3. remainder -= product + // 4. if product was < remainder at 2: + // 5. compare new remainder and divisor + // 6. If remainder > divisor: remainder -= divisor, k++ + + if (k > 1) { + if (k >= base) k = base - 1; + + // product = divisor * trial digit. + prod = multiplyInteger(yd, k, base); + prodL = prod.length; + remL = rem.length; + + // Compare product and remainder. + cmp = compare(prod, rem, prodL, remL); + + // product > remainder. + if (cmp == 1) { + k--; + + // Subtract divisor from product. + subtract(prod, yL < prodL ? yz : yd, prodL, base); + } + } else { + + // cmp is -1. + // If k is 0, there is no need to compare yd and rem again below, so change cmp to 1 + // to avoid it. If k is 1 there is a need to compare yd and rem again below. + if (k == 0) cmp = k = 1; + prod = yd.slice(); + } + + prodL = prod.length; + if (prodL < remL) prod.unshift(0); + + // Subtract product from remainder. + subtract(rem, prod, remL, base); + + // If product was < previous remainder. + if (cmp == -1) { + remL = rem.length; + + // Compare divisor and new remainder. + cmp = compare(yd, rem, yL, remL); + + // If divisor < new remainder, subtract divisor from remainder. + if (cmp < 1) { + k++; + + // Subtract divisor from remainder. + subtract(rem, yL < remL ? yz : yd, remL, base); + } + } + + remL = rem.length; + } else if (cmp === 0) { + k++; + rem = [0]; + } // if cmp === 1, k will be 0 + + // Add the next digit, k, to the result array. + qd[i++] = k; + + // Update the remainder. + if (cmp && rem[0]) { + rem[remL++] = xd[xi] || 0; + } else { + rem = [xd[xi]]; + remL = 1; + } + + } while ((xi++ < xL || rem[0] !== void 0) && sd--); + + more = rem[0] !== void 0; + } + + // Leading zero? + if (!qd[0]) qd.shift(); + } + + // logBase is 1 when divide is being used for base conversion. + if (logBase == 1) { + q.e = e; + inexact = more; + } else { + + // To calculate q.e, first get the number of digits of qd[0]. + for (i = 1, k = qd[0]; k >= 10; k /= 10) i++; + q.e = i + e * logBase - 1; + + finalise(q, dp ? pr + q.e + 1 : pr, rm, more); + } + + return q; + }; + })(); + + + /* + * Round `x` to `sd` significant digits using rounding mode `rm`. + * Check for over/under-flow. + */ + function finalise(x, sd, rm, isTruncated) { + var digits, i, j, k, rd, roundUp, w, xd, xdi, + Ctor = x.constructor; + + // Don't round if sd is null or undefined. + out: if (sd != null) { + xd = x.d; + + // Infinity/NaN. + if (!xd) return x; + + // rd: the rounding digit, i.e. the digit after the digit that may be rounded up. + // w: the word of xd containing rd, a base 1e7 number. + // xdi: the index of w within xd. + // digits: the number of digits of w. + // i: what would be the index of rd within w if all the numbers were 7 digits long (i.e. if + // they had leading zeros) + // j: if > 0, the actual index of rd within w (if < 0, rd is a leading zero). + + // Get the length of the first word of the digits array xd. + for (digits = 1, k = xd[0]; k >= 10; k /= 10) digits++; + i = sd - digits; + + // Is the rounding digit in the first word of xd? + if (i < 0) { + i += LOG_BASE; + j = sd; + w = xd[xdi = 0]; + + // Get the rounding digit at index j of w. + rd = w / mathpow(10, digits - j - 1) % 10 | 0; + } else { + xdi = Math.ceil((i + 1) / LOG_BASE); + k = xd.length; + if (xdi >= k) { + if (isTruncated) { + + // Needed by `naturalExponential`, `naturalLogarithm` and `squareRoot`. + for (; k++ <= xdi;) xd.push(0); + w = rd = 0; + digits = 1; + i %= LOG_BASE; + j = i - LOG_BASE + 1; + } else { + break out; + } + } else { + w = k = xd[xdi]; + + // Get the number of digits of w. + for (digits = 1; k >= 10; k /= 10) digits++; + + // Get the index of rd within w. + i %= LOG_BASE; + + // Get the index of rd within w, adjusted for leading zeros. + // The number of leading zeros of w is given by LOG_BASE - digits. + j = i - LOG_BASE + digits; + + // Get the rounding digit at index j of w. + rd = j < 0 ? 0 : w / mathpow(10, digits - j - 1) % 10 | 0; + } + } + + // Are there any non-zero digits after the rounding digit? + isTruncated = isTruncated || sd < 0 || + xd[xdi + 1] !== void 0 || (j < 0 ? w : w % mathpow(10, digits - j - 1)); + + // The expression `w % mathpow(10, digits - j - 1)` returns all the digits of w to the right + // of the digit at (left-to-right) index j, e.g. if w is 908714 and j is 2, the expression + // will give 714. + + roundUp = rm < 4 + ? (rd || isTruncated) && (rm == 0 || rm == (x.s < 0 ? 3 : 2)) + : rd > 5 || rd == 5 && (rm == 4 || isTruncated || rm == 6 && + + // Check whether the digit to the left of the rounding digit is odd. + ((i > 0 ? j > 0 ? w / mathpow(10, digits - j) : 0 : xd[xdi - 1]) % 10) & 1 || + rm == (x.s < 0 ? 8 : 7)); + + if (sd < 1 || !xd[0]) { + xd.length = 0; + if (roundUp) { + + // Convert sd to decimal places. + sd -= x.e + 1; + + // 1, 0.1, 0.01, 0.001, 0.0001 etc. + xd[0] = mathpow(10, (LOG_BASE - sd % LOG_BASE) % LOG_BASE); + x.e = -sd || 0; + } else { + + // Zero. + xd[0] = x.e = 0; + } + + return x; + } + + // Remove excess digits. + if (i == 0) { + xd.length = xdi; + k = 1; + xdi--; + } else { + xd.length = xdi + 1; + k = mathpow(10, LOG_BASE - i); + + // E.g. 56700 becomes 56000 if 7 is the rounding digit. + // j > 0 means i > number of leading zeros of w. + xd[xdi] = j > 0 ? (w / mathpow(10, digits - j) % mathpow(10, j) | 0) * k : 0; + } + + if (roundUp) { + for (;;) { + + // Is the digit to be rounded up in the first word of xd? + if (xdi == 0) { + + // i will be the length of xd[0] before k is added. + for (i = 1, j = xd[0]; j >= 10; j /= 10) i++; + j = xd[0] += k; + for (k = 1; j >= 10; j /= 10) k++; + + // if i != k the length has increased. + if (i != k) { + x.e++; + if (xd[0] == BASE) xd[0] = 1; + } + + break; + } else { + xd[xdi] += k; + if (xd[xdi] != BASE) break; + xd[xdi--] = 0; + k = 1; + } + } + } + + // Remove trailing zeros. + for (i = xd.length; xd[--i] === 0;) xd.pop(); + } + + if (external) { + + // Overflow? + if (x.e > Ctor.maxE) { + + // Infinity. + x.d = null; + x.e = NaN; + + // Underflow? + } else if (x.e < Ctor.minE) { + + // Zero. + x.e = 0; + x.d = [0]; + // Ctor.underflow = true; + } // else Ctor.underflow = false; + } + + return x; + } + + + function finiteToString(x, isExp, sd) { + if (!x.isFinite()) return nonFiniteToString(x); + var k, + e = x.e, + str = digitsToString(x.d), + len = str.length; + + if (isExp) { + if (sd && (k = sd - len) > 0) { + str = str.charAt(0) + '.' + str.slice(1) + getZeroString(k); + } else if (len > 1) { + str = str.charAt(0) + '.' + str.slice(1); + } + + str = str + (x.e < 0 ? 'e' : 'e+') + x.e; + } else if (e < 0) { + str = '0.' + getZeroString(-e - 1) + str; + if (sd && (k = sd - len) > 0) str += getZeroString(k); + } else if (e >= len) { + str += getZeroString(e + 1 - len); + if (sd && (k = sd - e - 1) > 0) str = str + '.' + getZeroString(k); + } else { + if ((k = e + 1) < len) str = str.slice(0, k) + '.' + str.slice(k); + if (sd && (k = sd - len) > 0) { + if (e + 1 === len) str += '.'; + str += getZeroString(k); + } + } + + return str; + } + + + // Calculate the base 10 exponent from the base 1e7 exponent. + function getBase10Exponent(digits, e) { + var w = digits[0]; + + // Add the number of digits of the first word of the digits array. + for ( e *= LOG_BASE; w >= 10; w /= 10) e++; + return e; + } + + + function getLn10(Ctor, sd, pr) { + if (sd > LN10_PRECISION) { + + // Reset global state in case the exception is caught. + external = true; + if (pr) Ctor.precision = pr; + throw Error(precisionLimitExceeded); + } + return finalise(new Ctor(LN10), sd, 1, true); + } + + + function getPi(Ctor, sd, rm) { + if (sd > PI_PRECISION) throw Error(precisionLimitExceeded); + return finalise(new Ctor(PI), sd, rm, true); + } + + + function getPrecision(digits) { + var w = digits.length - 1, + len = w * LOG_BASE + 1; + + w = digits[w]; + + // If non-zero... + if (w) { + + // Subtract the number of trailing zeros of the last word. + for (; w % 10 == 0; w /= 10) len--; + + // Add the number of digits of the first word. + for (w = digits[0]; w >= 10; w /= 10) len++; + } + + return len; + } + + + function getZeroString(k) { + var zs = ''; + for (; k--;) zs += '0'; + return zs; + } + + + /* + * Return a new Decimal whose value is the value of Decimal `x` to the power `n`, where `n` is an + * integer of type number. + * + * Implements 'exponentiation by squaring'. Called by `pow` and `parseOther`. + * + */ + function intPow(Ctor, x, n, pr) { + var isTruncated, + r = new Ctor(1), + + // Max n of 9007199254740991 takes 53 loop iterations. + // Maximum digits array length; leaves [28, 34] guard digits. + k = Math.ceil(pr / LOG_BASE + 4); + + external = false; + + for (;;) { + if (n % 2) { + r = r.times(x); + if (truncate(r.d, k)) isTruncated = true; + } + + n = mathfloor(n / 2); + if (n === 0) { + + // To ensure correct rounding when r.d is truncated, increment the last word if it is zero. + n = r.d.length - 1; + if (isTruncated && r.d[n] === 0) ++r.d[n]; + break; + } + + x = x.times(x); + truncate(x.d, k); + } + + external = true; + + return r; + } + + + function isOdd(n) { + return n.d[n.d.length - 1] & 1; + } + + + /* + * Handle `max` (`n` is -1) and `min` (`n` is 1). + */ + function maxOrMin(Ctor, args, n) { + var k, y, + x = new Ctor(args[0]), + i = 0; + + for (; ++i < args.length;) { + y = new Ctor(args[i]); + + // NaN? + if (!y.s) { + x = y; + break; + } + + k = x.cmp(y); + + if (k === n || k === 0 && x.s === n) { + x = y; + } + } + + return x; + } + + + /* + * Return a new Decimal whose value is the natural exponential of `x` rounded to `sd` significant + * digits. + * + * Taylor/Maclaurin series. + * + * exp(x) = x^0/0! + x^1/1! + x^2/2! + x^3/3! + ... + * + * Argument reduction: + * Repeat x = x / 32, k += 5, until |x| < 0.1 + * exp(x) = exp(x / 2^k)^(2^k) + * + * Previously, the argument was initially reduced by + * exp(x) = exp(r) * 10^k where r = x - k * ln10, k = floor(x / ln10) + * to first put r in the range [0, ln10], before dividing by 32 until |x| < 0.1, but this was + * found to be slower than just dividing repeatedly by 32 as above. + * + * Max integer argument: exp('20723265836946413') = 6.3e+9000000000000000 + * Min integer argument: exp('-20723265836946411') = 1.2e-9000000000000000 + * (Math object integer min/max: Math.exp(709) = 8.2e+307, Math.exp(-745) = 5e-324) + * + * exp(Infinity) = Infinity + * exp(-Infinity) = 0 + * exp(NaN) = NaN + * exp(±0) = 1 + * + * exp(x) is non-terminating for any finite, non-zero x. + * + * The result will always be correctly rounded. + * + */ + function naturalExponential(x, sd) { + var denominator, guard, j, pow, sum, t, wpr, + rep = 0, + i = 0, + k = 0, + Ctor = x.constructor, + rm = Ctor.rounding, + pr = Ctor.precision; + + // 0/NaN/Infinity? + if (!x.d || !x.d[0] || x.e > 17) { + + return new Ctor(x.d + ? !x.d[0] ? 1 : x.s < 0 ? 0 : 1 / 0 + : x.s ? x.s < 0 ? 0 : x : 0 / 0); + } + + if (sd == null) { + external = false; + wpr = pr; + } else { + wpr = sd; + } + + t = new Ctor(0.03125); + + // while abs(x) >= 0.1 + while (x.e > -2) { + + // x = x / 2^5 + x = x.times(t); + k += 5; + } + + // Use 2 * log10(2^k) + 5 (empirically derived) to estimate the increase in precision + // necessary to ensure the first 4 rounding digits are correct. + guard = Math.log(mathpow(2, k)) / Math.LN10 * 2 + 5 | 0; + wpr += guard; + denominator = pow = sum = new Ctor(1); + Ctor.precision = wpr; + + for (;;) { + pow = finalise(pow.times(x), wpr, 1); + denominator = denominator.times(++i); + t = sum.plus(divide(pow, denominator, wpr, 1)); + + if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) { + j = k; + while (j--) sum = finalise(sum.times(sum), wpr, 1); + + // Check to see if the first 4 rounding digits are [49]999. + // If so, repeat the summation with a higher precision, otherwise + // e.g. with precision: 18, rounding: 1 + // exp(18.404272462595034083567793919843761) = 98372560.1229999999 (should be 98372560.123) + // `wpr - guard` is the index of first rounding digit. + if (sd == null) { + + if (rep < 3 && checkRoundingDigits(sum.d, wpr - guard, rm, rep)) { + Ctor.precision = wpr += 10; + denominator = pow = t = new Ctor(1); + i = 0; + rep++; + } else { + return finalise(sum, Ctor.precision = pr, rm, external = true); + } + } else { + Ctor.precision = pr; + return sum; + } + } + + sum = t; + } + } + + + /* + * Return a new Decimal whose value is the natural logarithm of `x` rounded to `sd` significant + * digits. + * + * ln(-n) = NaN + * ln(0) = -Infinity + * ln(-0) = -Infinity + * ln(1) = 0 + * ln(Infinity) = Infinity + * ln(-Infinity) = NaN + * ln(NaN) = NaN + * + * ln(n) (n != 1) is non-terminating. + * + */ + function naturalLogarithm(y, sd) { + var c, c0, denominator, e, numerator, rep, sum, t, wpr, x1, x2, + n = 1, + guard = 10, + x = y, + xd = x.d, + Ctor = x.constructor, + rm = Ctor.rounding, + pr = Ctor.precision; + + // Is x negative or Infinity, NaN, 0 or 1? + if (x.s < 0 || !xd || !xd[0] || !x.e && xd[0] == 1 && xd.length == 1) { + return new Ctor(xd && !xd[0] ? -1 / 0 : x.s != 1 ? NaN : xd ? 0 : x); + } + + if (sd == null) { + external = false; + wpr = pr; + } else { + wpr = sd; + } + + Ctor.precision = wpr += guard; + c = digitsToString(xd); + c0 = c.charAt(0); + + if (Math.abs(e = x.e) < 1.5e15) { + + // Argument reduction. + // The series converges faster the closer the argument is to 1, so using + // ln(a^b) = b * ln(a), ln(a) = ln(a^b) / b + // multiply the argument by itself until the leading digits of the significand are 7, 8, 9, + // 10, 11, 12 or 13, recording the number of multiplications so the sum of the series can + // later be divided by this number, then separate out the power of 10 using + // ln(a*10^b) = ln(a) + b*ln(10). + + // max n is 21 (gives 0.9, 1.0 or 1.1) (9e15 / 21 = 4.2e14). + //while (c0 < 9 && c0 != 1 || c0 == 1 && c.charAt(1) > 1) { + // max n is 6 (gives 0.7 - 1.3) + while (c0 < 7 && c0 != 1 || c0 == 1 && c.charAt(1) > 3) { + x = x.times(y); + c = digitsToString(x.d); + c0 = c.charAt(0); + n++; + } + + e = x.e; + + if (c0 > 1) { + x = new Ctor('0.' + c); + e++; + } else { + x = new Ctor(c0 + '.' + c.slice(1)); + } + } else { + + // The argument reduction method above may result in overflow if the argument y is a massive + // number with exponent >= 1500000000000000 (9e15 / 6 = 1.5e15), so instead recall this + // function using ln(x*10^e) = ln(x) + e*ln(10). + t = getLn10(Ctor, wpr + 2, pr).times(e + ''); + x = naturalLogarithm(new Ctor(c0 + '.' + c.slice(1)), wpr - guard).plus(t); + Ctor.precision = pr; + + return sd == null ? finalise(x, pr, rm, external = true) : x; + } + + // x1 is x reduced to a value near 1. + x1 = x; + + // Taylor series. + // ln(y) = ln((1 + x)/(1 - x)) = 2(x + x^3/3 + x^5/5 + x^7/7 + ...) + // where x = (y - 1)/(y + 1) (|x| < 1) + sum = numerator = x = divide(x.minus(1), x.plus(1), wpr, 1); + x2 = finalise(x.times(x), wpr, 1); + denominator = 3; + + for (;;) { + numerator = finalise(numerator.times(x2), wpr, 1); + t = sum.plus(divide(numerator, new Ctor(denominator), wpr, 1)); + + if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) { + sum = sum.times(2); + + // Reverse the argument reduction. Check that e is not 0 because, besides preventing an + // unnecessary calculation, -0 + 0 = +0 and to ensure correct rounding -0 needs to stay -0. + if (e !== 0) sum = sum.plus(getLn10(Ctor, wpr + 2, pr).times(e + '')); + sum = divide(sum, new Ctor(n), wpr, 1); + + // Is rm > 3 and the first 4 rounding digits 4999, or rm < 4 (or the summation has + // been repeated previously) and the first 4 rounding digits 9999? + // If so, restart the summation with a higher precision, otherwise + // e.g. with precision: 12, rounding: 1 + // ln(135520028.6126091714265381533) = 18.7246299999 when it should be 18.72463. + // `wpr - guard` is the index of first rounding digit. + if (sd == null) { + if (checkRoundingDigits(sum.d, wpr - guard, rm, rep)) { + Ctor.precision = wpr += guard; + t = numerator = x = divide(x1.minus(1), x1.plus(1), wpr, 1); + x2 = finalise(x.times(x), wpr, 1); + denominator = rep = 1; + } else { + return finalise(sum, Ctor.precision = pr, rm, external = true); + } + } else { + Ctor.precision = pr; + return sum; + } + } + + sum = t; + denominator += 2; + } + } + + + // ±Infinity, NaN. + function nonFiniteToString(x) { + // Unsigned. + return String(x.s * x.s / 0); + } + + + /* + * Parse the value of a new Decimal `x` from string `str`. + */ + function parseDecimal(x, str) { + var e, i, len; + + // TODO BigInt str: no need to check for decimal point, exponential form or leading zeros. + + // Decimal point? + if ((e = str.indexOf('.')) > -1) str = str.replace('.', ''); + + // Exponential form? + if ((i = str.search(/e/i)) > 0) { + + // Determine exponent. + if (e < 0) e = i; + e += +str.slice(i + 1); + str = str.substring(0, i); + } else if (e < 0) { + + // Integer. + e = str.length; + } + + // Determine leading zeros. + for (i = 0; str.charCodeAt(i) === 48; i++); + + // Determine trailing zeros. + for (len = str.length; str.charCodeAt(len - 1) === 48; --len); + str = str.slice(i, len); + + if (str) { + len -= i; + x.e = e = e - i - 1; + x.d = []; + + // Transform base + + // e is the base 10 exponent. + // i is where to slice str to get the first word of the digits array. + i = (e + 1) % LOG_BASE; + if (e < 0) i += LOG_BASE; + + if (i < len) { + if (i) x.d.push(+str.slice(0, i)); + for (len -= LOG_BASE; i < len;) x.d.push(+str.slice(i, i += LOG_BASE)); + str = str.slice(i); + i = LOG_BASE - str.length; + } else { + i -= len; + } + + for (; i--;) str += '0'; + x.d.push(+str); + + if (external) { + + // Overflow? + if (x.e > x.constructor.maxE) { + + // Infinity. + x.d = null; + x.e = NaN; + + // Underflow? + } else if (x.e < x.constructor.minE) { + + // Zero. + x.e = 0; + x.d = [0]; + // x.constructor.underflow = true; + } // else x.constructor.underflow = false; + } + } else { + + // Zero. + x.e = 0; + x.d = [0]; + } + + return x; + } + + + /* + * Parse the value of a new Decimal `x` from a string `str`, which is not a decimal value. + */ + function parseOther(x, str) { + var base, Ctor, divisor, i, isFloat, len, p, xd, xe; + + if (str.indexOf('_') > -1) { + str = str.replace(/(\d)_(?=\d)/g, '$1'); + if (isDecimal.test(str)) return parseDecimal(x, str); + } else if (str === 'Infinity' || str === 'NaN') { + if (!+str) x.s = NaN; + x.e = NaN; + x.d = null; + return x; + } + + if (isHex.test(str)) { + base = 16; + str = str.toLowerCase(); + } else if (isBinary.test(str)) { + base = 2; + } else if (isOctal.test(str)) { + base = 8; + } else { + throw Error(invalidArgument + str); + } + + // Is there a binary exponent part? + i = str.search(/p/i); + + if (i > 0) { + p = +str.slice(i + 1); + str = str.substring(2, i); + } else { + str = str.slice(2); + } + + // Convert `str` as an integer then divide the result by `base` raised to a power such that the + // fraction part will be restored. + i = str.indexOf('.'); + isFloat = i >= 0; + Ctor = x.constructor; + + if (isFloat) { + str = str.replace('.', ''); + len = str.length; + i = len - i; + + // log[10](16) = 1.2041... , log[10](88) = 1.9444.... + divisor = intPow(Ctor, new Ctor(base), i, i * 2); + } + + xd = convertBase(str, base, BASE); + xe = xd.length - 1; + + // Remove trailing zeros. + for (i = xe; xd[i] === 0; --i) xd.pop(); + if (i < 0) return new Ctor(x.s * 0); + x.e = getBase10Exponent(xd, xe); + x.d = xd; + external = false; + + // At what precision to perform the division to ensure exact conversion? + // maxDecimalIntegerPartDigitCount = ceil(log[10](b) * otherBaseIntegerPartDigitCount) + // log[10](2) = 0.30103, log[10](8) = 0.90309, log[10](16) = 1.20412 + // E.g. ceil(1.2 * 3) = 4, so up to 4 decimal digits are needed to represent 3 hex int digits. + // maxDecimalFractionPartDigitCount = {Hex:4|Oct:3|Bin:1} * otherBaseFractionPartDigitCount + // Therefore using 4 * the number of digits of str will always be enough. + if (isFloat) x = divide(x, divisor, len * 4); + + // Multiply by the binary exponent part if present. + if (p) x = x.times(Math.abs(p) < 54 ? mathpow(2, p) : Decimal.pow(2, p)); + external = true; + + return x; + } + + + /* + * sin(x) = x - x^3/3! + x^5/5! - ... + * |x| < pi/2 + * + */ + function sine(Ctor, x) { + var k, + len = x.d.length; + + if (len < 3) { + return x.isZero() ? x : taylorSeries(Ctor, 2, x, x); + } + + // Argument reduction: sin(5x) = 16*sin^5(x) - 20*sin^3(x) + 5*sin(x) + // i.e. sin(x) = 16*sin^5(x/5) - 20*sin^3(x/5) + 5*sin(x/5) + // and sin(x) = sin(x/5)(5 + sin^2(x/5)(16sin^2(x/5) - 20)) + + // Estimate the optimum number of times to use the argument reduction. + k = 1.4 * Math.sqrt(len); + k = k > 16 ? 16 : k | 0; + + x = x.times(1 / tinyPow(5, k)); + x = taylorSeries(Ctor, 2, x, x); + + // Reverse argument reduction + var sin2_x, + d5 = new Ctor(5), + d16 = new Ctor(16), + d20 = new Ctor(20); + for (; k--;) { + sin2_x = x.times(x); + x = x.times(d5.plus(sin2_x.times(d16.times(sin2_x).minus(d20)))); + } + + return x; + } + + + // Calculate Taylor series for `cos`, `cosh`, `sin` and `sinh`. + function taylorSeries(Ctor, n, x, y, isHyperbolic) { + var j, t, u, x2, + i = 1, + pr = Ctor.precision, + k = Math.ceil(pr / LOG_BASE); + + external = false; + x2 = x.times(x); + u = new Ctor(y); + + for (;;) { + t = divide(u.times(x2), new Ctor(n++ * n++), pr, 1); + u = isHyperbolic ? y.plus(t) : y.minus(t); + y = divide(t.times(x2), new Ctor(n++ * n++), pr, 1); + t = u.plus(y); + + if (t.d[k] !== void 0) { + for (j = k; t.d[j] === u.d[j] && j--;); + if (j == -1) break; + } + + j = u; + u = y; + y = t; + t = j; + i++; + } + + external = true; + t.d.length = k + 1; + + return t; + } + + + // Exponent e must be positive and non-zero. + function tinyPow(b, e) { + var n = b; + while (--e) n *= b; + return n; + } + + + // Return the absolute value of `x` reduced to less than or equal to half pi. + function toLessThanHalfPi(Ctor, x) { + var t, + isNeg = x.s < 0, + pi = getPi(Ctor, Ctor.precision, 1), + halfPi = pi.times(0.5); + + x = x.abs(); + + if (x.lte(halfPi)) { + quadrant = isNeg ? 4 : 1; + return x; + } + + t = x.divToInt(pi); + + if (t.isZero()) { + quadrant = isNeg ? 3 : 2; + } else { + x = x.minus(t.times(pi)); + + // 0 <= x < pi + if (x.lte(halfPi)) { + quadrant = isOdd(t) ? (isNeg ? 2 : 3) : (isNeg ? 4 : 1); + return x; + } + + quadrant = isOdd(t) ? (isNeg ? 1 : 4) : (isNeg ? 3 : 2); + } + + return x.minus(pi).abs(); + } + + + /* + * Return the value of Decimal `x` as a string in base `baseOut`. + * + * If the optional `sd` argument is present include a binary exponent suffix. + */ + function toStringBinary(x, baseOut, sd, rm) { + var base, e, i, k, len, roundUp, str, xd, y, + Ctor = x.constructor, + isExp = sd !== void 0; + + if (isExp) { + checkInt32(sd, 1, MAX_DIGITS); + if (rm === void 0) rm = Ctor.rounding; + else checkInt32(rm, 0, 8); + } else { + sd = Ctor.precision; + rm = Ctor.rounding; + } + + if (!x.isFinite()) { + str = nonFiniteToString(x); + } else { + str = finiteToString(x); + i = str.indexOf('.'); + + // Use exponential notation according to `toExpPos` and `toExpNeg`? No, but if required: + // maxBinaryExponent = floor((decimalExponent + 1) * log[2](10)) + // minBinaryExponent = floor(decimalExponent * log[2](10)) + // log[2](10) = 3.321928094887362347870319429489390175864 + + if (isExp) { + base = 2; + if (baseOut == 16) { + sd = sd * 4 - 3; + } else if (baseOut == 8) { + sd = sd * 3 - 2; + } + } else { + base = baseOut; + } + + // Convert the number as an integer then divide the result by its base raised to a power such + // that the fraction part will be restored. + + // Non-integer. + if (i >= 0) { + str = str.replace('.', ''); + y = new Ctor(1); + y.e = str.length - i; + y.d = convertBase(finiteToString(y), 10, base); + y.e = y.d.length; + } + + xd = convertBase(str, 10, base); + e = len = xd.length; + + // Remove trailing zeros. + for (; xd[--len] == 0;) xd.pop(); + + if (!xd[0]) { + str = isExp ? '0p+0' : '0'; + } else { + if (i < 0) { + e--; + } else { + x = new Ctor(x); + x.d = xd; + x.e = e; + x = divide(x, y, sd, rm, 0, base); + xd = x.d; + e = x.e; + roundUp = inexact; + } + + // The rounding digit, i.e. the digit after the digit that may be rounded up. + i = xd[sd]; + k = base / 2; + roundUp = roundUp || xd[sd + 1] !== void 0; + + roundUp = rm < 4 + ? (i !== void 0 || roundUp) && (rm === 0 || rm === (x.s < 0 ? 3 : 2)) + : i > k || i === k && (rm === 4 || roundUp || rm === 6 && xd[sd - 1] & 1 || + rm === (x.s < 0 ? 8 : 7)); + + xd.length = sd; + + if (roundUp) { + + // Rounding up may mean the previous digit has to be rounded up and so on. + for (; ++xd[--sd] > base - 1;) { + xd[sd] = 0; + if (!sd) { + ++e; + xd.unshift(1); + } + } + } + + // Determine trailing zeros. + for (len = xd.length; !xd[len - 1]; --len); + + // E.g. [4, 11, 15] becomes 4bf. + for (i = 0, str = ''; i < len; i++) str += NUMERALS.charAt(xd[i]); + + // Add binary exponent suffix? + if (isExp) { + if (len > 1) { + if (baseOut == 16 || baseOut == 8) { + i = baseOut == 16 ? 4 : 3; + for (--len; len % i; len++) str += '0'; + xd = convertBase(str, base, baseOut); + for (len = xd.length; !xd[len - 1]; --len); + + // xd[0] will always be be 1 + for (i = 1, str = '1.'; i < len; i++) str += NUMERALS.charAt(xd[i]); + } else { + str = str.charAt(0) + '.' + str.slice(1); + } + } + + str = str + (e < 0 ? 'p' : 'p+') + e; + } else if (e < 0) { + for (; ++e;) str = '0' + str; + str = '0.' + str; + } else { + if (++e > len) for (e -= len; e-- ;) str += '0'; + else if (e < len) str = str.slice(0, e) + '.' + str.slice(e); + } + } + + str = (baseOut == 16 ? '0x' : baseOut == 2 ? '0b' : baseOut == 8 ? '0o' : '') + str; + } + + return x.s < 0 ? '-' + str : str; + } + + + // Does not strip trailing zeros. + function truncate(arr, len) { + if (arr.length > len) { + arr.length = len; + return true; + } + } + + + // Decimal methods + + + /* + * abs + * acos + * acosh + * add + * asin + * asinh + * atan + * atanh + * atan2 + * cbrt + * ceil + * clamp + * clone + * config + * cos + * cosh + * div + * exp + * floor + * hypot + * ln + * log + * log2 + * log10 + * max + * min + * mod + * mul + * pow + * random + * round + * set + * sign + * sin + * sinh + * sqrt + * sub + * sum + * tan + * tanh + * trunc + */ + + + /* + * Return a new Decimal whose value is the absolute value of `x`. + * + * x {number|string|bigint|Decimal} + * + */ + function abs(x) { + return new this(x).abs(); + } + + + /* + * Return a new Decimal whose value is the arccosine in radians of `x`. + * + * x {number|string|bigint|Decimal} + * + */ + function acos(x) { + return new this(x).acos(); + } + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic cosine of `x`, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function acosh(x) { + return new this(x).acosh(); + } + + + /* + * Return a new Decimal whose value is the sum of `x` and `y`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * y {number|string|bigint|Decimal} + * + */ + function add(x, y) { + return new this(x).plus(y); + } + + + /* + * Return a new Decimal whose value is the arcsine in radians of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function asin(x) { + return new this(x).asin(); + } + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic sine of `x`, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function asinh(x) { + return new this(x).asinh(); + } + + + /* + * Return a new Decimal whose value is the arctangent in radians of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function atan(x) { + return new this(x).atan(); + } + + + /* + * Return a new Decimal whose value is the inverse of the hyperbolic tangent of `x`, rounded to + * `precision` significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function atanh(x) { + return new this(x).atanh(); + } + + + /* + * Return a new Decimal whose value is the arctangent in radians of `y/x` in the range -pi to pi + * (inclusive), rounded to `precision` significant digits using rounding mode `rounding`. + * + * Domain: [-Infinity, Infinity] + * Range: [-pi, pi] + * + * y {number|string|bigint|Decimal} The y-coordinate. + * x {number|string|bigint|Decimal} The x-coordinate. + * + * atan2(±0, -0) = ±pi + * atan2(±0, +0) = ±0 + * atan2(±0, -x) = ±pi for x > 0 + * atan2(±0, x) = ±0 for x > 0 + * atan2(-y, ±0) = -pi/2 for y > 0 + * atan2(y, ±0) = pi/2 for y > 0 + * atan2(±y, -Infinity) = ±pi for finite y > 0 + * atan2(±y, +Infinity) = ±0 for finite y > 0 + * atan2(±Infinity, x) = ±pi/2 for finite x + * atan2(±Infinity, -Infinity) = ±3*pi/4 + * atan2(±Infinity, +Infinity) = ±pi/4 + * atan2(NaN, x) = NaN + * atan2(y, NaN) = NaN + * + */ + function atan2(y, x) { + y = new this(y); + x = new this(x); + var r, + pr = this.precision, + rm = this.rounding, + wpr = pr + 4; + + // Either NaN + if (!y.s || !x.s) { + r = new this(NaN); + + // Both ±Infinity + } else if (!y.d && !x.d) { + r = getPi(this, wpr, 1).times(x.s > 0 ? 0.25 : 0.75); + r.s = y.s; + + // x is ±Infinity or y is ±0 + } else if (!x.d || y.isZero()) { + r = x.s < 0 ? getPi(this, pr, rm) : new this(0); + r.s = y.s; + + // y is ±Infinity or x is ±0 + } else if (!y.d || x.isZero()) { + r = getPi(this, wpr, 1).times(0.5); + r.s = y.s; + + // Both non-zero and finite + } else if (x.s < 0) { + this.precision = wpr; + this.rounding = 1; + r = this.atan(divide(y, x, wpr, 1)); + x = getPi(this, wpr, 1); + this.precision = pr; + this.rounding = rm; + r = y.s < 0 ? r.minus(x) : r.plus(x); + } else { + r = this.atan(divide(y, x, wpr, 1)); + } + + return r; + } + + + /* + * Return a new Decimal whose value is the cube root of `x`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function cbrt(x) { + return new this(x).cbrt(); + } + + + /* + * Return a new Decimal whose value is `x` rounded to an integer using `ROUND_CEIL`. + * + * x {number|string|bigint|Decimal} + * + */ + function ceil(x) { + return finalise(x = new this(x), x.e + 1, 2); + } + + + /* + * Return a new Decimal whose value is `x` clamped to the range delineated by `min` and `max`. + * + * x {number|string|bigint|Decimal} + * min {number|string|bigint|Decimal} + * max {number|string|bigint|Decimal} + * + */ + function clamp(x, min, max) { + return new this(x).clamp(min, max); + } + + + /* + * Configure global settings for a Decimal constructor. + * + * `obj` is an object with one or more of the following properties, + * + * precision {number} + * rounding {number} + * toExpNeg {number} + * toExpPos {number} + * maxE {number} + * minE {number} + * modulo {number} + * crypto {boolean|number} + * defaults {true} + * + * E.g. Decimal.config({ precision: 20, rounding: 4 }) + * + */ + function config(obj) { + if (!obj || typeof obj !== 'object') throw Error(decimalError + 'Object expected'); + var i, p, v, + useDefaults = obj.defaults === true, + ps = [ + 'precision', 1, MAX_DIGITS, + 'rounding', 0, 8, + 'toExpNeg', -EXP_LIMIT, 0, + 'toExpPos', 0, EXP_LIMIT, + 'maxE', 0, EXP_LIMIT, + 'minE', -EXP_LIMIT, 0, + 'modulo', 0, 9 + ]; + + for (i = 0; i < ps.length; i += 3) { + if (p = ps[i], useDefaults) this[p] = DEFAULTS[p]; + if ((v = obj[p]) !== void 0) { + if (mathfloor(v) === v && v >= ps[i + 1] && v <= ps[i + 2]) this[p] = v; + else throw Error(invalidArgument + p + ': ' + v); + } + } + + if (p = 'crypto', useDefaults) this[p] = DEFAULTS[p]; + if ((v = obj[p]) !== void 0) { + if (v === true || v === false || v === 0 || v === 1) { + if (v) { + if (typeof crypto != 'undefined' && crypto && + (crypto.getRandomValues || crypto.randomBytes)) { + this[p] = true; + } else { + throw Error(cryptoUnavailable); + } + } else { + this[p] = false; + } + } else { + throw Error(invalidArgument + p + ': ' + v); + } + } + + return this; + } + + + /* + * Return a new Decimal whose value is the cosine of `x`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function cos(x) { + return new this(x).cos(); + } + + + /* + * Return a new Decimal whose value is the hyperbolic cosine of `x`, rounded to precision + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function cosh(x) { + return new this(x).cosh(); + } + + + /* + * Create and return a Decimal constructor with the same configuration properties as this Decimal + * constructor. + * + */ + function clone(obj) { + var i, p, ps; + + /* + * The Decimal constructor and exported function. + * Return a new Decimal instance. + * + * v {number|string|bigint|Decimal} A numeric value. + * + */ + function Decimal(v) { + var e, i, t, + x = this; + + // Decimal called without new. + if (!(x instanceof Decimal)) return new Decimal(v); + + // Retain a reference to this Decimal constructor, and shadow Decimal.prototype.constructor + // which points to Object. + x.constructor = Decimal; + + if (isDecimalInstance(v)) { + x.s = v.s; + + if (external) { + if (!v.d || v.e > Decimal.maxE) { + + // Infinity. + x.e = NaN; + x.d = null; + } else if (v.e < Decimal.minE) { + + // Zero. + x.e = 0; + x.d = [0]; + } else { + x.e = v.e; + x.d = v.d.slice(); + } + } else { + x.e = v.e; + x.d = v.d ? v.d.slice() : v.d; + } + + return; + } + + t = typeof v; + + if (t === 'number') { + if (v === 0) { + x.s = 1 / v < 0 ? -1 : 1; + x.e = 0; + x.d = [0]; + return; + } + + if (v < 0) { + v = -v; + x.s = -1; + } else { + x.s = 1; + } + + // Fast path for small integers. + if (v === ~~v && v < 1e7) { + for (e = 0, i = v; i >= 10; i /= 10) e++; + + if (external) { + if (e > Decimal.maxE) { + x.e = NaN; + x.d = null; + } else if (e < Decimal.minE) { + x.e = 0; + x.d = [0]; + } else { + x.e = e; + x.d = [v]; + } + } else { + x.e = e; + x.d = [v]; + } + + return; + } + + // Infinity or NaN? + if (v * 0 !== 0) { + if (!v) x.s = NaN; + x.e = NaN; + x.d = null; + return; + } + + return parseDecimal(x, v.toString()); + } + + if (t === 'string') { + if ((i = v.charCodeAt(0)) === 45) { // minus sign + v = v.slice(1); + x.s = -1; + } else { + if (i === 43) v = v.slice(1); // plus sign + x.s = 1; + } + + return isDecimal.test(v) ? parseDecimal(x, v) : parseOther(x, v); + } + + if (t === 'bigint') { + if (v < 0) { + v = -v; + x.s = -1; + } else { + x.s = 1; + } + + return parseDecimal(x, v.toString()); + } + + throw Error(invalidArgument + v); + } + + Decimal.prototype = P; + + Decimal.ROUND_UP = 0; + Decimal.ROUND_DOWN = 1; + Decimal.ROUND_CEIL = 2; + Decimal.ROUND_FLOOR = 3; + Decimal.ROUND_HALF_UP = 4; + Decimal.ROUND_HALF_DOWN = 5; + Decimal.ROUND_HALF_EVEN = 6; + Decimal.ROUND_HALF_CEIL = 7; + Decimal.ROUND_HALF_FLOOR = 8; + Decimal.EUCLID = 9; + + Decimal.config = Decimal.set = config; + Decimal.clone = clone; + Decimal.isDecimal = isDecimalInstance; + + Decimal.abs = abs; + Decimal.acos = acos; + Decimal.acosh = acosh; // ES6 + Decimal.add = add; + Decimal.asin = asin; + Decimal.asinh = asinh; // ES6 + Decimal.atan = atan; + Decimal.atanh = atanh; // ES6 + Decimal.atan2 = atan2; + Decimal.cbrt = cbrt; // ES6 + Decimal.ceil = ceil; + Decimal.clamp = clamp; + Decimal.cos = cos; + Decimal.cosh = cosh; // ES6 + Decimal.div = div; + Decimal.exp = exp; + Decimal.floor = floor; + Decimal.hypot = hypot; // ES6 + Decimal.ln = ln; + Decimal.log = log; + Decimal.log10 = log10; // ES6 + Decimal.log2 = log2; // ES6 + Decimal.max = max; + Decimal.min = min; + Decimal.mod = mod; + Decimal.mul = mul; + Decimal.pow = pow; + Decimal.random = random; + Decimal.round = round; + Decimal.sign = sign; // ES6 + Decimal.sin = sin; + Decimal.sinh = sinh; // ES6 + Decimal.sqrt = sqrt; + Decimal.sub = sub; + Decimal.sum = sum; + Decimal.tan = tan; + Decimal.tanh = tanh; // ES6 + Decimal.trunc = trunc; // ES6 + + if (obj === void 0) obj = {}; + if (obj) { + if (obj.defaults !== true) { + ps = ['precision', 'rounding', 'toExpNeg', 'toExpPos', 'maxE', 'minE', 'modulo', 'crypto']; + for (i = 0; i < ps.length;) if (!obj.hasOwnProperty(p = ps[i++])) obj[p] = this[p]; + } + } + + Decimal.config(obj); + + return Decimal; + } + + + /* + * Return a new Decimal whose value is `x` divided by `y`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * y {number|string|bigint|Decimal} + * + */ + function div(x, y) { + return new this(x).div(y); + } + + + /* + * Return a new Decimal whose value is the natural exponential of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} The power to which to raise the base of the natural log. + * + */ + function exp(x) { + return new this(x).exp(); + } + + + /* + * Return a new Decimal whose value is `x` round to an integer using `ROUND_FLOOR`. + * + * x {number|string|bigint|Decimal} + * + */ + function floor(x) { + return finalise(x = new this(x), x.e + 1, 3); + } + + + /* + * Return a new Decimal whose value is the square root of the sum of the squares of the arguments, + * rounded to `precision` significant digits using rounding mode `rounding`. + * + * hypot(a, b, ...) = sqrt(a^2 + b^2 + ...) + * + * arguments {number|string|bigint|Decimal} + * + */ + function hypot() { + var i, n, + t = new this(0); + + external = false; + + for (i = 0; i < arguments.length;) { + n = new this(arguments[i++]); + if (!n.d) { + if (n.s) { + external = true; + return new this(1 / 0); + } + t = n; + } else if (t.d) { + t = t.plus(n.times(n)); + } + } + + external = true; + + return t.sqrt(); + } + + + /* + * Return true if object is a Decimal instance (where Decimal is any Decimal constructor), + * otherwise return false. + * + */ + function isDecimalInstance(obj) { + return obj instanceof Decimal || obj && obj.toStringTag === tag || false; + } + + + /* + * Return a new Decimal whose value is the natural logarithm of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function ln(x) { + return new this(x).ln(); + } + + + /* + * Return a new Decimal whose value is the log of `x` to the base `y`, or to base 10 if no base + * is specified, rounded to `precision` significant digits using rounding mode `rounding`. + * + * log[y](x) + * + * x {number|string|bigint|Decimal} The argument of the logarithm. + * y {number|string|bigint|Decimal} The base of the logarithm. + * + */ + function log(x, y) { + return new this(x).log(y); + } + + + /* + * Return a new Decimal whose value is the base 2 logarithm of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function log2(x) { + return new this(x).log(2); + } + + + /* + * Return a new Decimal whose value is the base 10 logarithm of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function log10(x) { + return new this(x).log(10); + } + + + /* + * Return a new Decimal whose value is the maximum of the arguments. + * + * arguments {number|string|bigint|Decimal} + * + */ + function max() { + return maxOrMin(this, arguments, -1); + } + + + /* + * Return a new Decimal whose value is the minimum of the arguments. + * + * arguments {number|string|bigint|Decimal} + * + */ + function min() { + return maxOrMin(this, arguments, 1); + } + + + /* + * Return a new Decimal whose value is `x` modulo `y`, rounded to `precision` significant digits + * using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * y {number|string|bigint|Decimal} + * + */ + function mod(x, y) { + return new this(x).mod(y); + } + + + /* + * Return a new Decimal whose value is `x` multiplied by `y`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * y {number|string|bigint|Decimal} + * + */ + function mul(x, y) { + return new this(x).mul(y); + } + + + /* + * Return a new Decimal whose value is `x` raised to the power `y`, rounded to precision + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} The base. + * y {number|string|bigint|Decimal} The exponent. + * + */ + function pow(x, y) { + return new this(x).pow(y); + } + + + /* + * Returns a new Decimal with a random value equal to or greater than 0 and less than 1, and with + * `sd`, or `Decimal.precision` if `sd` is omitted, significant digits (or less if trailing zeros + * are produced). + * + * [sd] {number} Significant digits. Integer, 0 to MAX_DIGITS inclusive. + * + */ + function random(sd) { + var d, e, k, n, + i = 0, + r = new this(1), + rd = []; + + if (sd === void 0) sd = this.precision; + else checkInt32(sd, 1, MAX_DIGITS); + + k = Math.ceil(sd / LOG_BASE); + + if (!this.crypto) { + for (; i < k;) rd[i++] = Math.random() * 1e7 | 0; + + // Browsers supporting crypto.getRandomValues. + } else if (crypto.getRandomValues) { + d = crypto.getRandomValues(new Uint32Array(k)); + + for (; i < k;) { + n = d[i]; + + // 0 <= n < 4294967296 + // Probability n >= 4.29e9, is 4967296 / 4294967296 = 0.00116 (1 in 865). + if (n >= 4.29e9) { + d[i] = crypto.getRandomValues(new Uint32Array(1))[0]; + } else { + + // 0 <= n <= 4289999999 + // 0 <= (n % 1e7) <= 9999999 + rd[i++] = n % 1e7; + } + } + + // Node.js supporting crypto.randomBytes. + } else if (crypto.randomBytes) { + + // buffer + d = crypto.randomBytes(k *= 4); + + for (; i < k;) { + + // 0 <= n < 2147483648 + n = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + ((d[i + 3] & 0x7f) << 24); + + // Probability n >= 2.14e9, is 7483648 / 2147483648 = 0.0035 (1 in 286). + if (n >= 2.14e9) { + crypto.randomBytes(4).copy(d, i); + } else { + + // 0 <= n <= 2139999999 + // 0 <= (n % 1e7) <= 9999999 + rd.push(n % 1e7); + i += 4; + } + } + + i = k / 4; + } else { + throw Error(cryptoUnavailable); + } + + k = rd[--i]; + sd %= LOG_BASE; + + // Convert trailing digits to zeros according to sd. + if (k && sd) { + n = mathpow(10, LOG_BASE - sd); + rd[i] = (k / n | 0) * n; + } + + // Remove trailing words which are zero. + for (; rd[i] === 0; i--) rd.pop(); + + // Zero? + if (i < 0) { + e = 0; + rd = [0]; + } else { + e = -1; + + // Remove leading words which are zero and adjust exponent accordingly. + for (; rd[0] === 0; e -= LOG_BASE) rd.shift(); + + // Count the digits of the first word of rd to determine leading zeros. + for (k = 1, n = rd[0]; n >= 10; n /= 10) k++; + + // Adjust the exponent for leading zeros of the first word of rd. + if (k < LOG_BASE) e -= LOG_BASE - k; + } + + r.e = e; + r.d = rd; + + return r; + } + + + /* + * Return a new Decimal whose value is `x` rounded to an integer using rounding mode `rounding`. + * + * To emulate `Math.round`, set rounding to 7 (ROUND_HALF_CEIL). + * + * x {number|string|bigint|Decimal} + * + */ + function round(x) { + return finalise(x = new this(x), x.e + 1, this.rounding); + } + + + /* + * Return + * 1 if x > 0, + * -1 if x < 0, + * 0 if x is 0, + * -0 if x is -0, + * NaN otherwise + * + * x {number|string|bigint|Decimal} + * + */ + function sign(x) { + x = new this(x); + return x.d ? (x.d[0] ? x.s : 0 * x.s) : x.s || NaN; + } + + + /* + * Return a new Decimal whose value is the sine of `x`, rounded to `precision` significant digits + * using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function sin(x) { + return new this(x).sin(); + } + + + /* + * Return a new Decimal whose value is the hyperbolic sine of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function sinh(x) { + return new this(x).sinh(); + } + + + /* + * Return a new Decimal whose value is the square root of `x`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * + */ + function sqrt(x) { + return new this(x).sqrt(); + } + + + /* + * Return a new Decimal whose value is `x` minus `y`, rounded to `precision` significant digits + * using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} + * y {number|string|bigint|Decimal} + * + */ + function sub(x, y) { + return new this(x).sub(y); + } + + + /* + * Return a new Decimal whose value is the sum of the arguments, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * Only the result is rounded, not the intermediate calculations. + * + * arguments {number|string|bigint|Decimal} + * + */ + function sum() { + var i = 0, + args = arguments, + x = new this(args[i]); + + external = false; + for (; x.s && ++i < args.length;) x = x.plus(args[i]); + external = true; + + return finalise(x, this.precision, this.rounding); + } + + + /* + * Return a new Decimal whose value is the tangent of `x`, rounded to `precision` significant + * digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function tan(x) { + return new this(x).tan(); + } + + + /* + * Return a new Decimal whose value is the hyperbolic tangent of `x`, rounded to `precision` + * significant digits using rounding mode `rounding`. + * + * x {number|string|bigint|Decimal} A value in radians. + * + */ + function tanh(x) { + return new this(x).tanh(); + } + + + /* + * Return a new Decimal whose value is `x` truncated to an integer. + * + * x {number|string|bigint|Decimal} + * + */ + function trunc(x) { + return finalise(x = new this(x), x.e + 1, 1); + } + + + // Create and configure initial Decimal constructor. + Decimal = clone(DEFAULTS); + Decimal.prototype.constructor = Decimal; + Decimal['default'] = Decimal.Decimal = Decimal; + + // Create the internal constants from their string values. + LN10 = new Decimal(LN10); + PI = new Decimal(PI); + + + // Export. + + + // AMD. + if (typeof define == 'function' && define.amd) { + define(function () { + return Decimal; + }); + + // Node and other environments that support module.exports. + } else if (typeof module != 'undefined' && module.exports) { + if (typeof Symbol == 'function' && typeof Symbol.iterator == 'symbol') { + P[Symbol['for']('nodejs.util.inspect.custom')] = P.toString; + P[Symbol.toStringTag] = 'Decimal'; + } + + module.exports = Decimal; + + // Browser. + } else { + if (!globalScope) { + globalScope = typeof self != 'undefined' && self && self.self == self ? self : window; + } + + noConflict = globalScope.Decimal; + Decimal.noConflict = function () { + globalScope.Decimal = noConflict; + return Decimal; + }; + + globalScope.Decimal = Decimal; + } +})(this); diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/Algebra.js b/tools/ui/src/lib/vendors/nerdamer-prime/Algebra.js new file mode 100644 index 000000000..4371baa23 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/Algebra.js @@ -0,0 +1,6213 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * License : MIT + * Source : https://github.com/jiggzson/nerdamer + */ + +// Type imports for JSDoc ====================================================== +// These typedefs provide type aliases for the interfaces defined in index.d.ts. +// They enable proper type checking when working with the classes defined in this file. +// +// Usage patterns: +// - For return types: @returns {NerdamerSymbolType} +// - For parameters: @param {NerdamerSymbolType} symbol +// - For variable declarations: /** @type {NerdamerSymbolType} */ +// +// Note: When casting local class instances to interface types, use the pattern: +// /** @type {InterfaceType} */ (/** @type {unknown} */ (localInstance)) +// This is needed because TypeScript sees local classes and interfaces as separate types. + +/** + * Core type aliases from index.d.ts + * + * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType + * + * @typedef {import('./index').NerdamerCore.Frac} FracType + * + * @typedef {import('./index').NerdamerCore.Vector} VectorType + * + * @typedef {import('./index').NerdamerCore.Matrix} MatrixType + * + * @typedef {NerdamerSymbolType | VectorType | MatrixType} ParseResultType Union type for parse results + * + * @typedef {import('./index').NerdamerCore.Parser} ParserType + * + * @typedef {import('./index').NerdamerCore.Collection} CollectionType + * + * @typedef {import('./index').NerdamerCore.Settings} SettingsType + * + * @typedef {import('./index').NerdamerExpression} ExpressionType + * + * @typedef {typeof import('./index')} NerdamerType + * + * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface + * + * @typedef {import('./index').NerdamerCore.Math2} Math2Interface + * + * @typedef {import('./index').NerdamerCore.Core} CoreType + * + * @typedef {import('./index').ExpressionParam} ExpressionParam + * + * @typedef {import('./index').ArithmeticOperand} ArithmeticOperand + * + * @typedef {import('./index').ExpandOptions} ExpandOptions + * + * @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType Constructor types + * + * @typedef {import('./index').NerdamerCore.FracConstructor} FracConstructor + * + * @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor + * + * @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor + * + * @typedef {import('./index').NerdamerCore.AlgebraModule} AlgebraModuleType + * + * @typedef {import('./index').NerdamerCore.Polynomial} Polynomial + * + * @typedef {import('./index').NerdamerCore.Factors} Factors + * + * @typedef {import('./index').NerdamerCore.FactorsLike} FactorsLike + * + * @typedef {import('./index').NerdamerCore.MVTerm} MVTerm + * + * @typedef {import('./index').NerdamerCore.FactorSubModule} FactorInterface + * + * @typedef {import('./index').NerdamerCore.SimplifySubModule} SimplifyInterface + * + * @typedef {import('./index').NerdamerCore.PartFracSubModule} PartFracInterface + * + * @typedef {new () => Factors} FactorsConstructor + */ + +// Check if nerdamer exists globally (browser) or needs to be required (Node.js) +let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined; +if (typeof module !== 'undefined' && nerdamer === undefined) { + nerdamer = require('./nerdamer.core.js'); + require('./Calculus.js'); +} + +(function initAlgebraModule() { + /* Shortcuts*/ + /** @type {CoreType} */ + const core = nerdamer.getCore(); + /** @type {ParserType} */ + const _ = core.PARSER; + const { N, P, S, EX, FN, PL, CP, CB } = core.groups; + const { keys, even, variables, format, round, isInt } = core.Utils; + const { Frac, NerdamerSymbol, Vector: _Vector, Expression: _Expression } = core; + const { CONST_HASH } = core.Settings; + /** @type {Record} */ + const math = core.Utils.importFunctions(); + const _evaluate = core.Utils.evaluate; + //* ************** CLASSES ***************// + /** + * Converts a symbol into an equivalent polynomial arrays of the form [[coefficient_1, power_1],[coefficient_2, + * power_2], ... ] Univariate polymials only. + * + * @class + * @this {Polynomial} + * @param {NerdamerSymbolType | number | string} [symbol] + * @param {string} [variable] The variable name of the polynomial + * @param {number} [order] + */ + function Polynomial(symbol, variable, order) { + /** @type {FracType[]} */ + this.coeffs = []; + /** @type {string} */ + this.variable = ''; + + if (core.Utils.isSymbol(symbol)) { + this.parse(/** @type {NerdamerSymbolType} */ (symbol)); + this.variable ||= variable || ''; + } else if (typeof symbol === 'number' && !isNaN(symbol)) { + order ||= 0; + if (variable === undefined) { + throw new core.exceptions.InvalidVariableNameError( + 'Polynomial expects a variable name when creating using order' + ); + } + this.coeffs = []; + this.coeffs[order] = new Frac(symbol); + this.fill(symbol); + } else if (typeof symbol === 'string') { + this.parse(_.parse(symbol)); + } + } + /** + * Creates a Polynomial given an array of coefficients + * + * @param {FracType[]} arr + * @param {string} variable + * @returns {Polynomial} + */ + Polynomial.fromArray = function fromArray(arr, variable) { + if (typeof variable === 'undefined') { + throw new core.exceptions.InvalidVariableNameError( + 'A variable name must be specified when creating polynomial from array' + ); + } + /** @type {Polynomial} */ + const p = new Polynomial(); + p.coeffs = arr; + p.variable = variable; + return p; + }; + + /** + * @param {number} c1 + * @param {number} c2 + * @param {number} n + * @param {number} base + * @param {number} p + * @param {string} variable + * @returns {Polynomial | null} + */ + Polynomial.fit = function fit(c1, c2, n, base, p, variable) { + // After having looped through and mod 10 the number to get the matching factor + const terms = new Array(p + 1); + let t = n - c2; + terms[0] = c2; // The constants is assumed to be correct + // constant for x^p is also assumed know so add + terms[p] = c1; + t -= c1 * base ** p; + // Start fitting + for (let i = p - 1; i > 0; i--) { + const b = base ** i; // We want as many wholes as possible + const q = t / b; + const sign = Math.sign(q); + const c = sign * Math.floor(Math.abs(q)); + t -= c * b; + terms[i] = c; + } + if (t !== 0) { + return null; + } + for (let i = 0; i < terms.length; i++) { + terms[i] = new Frac(terms[i]); + } + + return Polynomial.fromArray(terms, variable); + }; + + Polynomial.prototype = { + /** + * Converts NerdamerSymbol to Polynomial + * + * @this {Polynomial} + * @param {NerdamerSymbolType} symbol + * @param {FracType[]} [c] - A collector array + * @returns {void} + */ + parse(symbol, c) { + this.variable = variables(symbol)[0]; + if (!symbol.isPoly()) { + throw new core.exceptions.NerdamerTypeError(`Polynomial Expected! Received ${core.Utils.text(symbol)}`); + } + c ||= []; + if (!(/** @type {FracType} */ (symbol.power).absEquals(1))) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + + if (symbol.group === core.groups.N) { + c[0] = symbol.multiplier; + } else if (symbol.group === core.groups.S) { + c[Number(/** @type {FracType} */ (symbol.power).toDecimal())] = symbol.multiplier; + } else { + for (const x in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, x)) { + continue; + } + const sub = symbol.symbols[x]; + const p = sub.power; + if (core.Utils.isSymbol(p)) { + throw new core.exceptions.NerdamerTypeError('power cannot be a NerdamerSymbol'); + } + + const pNum = sub.group === N ? 0 : Number(/** @type {FracType} */ (p).toDecimal()); + if (sub.symbols) { + this.parse(sub, c); + } else { + c[pNum] = sub.multiplier; + } + } + } + + this.coeffs = c; + + this.fill(); + }, + /** + * Fills in the holes in a polynomial with zeroes + * + * @this {Polynomial} + * @param {number} [x] - The number to fill the holes with + * @returns {Polynomial} + */ + fill(x) { + x = Number(x) || 0; + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + if (this.coeffs[i] === undefined) { + this.coeffs[i] = new Frac(x); + } + } + return this; + }, + /** + * Removes higher order zeros or a specific coefficient + * + * @this {Polynomial} + * @returns {Polynomial} + */ + trim() { + let l = this.coeffs.length; + while (l--) { + const c = this.coeffs[l]; + const equalsZero = c.equals(0); + if (c && equalsZero) { + if (l === 0) { + break; + } + this.coeffs.pop(); + } else { + break; + } + } + + return this; + }, + /** + * Returns polynomial mod p **currently fails** + * + * @this {Polynomial} + * @param {number} p + * @returns {Polynomial} + */ + modP(p) { + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + let c = this.coeffs[i]; + let j; + if (c.lessThan(0)) { + // Go borrow + /** @type {FracType | undefined} */ + let b; // A coefficient > 0 + for (j = i; j < l; j++) { + // Starting from where we left off + if (this.coeffs[j].greaterThan(0)) { + b = this.coeffs[j]; + break; + } + } + + if (b) { + // If such a coefficient exists + for (; j > i; j--) { + // Go down the line and adjust using p + this.coeffs[j] = this.coeffs[j].subtract(new Frac(1)); + this.coeffs[j - 1] = this.coeffs[j - 1].add(new Frac(p)); + } + c = this.coeffs[i]; // Reset c + } + } + + const d = c.mod(new Frac(p)); + const w = c.subtract(d).divide(new Frac(p)); + if (!w.equals(0)) { + const upOne = i + 1; + let next = this.coeffs[upOne] || new Frac(0); + next = next.add(w); + this.coeffs[upOne] = next; + this.coeffs[i] = d; + } + } + + return this; + }, + /** + * Adds together 2 polynomials + * + * @this {Polynomial} + * @param {Polynomial} poly + * @returns {Polynomial} + */ + add(poly) { + const l = Math.max(this.coeffs.length, poly.coeffs.length); + for (let i = 0; i < l; i++) { + const a = this.coeffs[i] || new Frac(0); + const b = poly.coeffs[i] || new Frac(0); + this.coeffs[i] = a.add(b); + } + return this; + }, + /** + * Subtracts 2 polynomials + * + * @this {Polynomial} + * @param {Polynomial} poly + * @returns {Polynomial} + */ + subtract(poly) { + const l = Math.max(this.coeffs.length, poly.coeffs.length); + for (let i = 0; i < l; i++) { + const a = this.coeffs[i] || new Frac(0); + const b = poly.coeffs[i] || new Frac(0); + this.coeffs[i] = a.subtract(b); + } + return this; + }, + /** + * Divides two polynomials + * + * @this {Polynomial} + * @param {Polynomial} poly + * @returns {[Polynomial, Polynomial]} + */ + divide(poly) { + const { variable } = this; + /** @type {FracType[]} */ + const dividend = /** @type {FracType[]} */ (core.Utils.arrayClone(this.coeffs)); + /** @type {FracType[]} */ + const divisor = /** @type {FracType[]} */ (core.Utils.arrayClone(poly.coeffs)); + const n = dividend.length; + const mp = divisor.length - 1; + /** @type {FracType[]} */ + const quotient = []; + + // Loop through the dividend + for (let i = 0; i < n; i++) { + const p = n - (i + 1); + // Get the difference of the powers + const d = p - mp; + // Get the quotient of the coefficients + const q = dividend[p].divide(divisor[mp]); + + if (d < 0) { + break; + } // The divisor is not greater than the dividend + // place it in the quotient + quotient[d] = q; + + for (let j = 0; j <= mp; j++) { + // Reduce the dividend + dividend[j + d] = dividend[j + d].subtract(divisor[j].multiply(q)); + } + } + + // Clean up + const p1 = Polynomial.fromArray(dividend, variable || 'x').trim(); // Pass in x for safety + const p2 = Polynomial.fromArray(quotient, variable || 'x'); + return [p2, p1]; + }, + /** + * Multiplies two polynomials + * + * @this {Polynomial} + * @param {Polynomial} poly + * @returns {Polynomial} + */ + multiply(poly) { + const l1 = this.coeffs.length; + const l2 = poly.coeffs.length; + /** @type {FracType[]} */ + const c = []; // Array to be returned + for (let i = 0; i < l1; i++) { + const x1 = this.coeffs[i]; + for (let j = 0; j < l2; j++) { + const k = i + j; // Add the powers together + const x2 = poly.coeffs[j]; + const e = c[k] || new Frac(0); // Get the existing term from the new array + c[k] = e.add(x1.multiply(x2)); // Multiply the coefficients and add to new polynomial array + } + } + this.coeffs = c; + return this; + }, + /** + * Checks if a polynomial is zero + * + * @this {Polynomial} + * @returns {boolean} + */ + isZero() { + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + const e = this.coeffs[i]; + if (!e.equals(0)) { + return false; + } + } + return true; + }, + /** + * Substitutes in a number n into the polynomial p(n) + * + * @this {Polynomial} + * @param {number} n + * @returns {FracType} + */ + sub(n) { + let sum = new Frac(0); + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + const t = this.coeffs[i]; + if (!t.equals(0)) { + sum = sum.add(t.multiply(new Frac(n ** i))); + } + } + return sum; + }, + /** + * Returns a clone of the polynomial + * + * @this {Polynomial} + * @returns {Polynomial} + */ + clone() { + /** @type {Polynomial} */ + const p = new Polynomial(); + p.coeffs = this.coeffs.slice(); + p.variable = this.variable; + return p; + }, + /** + * Gets the degree of the polynomial + * + * @this {Polynomial} + * @returns {number} + */ + deg() { + this.trim(); + return this.coeffs.length - 1; + }, + /** + * Returns a lead coefficient + * + * @this {Polynomial} + * @returns {FracType} + */ + lc() { + return this.coeffs[this.deg()].clone(); + }, + /** + * Converts polynomial into a monic polynomial + * + * @this {Polynomial} + * @returns {Polynomial} + */ + monic() { + const lc = this.lc(); + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + this.coeffs[i] = this.coeffs[i].divide(lc); + } + return this; + }, + /** + * Returns the GCD of two polynomials + * + * @this {Polynomial} + * @param {Polynomial} poly + * @returns {Polynomial} + */ + gcd(poly) { + // Get the maximum power of each + const mp1 = this.coeffs.length - 1; + const mp2 = poly.coeffs.length - 1; + /** @type {[Polynomial, Polynomial]} */ + let T; + // Swap so we always have the greater power first + if (mp1 < mp2) { + return poly.gcd(this); + } + /** @type {Polynomial} */ + let a = this; + + while (!poly.isZero()) { + const t = poly.clone(); + a = a.clone(); + T = a.divide(t); + poly = T[1]; + a = t; + } + + const gcd = core.Math2.QGCD.apply(null, a.coeffs); + if (!gcd.equals(1)) { + const l = a.coeffs.length; + for (let i = 0; i < l; i++) { + a.coeffs[i] = a.coeffs[i].divide(gcd); + } + } + return a; + }, + /** + * Differentiates the polynomial + * + * @this {Polynomial} + * @returns {Polynomial} + */ + diff() { + /** @type {FracType[]} */ + const newArray = []; + const l = this.coeffs.length; + for (let i = 1; i < l; i++) { + newArray.push(this.coeffs[i].multiply(new Frac(i))); + } + this.coeffs = newArray; + return this; + }, + /** + * Integrates the polynomial + * + * @this {Polynomial} + * @returns {Polynomial} + */ + integrate() { + /** @type {FracType[]} */ + const newArray = [new Frac(0)]; + const l = this.coeffs.length; + for (let i = 0; i < l; i++) { + const c = new Frac(i + 1); + newArray[i + 1] = this.coeffs[i].divide(c); + } + this.coeffs = newArray; + return this; + }, + /** + * Returns the Greatest common factor of the polynomial + * + * @this {Polynomial} + * @param {boolean} [toPolynomial] - True if a polynomial is wanted + * @returns {[FracType, number] | Polynomial} + */ + gcf(toPolynomial) { + // Get the first nozero coefficient and returns its power + /** + * @param {FracType[]} a + * @returns {number | undefined} + */ + const fnz = function (a) { + for (let i = 0; i < a.length; i++) { + if (!a[i].equals(0)) { + return i; + } + } + return undefined; + }; + /** @type {FracType[]} */ + const ca = []; + for (let i = 0; i < this.coeffs.length; i++) { + const c = this.coeffs[i]; + if (!c.equals(0) && ca.indexOf(c) === -1) { + ca.push(c); + } + } + /** @type {[FracType, number] | Polynomial} */ + let p = [core.Math2.QGCD.apply(undefined, ca), fnz(this.coeffs) || 0]; + + if (toPolynomial) { + const parr = []; + parr[p[1] - 1] = p[0]; + p = Polynomial.fromArray(parr, this.variable).fill(); + } + + return p; + }, + /** + * Raises a polynomial P to a power p -> P^p. e.g. (x+1)^2 + * + * @this {Polynomial} + * @param {boolean} [inclImg] - Include imaginary numbers + * @returns {number[]} + */ + quad(inclImg) { + /** @type {number[]} */ + const roots = []; + if (this.coeffs.length > 3) { + throw new Error(`Cannot calculate quadratic order of ${this.coeffs.length - 1}`); + } + if (this.coeffs.length === 0) { + throw new Error('Polynomial array has no terms'); + } + const a = this.coeffs[2] ? Number(this.coeffs[2].toDecimal()) : 0; + const b = this.coeffs[1] ? Number(this.coeffs[1].toDecimal()) : 0; + const c = Number(this.coeffs[0].toDecimal()); + const dsc = b * b - 4 * a * c; + if (dsc < 0 && !inclImg) { + return roots; + } + roots[0] = (-b + Math.sqrt(dsc)) / (2 * a); + roots[1] = (-b - Math.sqrt(dsc)) / (2 * a); + + return roots; + }, + /** + * Makes polynomial square free + * + * @this {Polynomial} + * @returns {[Polynomial, Polynomial, number]} + */ + squareFree() { + const a = this.clone(); + let i = 1; + const b = a.clone().diff(); + let c = a.clone().gcd(b); + let w = a.divide(c)[0]; + let output = Polynomial.fromArray([new Frac(1)], a.variable); + while (!c.equalsNumber(1)) { + const y = w.gcd(c); + let z = w.divide(y)[0]; + // One of the factors may have shown up since it's square but smaller than the + // one where finding + if (!z.equalsNumber(1) && i > 1) { + const t = z.clone(); + for (let j = 1; j < i; j++) { + t.multiply(z.clone()); + } + z = t; + } + output = output.multiply(z); + i++; + w = y; + c = c.divide(y)[0]; + } + + return [output, w, i]; + }, + /** + * Converts polynomial to NerdamerSymbol + * + * @this {Polynomial} + * @returns {NerdamerSymbolType} + */ + toSymbol() { + const l = this.coeffs.length; + const { variable } = this; + if (l === 0) { + return new NerdamerSymbol(0); + } + + // Polynomials must have a variable + if (!variable) { + throw new core.exceptions.NerdamerTypeError( + 'Polynomial.toSymbol requires a variable. Constants should not be converted to Polynomial.' + ); + } + + const terms = []; + + for (let i = 0; i < l; i++) { + const e = this.coeffs[i]; + if (!e.equals(0)) { + terms.push(`${e}*${variable}^${i}`); + } + } + if (terms.length === 0) { + return new NerdamerSymbol(0); + } + return _.parse(terms.join('+')); + }, + /** + * Checks if polynomial is equal to a number + * + * @this {Polynomial} + * @param {number} x + * @returns {boolean} + */ + equalsNumber(x) { + this.trim(); + return this.coeffs.length === 1 && this.coeffs[0].toDecimal() === String(x); + }, + /** + * @this {Polynomial} + * @returns {string} + */ + toString() { + return this.toSymbol().toString(); + }, + }; + + /** + * # TODO + * + * # THIS METHOD HAS A NASTY HIDDEN BUG. IT HAS INCONSISTENT RETURN TYPES PRIMARILY DUE TO + * + * WRONG ASSUMPTIONS AT THE BEGINNING. THE ASSUMPTION WAS THAT COEFFS WERE ALWAYS GOING BE NUMBERS NOT TAKING INTO + * ACCOUNT THAT IMAGINARY NUMBERS. FIXING THIS BREAKS WAY TOO MANY TESTS AT THEM MOMENT WHICH I DON'T HAVE TO FIX + * + * If the symbols is of group PL or CP it will return the multipliers of each symbol as these are polynomial + * coefficients. CB symbols are glued together by multiplication so the symbol multiplier carries the coefficients + * for all contained symbols. For S it just returns it's own multiplier. This function doesn't care if it's a + * polynomial or not + * + * @this {NerdamerSymbolType} + * @param {Array} [c] The coefficient array + * @param {boolean} [withOrder] + * @returns {Array} + */ + NerdamerSymbol.prototype.coeffs = function coeffs(c, withOrder) { + if (withOrder && !this.isPoly(true)) { + _.error('Polynomial expected when requesting coefficients with order'); + } + c ||= []; + const s = this.clone().distributeMultiplier(); + if (s.isComposite()) { + for (const x in s.symbols) { + if (!Object.hasOwn(s.symbols, x)) { + continue; + } + const sub = s.symbols[x]; + if (sub.isComposite()) { + sub.clone().distributeMultiplier().coeffs(c, withOrder); + } else if (withOrder) { + c[sub.isConstant() ? 0 : Number(/** @type {FracType} */ (sub.power).toDecimal())] = sub.multiplier; + } else { + c.push(sub.multiplier); + } + } + } else if (withOrder) { + c[s.isConstant(true) ? 0 : Number(/** @type {FracType} */ (s.power).toDecimal())] = s.multiplier; + } else if (s.group === CB && s.isImaginary()) { + let m = new NerdamerSymbol(s.multiplier); + s.each(x => { + // Add the imaginary part + if (x.isConstant(true) || x.imaginary) { + m = /** @type {NerdamerSymbolType} */ (_.multiply(m, x)); + } + }); + c.push(m); + } else { + c.push(s.multiplier); + } + // Fill the holes + if (withOrder) { + for (let i = 0; i < c.length; i++) { + if (c[i] === undefined) { + c[i] = new NerdamerSymbol(0); + } + } + } + return c; + }; + /** + * @this {NerdamerSymbolType} + * @param {Record & { length: number }} map + * @returns {MVTerm[]} + */ + NerdamerSymbol.prototype.tBase = function tBase(map) { + if (typeof map === 'undefined') { + throw new Error('NerdamerSymbol.tBase requires a map object!'); + } + /** @type {MVTerm[]} */ + const terms = []; + const symbols = /** @type {NerdamerSymbolType[]} */ (this.collectSymbols(null, null, null, true)); + const l = symbols.length; + for (let i = 0; i < l; i++) { + const symbol = symbols[i]; + const g = symbol.group; + /** @type {MVTerm} */ + const nterm = new MVTerm(symbol.multiplier, [], map); + if (g === CB) { + for (const x in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, x)) { + continue; + } + const sym = symbol.symbols[x]; + nterm.terms[map[x]] = /** @type {FracType} */ (sym.power); + } + } else { + nterm.terms[map[symbol.value]] = /** @type {FracType} */ (symbol.power); + } + + terms.push(nterm.fill()); + nterm.updateCount(); + } + return terms; + }; + /** + * @this {NerdamerSymbolType} + * @param {string} x + * @returns {string} + */ + NerdamerSymbol.prototype.altVar = function altVar(x) { + const m = this.multiplier.toString(); + const p = this.power.toString(); + return (m === '1' ? '' : `${m}*`) + x + (p === '1' ? '' : `^${p}`); + }; + /** + * Checks to see if the symbols contain the same variables + * + * @this {NerdamerSymbolType} + * @param {NerdamerSymbolType} symbol + * @returns {boolean} + */ + NerdamerSymbol.prototype.sameVars = function sameVars(symbol) { + if (!(this.symbols || this.group === symbol.group)) { + return false; + } + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const a = this.symbols[x]; + const b = symbol.symbols[x]; + if (!b) { + return false; + } + if (a.value !== b.value) { + return false; + } + } + return true; + }; + /** + * Groups the terms in a symbol with respect to a variable For instance the symbol {a_b^2_x^2+a_b_x^2+x+6} returns + * [6,1,a_b+a_b^2] + * + * @this {NerdamerSymbolType} + * @param {string} x + * @returns {NerdamerSymbolType[]} + */ + NerdamerSymbol.prototype.groupTerms = function groupTerms(x) { + x = String(x); + /** @type {DecomposeResultType | undefined} */ + let f; + /** @type {number} */ + let p; + /** @type {NerdamerSymbolType[] | undefined} */ + let egrouped; + /** @type {NerdamerSymbolType[]} */ + const grouped = []; + this.each(e => { + if (e.group === PL) { + egrouped = e.groupTerms(x); + for (let i = 0; i < egrouped.length; i++) { + const el = egrouped[i]; + if (el) { + grouped[i] = el; + } + } + } else { + f = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(e, x, true)); + p = + /** @type {NerdamerSymbolType} */ (f.x).value === x + ? Number(/** @type {NerdamerSymbolType} */ (f.x).power) + : 0; + // Check if there's an existing value + grouped[p] = /** @type {NerdamerSymbolType} */ (_.add(grouped[p] || new NerdamerSymbol(0), f.a)); + } + }); + return grouped; + }; + /** + * Use this to collect Factors + * + * @this {NerdamerSymbolType} + * @returns {NerdamerSymbolType[]} + */ + NerdamerSymbol.prototype.collectFactors = function collectFactors() { + /** @type {NerdamerSymbolType[]} */ + const factors = []; + if (this.group === CB) { + this.each(x => { + factors.push(x.clone()); + }); + } else { + factors.push(this.clone()); + } + return factors; + }; + /** + * A container class for factors + * + * @class + * @this {Factors} + */ + function Factors() { + /** @type {Record} */ + this.factors = {}; + /** @type {number} */ + this.length = 0; + /** @type {((s: NerdamerSymbolType) => NerdamerSymbolType) | undefined} */ + this.preAdd = undefined; + /** @type {number | string | undefined} */ + this.pFactor = undefined; + } + /** + * @this {Factors} + * @returns {number} + */ + Factors.prototype.getNumberSymbolics = function getNumberSymbolics() { + let n = 0; + this.each(x => { + if (!x.isConstant(true)) { + n++; + } + }); + return n; + }; + /** + * Adds the factors to the factor object + * + * @this {Factors} + * @param {NerdamerSymbolType} s + * @returns {Factors} + */ + Factors.prototype.add = function add(s) { + if (s.equals(0)) { + return this; + } // Nothing to add + + // we don't want to carry -1 as a factor. If a factor already exists, + // then add the minus one to that factor and return. + if (s.equals(-1) && this.length > 0) { + const fo = core.Utils.firstObject(this.factors, null, true); + const newObj = /** @type {NerdamerSymbolType} */ ( + _.symfunction(core.Settings.PARENTHESIS, [fo.obj]).negate() + ); + delete this.factors[fo.key]; + this.add(newObj); + this.length--; + return this; + } + + if (s.group === CB) { + const factors = this; + if (!s.multiplier.equals(1)) { + factors.add(new NerdamerSymbol(s.multiplier)); + } + s.each(x => { + factors.add(x); + }); + } else { + if (this.preAdd) // If a preAdd function was defined call it to do prep + { + s = this.preAdd(s); + } + if (this.pFactor) // If the symbol isn't linear add back the power + { + s = /** @type {NerdamerSymbolType} */ (_.pow(s, new NerdamerSymbol(this.pFactor))); + } + + const isConstant = s.isConstant(); + if (isConstant && s.equals(1)) { + return this; + } // Don't add 1 + const v = isConstant ? s.value : s.text(); + if (v in this.factors) { + this.factors[v] = /** @type {NerdamerSymbolType} */ (_.multiply(this.factors[v], s)); + // Did the addition cancel out the existing factor? If so remove it and decrement the length + if (this.factors[v].equals(1)) { + delete this.factors[v]; + this.length--; + } + } else { + this.factors[v] = s; + this.length++; + } + } + return this; + }; + /** + * Converts the factor object to a NerdamerSymbol + * + * @this {Factors} + * @returns {NerdamerSymbolType} + */ + Factors.prototype.toSymbol = function toSymbol() { + /** @type {NerdamerSymbolType} */ + let factored = new NerdamerSymbol(1); + const factors = Object.values(this.factors).sort((a, b) => (a.group > b.group ? 1 : -1)); + + for (let i = 0, l = factors.length; i < l; i++) { + const f = factors[i]; + + // Don't wrap group S or FN + const factor = + f.power.equals(1) && f.fname !== '' /* Don't wrap it twice */ + ? _.symfunction(core.Settings.PARENTHESIS, [f]) + : f; + + factored = /** @type {NerdamerSymbolType} */ (_.multiply(factored, factor)); + } + if (factored.fname === '') { + factored = NerdamerSymbol.unwrapPARENS(factored); + } + return factored; + }; + /** + * Merges 2 factor objects into one + * + * @this {Factors} + * @param {Record} o + * @returns {Factors} + */ + Factors.prototype.merge = function merge(o) { + for (const x in o) { + if (x in this.factors) { + this.factors[x] = /** @type {NerdamerSymbolType} */ (_.multiply(this.factors[x], o[x])); + } else { + this.factors[x] = o[x]; + } + } + return this; + }; + /** + * The iterator for the factor object + * + * @this {Factors} + * @param {(factor: NerdamerSymbolType, key: string) => void} f - Callback + * @returns {Factors} + */ + Factors.prototype.each = function each(f) { + for (const x in this.factors) { + if (!Object.hasOwn(this.factors, x)) { + continue; + } + let factor = this.factors[x]; + if (factor.fname === core.Settings.PARENTHESIS && factor.isLinear()) { + factor = factor.args[0]; + } + f.call(this, factor, x); + } + return this; + }; + /** + * Return the number of factors contained in the factor object + * + * @this {Factors} + * @returns {number} + */ + Factors.prototype.count = function count() { + return keys(this.factors).length; + }; + /** + * Cleans up factors from -1 + * + * @this {Factors} + * @returns {void} + */ + Factors.prototype.clean = function clean() { + try { + const h = core.Settings.CONST_HASH; + if (this.factors[h].lessThan(0)) { + if (this.factors[h].equals(-1)) { + delete this.factors[h]; + } else { + this.factors[h].negate(); + } + this.each(x => { + x.negate(); + }); + } + } catch (e) { + if (/** @type {Error} */ (e).message === 'timeout') { + throw e; + } + } + }; + /** + * @this {Factors} + * @returns {string} + */ + Factors.prototype.toString = function toString() { + return this.toSymbol().toString(); + }; + + /** + * A wrapper for performing multivariate division + * + * @class + * @this {MVTerm} + * @param {FracType} coeff + * @param {FracType[]} [terms] + * @param {Record & { length: number }} [map] + */ + function MVTerm(coeff, terms, map) { + /** @type {FracType[]} */ + this.terms = terms || []; + /** @type {FracType} */ + this.coeff = coeff; + /** @type {(Record & { length: number }) | undefined} */ + this.map = map; // Careful! all maps are the same object + /** @type {FracType} */ + this.sum = new Frac(0); + /** @type {string | undefined} */ + this.image = undefined; + /** @type {Record | undefined} */ + this.revMap = undefined; + /** @type {number | undefined} */ + this.count = undefined; + } + /** + * @this {MVTerm} + * @returns {MVTerm} + */ + MVTerm.prototype.updateCount = function updateCount() { + this.count ||= 0; + for (let i = 0; i < this.terms.length; i++) { + if (!this.terms[i].equals(0)) { + this.count++; + } + } + return this; + }; + /** + * @this {MVTerm} + * @returns {string} + */ + MVTerm.prototype.getVars = function getVars() { + /** @type {string[]} */ + const vars = []; + for (let i = 0; i < this.terms.length; i++) { + const term = this.terms[i]; + this.getRevMap(); + if (!term.equals(0) && this.revMap) { + vars.push(this.revMap[i]); + } + } + return vars.join(' '); + }; + /** + * @this {MVTerm} + * @returns {number} + */ + MVTerm.prototype.len = function len() { + if (typeof this.count === 'undefined') { + this.updateCount(); + } + return this.count || 0; + }; + /** + * @this {MVTerm} + * @param {Record} [revMap] + * @returns {NerdamerSymbolType} + */ + MVTerm.prototype.toSymbol = function toSymbol(revMap) { + revMap ||= this.getRevMap(); + /** @type {NerdamerSymbolType} */ + let symbol = new NerdamerSymbol(this.coeff); + for (let i = 0; i < this.terms.length; i++) { + const v = revMap[i]; + const t = this.terms[i]; + if (t.equals(0) || v === CONST_HASH) { + continue; + } + const mapped = new NerdamerSymbol(v); + mapped.power = t; + symbol = /** @type {NerdamerSymbolType} */ (_.multiply(symbol, mapped)); + } + return symbol; + }; + /** + * @this {MVTerm} + * @returns {Record} + */ + MVTerm.prototype.getRevMap = function getRevMap() { + if (this.revMap) { + return this.revMap; + } + /** @type {Record} */ + const o = {}; + if (this.map) { + for (const x in this.map) { + if (!Object.hasOwn(this.map, x)) { + continue; + } + o[this.map[x]] = x; + } + } + this.revMap = o; + return o; + }; + /** + * @this {MVTerm} + * @returns {MVTerm} + */ + MVTerm.prototype.generateImage = function generateImage() { + this.image = this.terms.join(' '); + return this; + }; + /** + * @this {MVTerm} + * @returns {string} + */ + MVTerm.prototype.getImg = function getImg() { + if (!this.image) { + this.generateImage(); + } + return this.image || ''; + }; + /** + * @this {MVTerm} + * @returns {MVTerm} + */ + MVTerm.prototype.fill = function fill() { + const l = this.map ? this.map.length : 0; + for (let i = 0; i < l; i++) { + if (typeof this.terms[i] === 'undefined') { + this.terms[i] = new Frac(0); + } else { + this.sum = this.sum.add(this.terms[i]); + } + } + return this; + }; + /** + * @this {MVTerm} + * @param {MVTerm} mvterm + * @returns {MVTerm} + */ + MVTerm.prototype.divide = function divide(mvterm) { + const c = this.coeff.divide(mvterm.coeff); + const l = this.terms.length; + /** @type {MVTerm} */ + const newMvterm = new MVTerm(c, [], this.map); + for (let i = 0; i < l; i++) { + newMvterm.terms[i] = this.terms[i].subtract(mvterm.terms[i]); + newMvterm.sum = newMvterm.sum.add(newMvterm.terms[i]); + } + return newMvterm; + }; + /** + * @this {MVTerm} + * @param {MVTerm} mvterm + * @returns {MVTerm} + */ + MVTerm.prototype.multiply = function multiply(mvterm) { + const c = this.coeff.multiply(mvterm.coeff); + const l = this.terms.length; + /** @type {MVTerm} */ + const newMvterm = new MVTerm(c, [], this.map); + for (let i = 0; i < l; i++) { + newMvterm.terms[i] = this.terms[i].add(mvterm.terms[i]); + newMvterm.sum = newMvterm.sum.add(newMvterm.terms[i]); + } + return newMvterm; + }; + /** + * @this {MVTerm} + * @returns {boolean} + */ + MVTerm.prototype.isZero = function isZero() { + return this.coeff.equals(0); + }; + /** + * @this {MVTerm} + * @returns {string} + */ + MVTerm.prototype.toString = function toString() { + return `{ coeff: ${this.coeff.toString()}, terms: [${this.terms.join( + ',' + )}]: sum: ${this.sum.toString()}, count: ${this.count}}`; + }; + + /** + * @param {string[]} arr + * @returns {Record & { length: number }} + */ + core.Utils.toMapObj = function toMapObj(arr) { + let c = 0; + /** @type {Record & { length: number }} */ + const o = /** @type {Record & { length: number }} */ ({ length: 0 }); + for (let i = 0; i < arr.length; i++) { + const v = arr[i]; + if (typeof o[v] === 'undefined') { + o[v] = c; + c++; + } + } + o.length = c; + return o; + }; + /** + * @template T + * @param {T} v + * @param {number} n + * @param {new (v: T) => T} [Clss] + * @returns {T[]} + */ + core.Utils.filledArray = function filledArray(v, n, Clss) { + const a = []; + while (n--) { + a[n] = Clss ? new Clss(v) : v; + } + return a; + }; + /** + * @param {number[]} arr + * @returns {number} + */ + core.Utils.arrSum = function arrSum(arr) { + let sum = 0; + const l = arr.length; + for (let i = 0; i < l; i++) { + sum += arr[i]; + } + return sum; + }; + /** + * Determines if 2 arrays have intersecting elements. + * + * @template T + * @param {T[]} a + * @param {T[]} b + * @returns {boolean} True if a and b have intersecting elements. + */ + core.Utils.haveIntersection = function haveIntersection(a, b) { + if (b.length > a.length) { + [a, b] = [b, a]; // IndexOf to loop over shorter + } + return a.some(e => b.indexOf(e) > -1); + }; + /** + * Substitutes out functions as variables so they can be used in regular algorithms + * + * @param {NerdamerSymbolType} symbol + * @param {Record} [map] + * @returns {string} The expression string + */ + core.Utils.subFunctions = function subFunctions(symbol, map) { + map ||= {}; + /** @type {string[]} */ + const subbed = []; + const vars = new Set(variables(symbol)); + symbol.each(x => { + if (x.group === FN || x.previousGroup === FN) { + // We need a new variable name so why not use one of the existing + const val = core.Utils.text(x, 'hash'); + const tvar = map[val]; + if (tvar) { + subbed.push(x.altVar(tvar)); + } else { + // Generate a unique enough name + // GM make sure it's not the name of an existing variable + let i = 0; + let t; + do { + t = x.fname + keys(map).length + (i > 0 ? String(i) : ''); + i++; + } while (vars.has(t)); + map[val] = t; + subbed.push(x.altVar(t)); + } + } else if (x.group === CB || x.group === PL || x.group === CP) { + subbed.push(core.Utils.subFunctions(x, map)); + } else { + subbed.push(x.text()); + } + }); + if (symbol.group === CP || symbol.group === PL) { + return symbol.altVar(core.Utils.inBrackets(subbed.join('+'))); + } + if (symbol.group === CB) { + return symbol.altVar(core.Utils.inBrackets(subbed.join('*'))); + } + return symbol.text(); + }; + /** + * @param {Record} map + * @returns {Record} + */ + core.Utils.getFunctionsSubs = function getFunctionsSubs(map) { + /** @type {Record} */ + const subs = {}; + // Prepare substitutions + for (const x in map) { + if (!Object.hasOwn(map, x)) { + continue; + } + subs[map[x]] = _.parse(x); + } + return subs; + }; + + /** @type {AlgebraModuleType} */ + const __ = (core.Algebra = { + version: '1.4.6', + /** + * @param {NerdamerSymbolType | Array} symbol + * @param {number} [decp] + * @returns {(string | number)[]} + */ + proots(symbol, decp) { + // The roots will be rounded up to 7 decimal places. + // if this causes trouble you can explicitly pass in a different number of places + // rarr for polynomial of power n is of format [n, coeff x^n, coeff x^(n-1), ..., coeff x^0] + decp ||= 7; + const zeros = 0; + /** @type {(string | number)[]} */ + const knownRoots = []; + /** + * @param {FracType[]} rarr + * @param {(string | number)[]} powers + * @param {number} max + * @returns {(string | number)[]} + */ + const getRoots = function (rarr, powers, max) { + const roots = calcroots(rarr, powers, max).concat(knownRoots); + for (let i = 0; i < zeros; i++) { + roots.unshift(0); + } + return /** @type {string[]} */ (roots); + }; + + if (core.Utils.isSymbol(symbol) && /** @type {NerdamerSymbolType} */ (symbol).isPoly()) { + let sym = /** @type {NerdamerSymbolType} */ (symbol); + sym.distributeMultiplier(); + // Make it so the symbol has a constants as the lowest term + if (sym.group === PL) { + const lowestPow = core.Utils.arrayMin( + /** @type {number[]} */ (/** @type {unknown} */ (keys(sym.symbols))) + ); + const lowestSymbol = sym.symbols[lowestPow].clone().toUnitMultiplier(); + sym = /** @type {NerdamerSymbolType} */ (_.expand(_.divide(sym, lowestSymbol))); + knownRoots.push(0); // Add zero since this is a known root + } + if (sym.group === core.groups.S) { + return [/** @type {string} */ ('0')]; + } + if (sym.group === core.groups.PL) { + const powers = keys(sym.symbols); + const minpower = core.Utils.arrayMin(/** @type {number[]} */ (/** @type {unknown} */ (powers))); + sym = /** @type {NerdamerSymbolType} */ ( + core.PARSER.divide(sym, core.PARSER.parse(`${sym.value}^${minpower}`)) + ); + } + + const variable = keys(sym.symbols).sort().pop(); + const subSym = sym.group === core.groups.PL ? sym.symbols : sym.symbols[variable || '']; + const g = subSym.group; + const powers = g === S ? [/** @type {FracType} */ (subSym.power).toDecimal()] : keys(subSym.symbols); + /** @type {(FracType | number)[]} */ + const rarr = []; + const max = core.Utils.arrayMax(/** @type {number[]} */ (/** @type {unknown} */ (powers))); // Maximum power and degree of polynomial to be solved + + // Prepare the data + for (let i = 1; i <= max; i++) { + /** @type {FracType | number} */ + let c = 0; // If there is no power then the hole must be filled with a zero + if (powers.indexOf(`${i}`) !== -1) { + if (g === S) { + c = /** @type {FracType} */ (subSym.multiplier); + } else { + c = /** @type {FracType} */ (subSym.symbols[i].multiplier); + } + } + // Insert the coeffient but from the front + rarr.unshift(c); + } + + rarr.push(/** @type {NerdamerSymbolType} */ (symbol).symbols[CONST_HASH].multiplier); + + if (sym.group === S) { + rarr[0] = sym.multiplier; + } // The symbol maybe of group CP with one variable + + return /** @type {(string | number)[]} */ (getRoots(/** @type {FracType[]} */ (rarr), powers, max)); + } + if (core.Utils.isArray(symbol)) { + const parr = symbol; + const rarr = []; + const powers = []; + let lastPower = 0; + for (let i = 0; i < parr.length; i++) { + const coeff = parr[i][0]; + const pow = parr[i][1]; + const d = pow - lastPower - 1; + // Insert the zeros + for (let j = 0; j < d; j++) { + rarr.unshift(0); + } + + rarr.unshift(coeff); + if (pow !== 0) { + powers.push(pow); + } + lastPower = pow; + } + const max = Math.max.apply(undefined, powers); + + return getRoots(rarr, powers, max); + } + throw new core.exceptions.NerdamerTypeError('Cannot calculate roots. NerdamerSymbol must be a polynomial!'); + + function calcroots(coeffArr, powArr, maxPow) { + const MAXDEGREE = 100; // Degree of largest polynomial accepted by this script. + let i; + + // Make a clone of the coefficients before appending the max power + const p = coeffArr.slice(0); + + // Divide the string up into its individual entries, which--presumably--are separated by whitespace + coeffArr.unshift(maxPow); + + if (maxPow > MAXDEGREE) { + throw new core.exceptions.ValueLimitExceededError( + `This utility accepts polynomials of degree up to ${MAXDEGREE}. ` + ); + } + + const zeroi = []; // Vector of imaginary components of roots + const degreePar = {}; // DegreePar is a dummy variable for passing the parameter POLYDEGREE by reference + degreePar.Degree = maxPow; + + for (i = 0; i < maxPow; i++) { + zeroi.push(0); + } + const zeror = zeroi.slice(0); // Vector of real components of roots + + // Find the roots + // --> Begin Jenkins-Traub + + /* + * A verbatim copy of Mr. David Binner's Jenkins-Traub port + */ + function quadSdAk1(NN, u, v, poly, q, iPar) { + // Divides poly by the quadratic 1, u, v placing the quotient in q and the remainder in a, b + // iPar is a dummy variable for passing in the two parameters--a and b--by reference + q[0] = iPar.b = poly[0]; + q[1] = iPar.a = -(u * iPar.b) + poly[1]; + + for (let idx = 2; idx < NN; idx++) { + q[idx] = -(u * iPar.a + v * iPar.b) + poly[idx]; + iPar.b = iPar.a; + iPar.a = q[idx]; + } + } + + function calcScAk1(DBL_EPSILON, degree, a, b, iPar, K, u, v, qk) { + // This routine calculates scalar quantities used to compute the next K polynomial and + // new estimates of the quadratic coefficients. + // calcSC - integer variable set here indicating how the calculations are normalized + // to avoid overflow. + // iPar is a dummy variable for passing in the nine parameters--a1, a3, a7, c, d, e, f, g, and h --by reference + + // sdPar is a dummy variable for passing the two parameters--c and d--into quadSdAk1 by reference + const sdPar = {}; + // TYPE = 3 indicates the quadratic is almost a factor of K + let dumFlag = 3; + + // Synthetic division of K by the quadratic 1, u, v + sdPar.b = sdPar.a = 0.0; + quadSdAk1(degree, u, v, K, qk, sdPar); + iPar.c = sdPar.a; + iPar.d = sdPar.b; + + if (Math.abs(iPar.c) <= 100.0 * DBL_EPSILON * Math.abs(K[degree - 1])) { + if (Math.abs(iPar.d) <= 100.0 * DBL_EPSILON * Math.abs(K[degree - 2])) { + return dumFlag; + } + } + + iPar.h = v * b; + if (Math.abs(iPar.d) >= Math.abs(iPar.c)) { + // TYPE = 2 indicates that all formulas are divided by d + dumFlag = 2; + iPar.e = a / iPar.d; + iPar.f = iPar.c / iPar.d; + iPar.g = u * b; + iPar.a3 = iPar.e * (iPar.g + a) + iPar.h * (b / iPar.d); + iPar.a1 = -a + iPar.f * b; + iPar.a7 = iPar.h + (iPar.f + u) * a; + } else { + // TYPE = 1 indicates that all formulas are divided by c; + dumFlag = 1; + iPar.e = a / iPar.c; + iPar.f = iPar.d / iPar.c; + iPar.g = iPar.e * u; + iPar.a3 = iPar.e * a + (iPar.g + iPar.h / iPar.c) * b; + iPar.a1 = -(a * (iPar.d / iPar.c)) + b; + iPar.a7 = iPar.g * iPar.d + iPar.h * iPar.f + a; + } + return dumFlag; + } + + function nextKAk1(DBL_EPSILON, degree, tFlag, a, b, iPar, K, qk, qp) { + // Computes the next K polynomials using the scalars computed in calcScAk1 + // iPar is a dummy variable for passing in three parameters--a1, a3, and a7 + if (tFlag === 3) { + // Use unscaled form of the recurrence + K[1] = K[0] = 0.0; + for (let idx = 2; idx < degree; idx++) { + K[idx] = qk[idx - 2]; + } + return; + } + + const temp = tFlag === 1 ? b : a; + if (Math.abs(iPar.a1) > 10.0 * DBL_EPSILON * Math.abs(temp)) { + // Use scaled form of the recurrence + iPar.a7 /= iPar.a1; + iPar.a3 /= iPar.a1; + K[0] = qp[0]; + K[1] = -(qp[0] * iPar.a7) + qp[1]; + for (let idx = 2; idx < degree; idx++) { + K[idx] = -(qp[idx - 1] * iPar.a7) + qk[idx - 2] * iPar.a3 + qp[idx]; + } + } else { + // If a1 is nearly zero, then use a special form of the recurrence + K[0] = 0.0; + K[1] = -(qp[0] * iPar.a7); + for (let idx = 2; idx < degree; idx++) { + K[idx] = -(qp[idx - 1] * iPar.a7) + qk[idx - 2] * iPar.a3; + } + } + } + + function newestAk1(tFlag, iPar, a, a1, a3, a7, b, c, d, f, g, h, u, v, K, degree, poly) { + // Compute new estimates of the quadratic coefficients using the scalars computed in calcScAk1 + // iPar is a dummy variable for passing in the two parameters--uu and vv--by reference + // iPar.a = uu, iPar.b = vv + + let a4; + let a5; + let b1; + let b2; + let c1; + let c2; + let c3; + let c4; + let temp; + iPar.b = iPar.a = 0.0; // The quadratic is zeroed + + if (tFlag === 3) { + // No action needed when tFlag is 3 + } else { + if (tFlag === 2) { + a4 = (a + g) * f + h; + a5 = (f + u) * c + v * d; + } else { + a4 = a + u * b + h * f; + a5 = c + (u + v * f) * d; + } + + // Evaluate new quadratic coefficients + b1 = -(K[degree - 1] / poly[degree]); + b2 = -(K[degree - 2] + b1 * poly[degree - 1]) / poly[degree]; + c1 = v * b2 * a1; + c2 = b1 * a7; + c3 = b1 * b1 * a3; + c4 = -(c2 + c3) + c1; + temp = -c4 + a5 + b1 * a4; + if (temp !== 0.0) { + iPar.a = -((u * (c3 + c2) + v * (b1 * a1 + b2 * a7)) / temp) + u; + iPar.b = v * (1.0 + c4 / temp); + } + } + } + + function quadAk1(a, b1, c, iPar) { + // Calculates the zeros of the quadratic a*Z^2 + b1*Z + c + // The quadratic formula, modified to avoid overflow, is used to find the larger zero if the + // zeros are real and both zeros are complex. The smaller real zero is found directly from + // the product of the zeros c/a. + + // iPar is a dummy variable for passing in the four parameters--sr, si, lr, and li--by reference + + let d; + let e; + iPar.sr = iPar.si = iPar.lr = iPar.li = 0.0; + + if (a === 0) { + iPar.sr = b1 === 0 ? iPar.sr : -(c / b1); + return; + } + if (c === 0) { + iPar.lr = -(b1 / a); + return; + } + + // Compute discriminant avoiding overflow + const b = b1 / 2.0; + if (Math.abs(b) < Math.abs(c)) { + e = c >= 0 ? a : -a; + e = -e + b * (b / Math.abs(c)); + d = Math.sqrt(Math.abs(e)) * Math.sqrt(Math.abs(c)); + } else { + e = -((a / b) * (c / b)) + 1.0; + d = Math.sqrt(Math.abs(e)) * Math.abs(b); + } + + if (e >= 0) { + // Real zeros + d = b >= 0 ? -d : d; + iPar.lr = (-b + d) / a; + iPar.sr = iPar.lr === 0 ? iPar.sr : c / iPar.lr / a; + } else { + // Complex conjugate zeros + iPar.lr = iPar.sr = -(b / a); + iPar.si = Math.abs(d / a); + iPar.li = -iPar.si; + } + } + + function quadItAk1(DBL_EPSILON, degree, iPar, uu, vv, qp, NN, sdPar, poly, qk, calcPar, K) { + // Variable-shift K-polynomial iteration for a quadratic factor converges only if the + // zeros are equimodular or nearly so. + // iPar is a dummy variable for passing in the five parameters--NZ, lzi, lzr, szi, and szr--by reference + // sdPar is a dummy variable for passing the two parameters--a and b--in by reference + // calcPar is a dummy variable for passing the nine parameters--a1, a3, a7, c, d, e, f, g, and h --in by reference + + // qPar is a dummy variable for passing the four parameters--szr, szi, lzr, and lzi--into quadAk1 by reference + const qPar = {}; + let ee; + let mp; + let omp; + /** @type {number} */ + let relstp = 0; + let t; + let u; + let ui; + let v; + let vi; + let zm; + let idx; + let j = 0; + let tFlag; + let triedFlag = 0; // Integer variables + + iPar.NZ = 0; // Number of zeros found + u = uu; // Uu and vv are coefficients of the starting quadratic + v = vv; + + do { + qPar.li = qPar.lr = qPar.si = qPar.sr = 0.0; + quadAk1(1.0, u, v, qPar); + iPar.szr = qPar.sr; + iPar.szi = qPar.si; + iPar.lzr = qPar.lr; + iPar.lzi = qPar.li; + + // Return if roots of the quadratic are real and not close to multiple or nearly + // equal and of opposite sign. + if (Math.abs(Math.abs(iPar.szr) - Math.abs(iPar.lzr)) > 0.01 * Math.abs(iPar.lzr)) { + break; + } + + // Evaluate polynomial by quadratic synthetic division + + quadSdAk1(NN, u, v, poly, qp, sdPar); + + mp = Math.abs(-(iPar.szr * sdPar.b) + sdPar.a) + Math.abs(iPar.szi * sdPar.b); + + // Compute a rigorous bound on the rounding error in evaluating p + + zm = Math.sqrt(Math.abs(v)); + ee = 2.0 * Math.abs(qp[0]); + t = -(iPar.szr * sdPar.b); + + for (idx = 1; idx < degree; idx++) { + ee = ee * zm + Math.abs(qp[idx]); + } + + ee = ee * zm + Math.abs(t + sdPar.a); + ee = + (9.0 * ee + 2.0 * Math.abs(t) - 7.0 * (Math.abs(sdPar.a + t) + zm * Math.abs(sdPar.b))) * + DBL_EPSILON; + + // Iteration has converged sufficiently if the polynomial value is less than 20 times this bound + if (mp <= 20.0 * ee) { + iPar.NZ = 2; + break; + } + + j++; + // Stop iteration after 20 steps + if (j > 20) { + break; + } + if (j >= 2) { + if (relstp <= 0.01 && mp >= omp && !triedFlag) { + // A cluster appears to be stalling the convergence. Five fixed shift + // steps are taken with a u, v close to the cluster. + relstp = relstp < DBL_EPSILON ? Math.sqrt(DBL_EPSILON) : Math.sqrt(relstp); + u -= u * relstp; + v += v * relstp; + + quadSdAk1(NN, u, v, poly, qp, sdPar); + for (idx = 0; idx < 5; idx++) { + tFlag = calcScAk1(DBL_EPSILON, degree, sdPar.a, sdPar.b, calcPar, K, u, v, qk); + nextKAk1(DBL_EPSILON, degree, tFlag, sdPar.a, sdPar.b, calcPar, K, qk, qp); + } + + triedFlag = 1; + j = 0; + } + } + omp = mp; + + // Calculate next K polynomial and new u and v + tFlag = calcScAk1(DBL_EPSILON, degree, sdPar.a, sdPar.b, calcPar, K, u, v, qk); + nextKAk1(DBL_EPSILON, degree, tFlag, sdPar.a, sdPar.b, calcPar, K, qk, qp); + tFlag = calcScAk1(DBL_EPSILON, degree, sdPar.a, sdPar.b, calcPar, K, u, v, qk); + newestAk1( + tFlag, + sdPar, + sdPar.a, + calcPar.a1, + calcPar.a3, + calcPar.a7, + sdPar.b, + calcPar.c, + calcPar.d, + calcPar.f, + calcPar.g, + calcPar.h, + u, + v, + K, + degree, + poly + ); + ui = sdPar.a; + vi = sdPar.b; + + // If vi is zero, the iteration is not converging + if (vi !== 0) { + relstp = Math.abs((-v + vi) / vi); + u = ui; + v = vi; + } + } while (vi !== 0); + } + + function realItAk1(DBL_EPSILON, iPar, sdPar, degree, poly, NN, qp, K, qk) { + // Variable-shift H-polynomial iteration for a real zero + // sss - starting iterate = sdPar.a + // NZ - number of zeros found = iPar.NZ + // dumFlag - flag to indicate a pair of zeros near real axis, returned to iFlag + + let ee; + let kv; + let mp; + let ms; + let omp; + let pv; + let s; + let t; + let dumFlag; + let idx; + let j; + const nm1 = degree - 1; // Integer variables + + iPar.NZ = j = dumFlag = 0; + s = sdPar.a; + + for (;;) { + pv = poly[0]; + + // Evaluate p at s + qp[0] = pv; + for (idx = 1; idx < NN; idx++) { + qp[idx] = pv = pv * s + poly[idx]; + } + mp = Math.abs(pv); + + // Compute a rigorous bound on the error in evaluating p + ms = Math.abs(s); + ee = 0.5 * Math.abs(qp[0]); + for (idx = 1; idx < NN; idx++) { + ee = ee * ms + Math.abs(qp[idx]); + } + + // Iteration has converged sufficiently if the polynomial value is less than + // 20 times this bound + if (mp <= 20.0 * DBL_EPSILON * (2.0 * ee - mp)) { + iPar.NZ = 1; + iPar.szr = s; + iPar.szi = 0.0; + break; + } + j++; + // Stop iteration after 10 steps + if (j > 10) { + break; + } + + if (j >= 2) { + if (Math.abs(t) <= 0.001 * Math.abs(-t + s) && mp > omp) { + // A cluster of zeros near the real axis has been encountered. + // Return with iFlag set to initiate a quadratic iteration. + dumFlag = 1; + iPar.a = s; + break; + } // End if ((fabs(t) <= 0.001*fabs(s - t)) && (mp > omp)) + } // End if (j >= 2) + + // Return if the polynomial value has increased significantly + omp = mp; + + // Compute t, the next polynomial and the new iterate + qk[0] = kv = K[0]; + for (idx = 1; idx < degree; idx++) { + qk[idx] = kv = kv * s + K[idx]; + } + + if (Math.abs(kv) > Math.abs(K[nm1]) * 10.0 * DBL_EPSILON) { + // Use the scaled form of the recurrence if the value of K at s is non-zero + t = -(pv / kv); + K[0] = qp[0]; + for (idx = 1; idx < degree; idx++) { + K[idx] = t * qk[idx - 1] + qp[idx]; + } + } else { + // Use unscaled form + K[0] = 0.0; + for (idx = 1; idx < degree; idx++) { + K[idx] = qk[idx - 1]; + } + } + + kv = K[0]; + for (idx = 1; idx < degree; idx++) { + kv = kv * s + K[idx]; + } + t = Math.abs(kv) > Math.abs(K[nm1]) * 10.0 * DBL_EPSILON ? -(pv / kv) : 0.0; + s += t; + } + return dumFlag; + } + + function fxshfrAk1(DBL_EPSILON, MDP1, L2, sr, v, K, degree, poly, NN, qp, u, iPar) { + // Computes up to L2 fixed shift K-polynomials, testing for convergence in the linear or + // quadratic case. Initiates one of the variable shift iterations and returns with the + // number of zeros found. + // L2 limit of fixed shift steps + // iPar is a dummy variable for passing in the five parameters--NZ, lzi, lzr, szi, and szr--by reference + // NZ number of zeros found + const sdPar = {}; // SdPar is a dummy variable for passing the two parameters--a and b--into quadSdAk1 by reference + const calcPar = {}; + // CalcPar is a dummy variable for passing the nine parameters--a1, a3, a7, c, d, e, f, g, and h --into calcScAk1 by reference + + const qk = new Array(MDP1); + const svk = new Array(MDP1); + let a; + let b; + let betas; + let betav; + let oss; + let ots; + let otv; + let ovv; + let s; + let ss; + let ts; + let tss; + let tv; + let tvv; + let ui; + let vi; + let vv; + let fflag; + let idx; + let iFlag = 1; + let j; + let spass; + let stry; + let tFlag; + let vpass; + let vtry; // Integer variables + + iPar.NZ = 0; + betav = betas = 0.25; + oss = sr; + ovv = v; + + // Evaluate polynomial by synthetic division + sdPar.b = sdPar.a = 0.0; + quadSdAk1(NN, u, v, poly, qp, sdPar); + a = sdPar.a; + b = sdPar.b; + calcPar.h = + calcPar.g = + calcPar.f = + calcPar.e = + calcPar.d = + calcPar.c = + calcPar.a7 = + calcPar.a3 = + calcPar.a1 = + 0.0; + tFlag = calcScAk1(DBL_EPSILON, degree, a, b, calcPar, K, u, v, qk); + + for (j = 0; j < L2; j++) { + fflag = 1; + + // Calculate next K polynomial and estimate v + nextKAk1(DBL_EPSILON, degree, tFlag, a, b, calcPar, K, qk, qp); + tFlag = calcScAk1(DBL_EPSILON, degree, a, b, calcPar, K, u, v, qk); + + // Use sdPar for passing in uu and vv instead of defining a brand-new variable. + // sdPar.a = ui, sdPar.b = vi + newestAk1( + tFlag, + sdPar, + a, + calcPar.a1, + calcPar.a3, + calcPar.a7, + b, + calcPar.c, + calcPar.d, + calcPar.f, + calcPar.g, + calcPar.h, + u, + v, + K, + degree, + poly + ); + ui = sdPar.a; + vv = vi = sdPar.b; + + // Estimate s + ss = K[degree - 1] === 0.0 ? 0.0 : -(poly[degree] / K[degree - 1]); + ts = tv = 1.0; + + if (j !== 0 && tFlag !== 3) { + // Compute relative measures of convergence of s and v sequences + tv = vv === 0.0 ? tv : Math.abs((vv - ovv) / vv); + ts = ss === 0.0 ? ts : Math.abs((ss - oss) / ss); + + // If decreasing, multiply the two most recent convergence measures + tvv = tv < otv ? tv * otv : 1.0; + tss = ts < ots ? ts * ots : 1.0; + + // Compare with convergence criteria + vpass = tvv < betav ? 1 : 0; + spass = tss < betas ? 1 : 0; + + if (spass || vpass) { + // At least one sequence has passed the convergence test. + // Store variables before iterating + + for (idx = 0; idx < degree; idx++) { + svk[idx] = K[idx]; + } + s = ss; + + // Choose iteration according to the fastest converging sequence + + stry = vtry = 0; + + for (;;) { + if (fflag && (fflag = 0) === 0 && spass && (!vpass || tss < tvv)) { + // Do nothing. Provides a quick "short circuit". + } else { + quadItAk1( + DBL_EPSILON, + degree, + iPar, + ui, + vi, + qp, + NN, + sdPar, + poly, + qk, + calcPar, + K + ); + a = sdPar.a; + b = sdPar.b; + + if (iPar.NZ > 0) { + return; + } + + // Quadratic iteration has failed. Flag that it has been tried and decrease the + // convergence criterion + iFlag = vtry = 1; + betav *= 0.25; + + // Try linear iteration if it has not been tried and the s sequence is converging + if (stry || !spass) { + iFlag = 0; + } else { + for (idx = 0; idx < degree; idx++) { + K[idx] = svk[idx]; + } + } + } + // Fflag = 0; + if (iFlag !== 0) { + // Use sdPar for passing in s instead of defining a brand-new variable. + // sdPar.a = s + sdPar.a = s; + iFlag = realItAk1(DBL_EPSILON, iPar, sdPar, degree, poly, NN, qp, K, qk); + s = sdPar.a; + + if (iPar.NZ > 0) { + return; + } + + // Linear iteration has failed. Flag that it has been tried and decrease the + // convergence criterion + stry = 1; + betas *= 0.25; + + if (iFlag !== 0) { + // If linear iteration signals an almost double real zero, attempt quadratic iteration + ui = -(s + s); + vi = s * s; + continue; + } + } + + // Restore variables + for (idx = 0; idx < degree; idx++) { + K[idx] = svk[idx]; + } + + // Try quadratic iteration if it has not been tried and the v sequence is converging + if (!vpass || vtry) { + break; + } // Break out of infinite for loop + } + + // Re-compute qp and scalar values to continue the second stage + + quadSdAk1(NN, u, v, poly, qp, sdPar); + a = sdPar.a; + b = sdPar.b; + + tFlag = calcScAk1(DBL_EPSILON, degree, a, b, calcPar, K, u, v, qk); + } + } + ovv = vv; + oss = ss; + otv = tv; + ots = ts; + } + } + + function rpSolve(degPar, poly, zeroReal, zeroImag) { + let degree = degPar.Degree; + const RADFAC = Math.PI / 180; // Degrees-to-radians conversion factor = PI/180 + const LB2 = Math.LN2; // Dummy variable to avoid re-calculating this value in loop below + const MDP1 = degPar.Degree + 1; + const K = new Array(MDP1); + const pt = new Array(MDP1); + const qp = new Array(MDP1); + const temp = new Array(MDP1); + // QPar is a dummy variable for passing the four parameters--sr, si, lr, and li--by reference + const qPar = {}; + // FxshfrPar is a dummy variable for passing parameters by reference : NZ, lzi, lzr, szi, szr); + const fxshfrPar = {}; + let bnd; + let DBL_EPSILON; + let df; + let dx; + let factor; + let ff; + let moduliMax; + let moduliMin; + let sc; + let x; + let xm; + let aa; + let bb; + let cc; + let sr; + let t; + let u; + let xxx; + let j; + let jj; + let l; + let NM1; + let NN; + let zerok; // Integer variables + + // Calculate the machine epsilon and store in the variable DBL_EPSILON. + // To calculate this value, just use existing variables rather than create new ones that will be used only for this code block + aa = 1.0; + do { + DBL_EPSILON = aa; + aa /= 2; + bb = 1.0 + aa; + } while (bb > 1.0); + + const LO = Number.MIN_VALUE / DBL_EPSILON; + const cosr = Math.cos(94.0 * RADFAC); // = -0.069756474 + const sinr = Math.sin(94.0 * RADFAC); // = 0.99756405 + let xx = Math.sqrt(0.5); // = 0.70710678 + let yy = -xx; + + fxshfrPar.NZ = j = 0; + fxshfrPar.szr = fxshfrPar.szi = fxshfrPar.lzr = fxshfrPar.lzi = 0.0; + + // Remove zeros at the origin, if any + while (poly[degree] === 0) { + zeroReal[j] = zeroImag[j] = 0; + degree--; + j++; + } + NN = degree + 1; + + // >>>>> Begin Main Loop <<<<< + while (degree >= 1) { + // Main loop + // Start the algorithm for one zero + if (degree <= 2) { + // Calculate the final zero or pair of zeros + if (degree < 2) { + zeroReal[degPar.Degree - 1] = -(poly[1] / poly[0]); + zeroImag[degPar.Degree - 1] = 0; + } else { + qPar.li = qPar.lr = qPar.si = qPar.sr = 0.0; + quadAk1(poly[0], poly[1], poly[2], qPar); + zeroReal[degPar.Degree - 2] = qPar.sr; + zeroImag[degPar.Degree - 2] = qPar.si; + zeroReal[degPar.Degree - 1] = qPar.lr; + zeroImag[degPar.Degree - 1] = qPar.li; + } + break; + } + + // Find the largest and smallest moduli of the coefficients + moduliMax = 0.0; + moduliMin = Number.MAX_VALUE; + + for (i = 0; i < NN; i++) { + x = Math.abs(poly[i]); + if (x > moduliMax) { + moduliMax = x; + } + if (x !== 0 && x < moduliMin) { + moduliMin = x; + } + } + + // Scale if there are large or very small coefficients + // Computes a scale factor to multiply the coefficients of the polynomial. The scaling + // is done to avoid overflow and to avoid undetected underflow interfering with the + // convergence criterion. + // The factor is a power of the base. + sc = LO / moduliMin; + + if ((sc <= 1.0 && moduliMax >= 10) || (sc > 1.0 && Number.MAX_VALUE / sc >= moduliMax)) { + sc = sc === 0 ? Number.MIN_VALUE : sc; + l = Math.floor(Math.log(sc) / LB2 + 0.5); + factor = 2.0 ** l; + if (factor !== 1.0) { + for (i = 0; i < NN; i++) { + poly[i] *= factor; + } + } + } + + // Compute lower bound on moduli of zeros + for (let idx = 0; idx < NN; idx++) { + pt[idx] = Math.abs(poly[idx]); + } + pt[degree] = -pt[degree]; + NM1 = degree - 1; + + // Compute upper estimate of bound + x = Math.exp((Math.log(-pt[degree]) - Math.log(pt[0])) / degree); + + if (pt[NM1] !== 0) { + // If Newton step at the origin is better, use it + xm = -pt[degree] / pt[NM1]; + x = xm < x ? xm : x; + } + + // Chop the interval (0, x) until ff <= 0 + xm = x; + do { + x = xm; + xm = 0.1 * x; + ff = pt[0]; + for (let idx = 1; idx < NN; idx++) { + ff = ff * xm + pt[idx]; + } + } while (ff > 0); // End do-while loop + + dx = x; + // Do Newton iteration until x converges to two decimal places + + do { + df = ff = pt[0]; + for (let idx = 1; idx < degree; idx++) { + ff = x * ff + pt[idx]; + df = x * df + ff; + } // End for i + ff = x * ff + pt[degree]; + dx = ff / df; + x -= dx; + } while (Math.abs(dx / x) > 0.005); // End do-while loop + + bnd = x; + + // Compute the derivative as the initial K polynomial and do 5 steps with no shift + for (let idx = 1; idx < degree; idx++) { + K[idx] = ((degree - idx) * poly[idx]) / degree; + } + K[0] = poly[0]; + aa = poly[degree]; + bb = poly[NM1]; + zerok = K[NM1] === 0 ? 1 : 0; + + for (jj = 0; jj < 5; jj++) { + cc = K[NM1]; + if (zerok) { + // Use unscaled form of recurrence + for (let idx = 0; idx < NM1; idx++) { + j = NM1 - idx; + K[j] = K[j - 1]; + } // End for i + K[0] = 0; + zerok = K[NM1] === 0 ? 1 : 0; + } else { + // Used scaled form of recurrence if value of K at 0 is nonzero + t = -aa / cc; + for (let idx = 0; idx < NM1; idx++) { + j = NM1 - idx; + K[j] = t * K[j - 1] + poly[j]; + } // End for i + K[0] = poly[0]; + zerok = Math.abs(K[NM1]) <= Math.abs(bb) * DBL_EPSILON * 10.0 ? 1 : 0; + } + } + + // Save K for restarts with new shifts + for (let idx = 0; idx < degree; idx++) { + temp[idx] = K[idx]; + } + + // Loop to select the quadratic corresponding to each new shift + for (jj = 1; jj <= 20; jj++) { + // Quadratic corresponds to a double shift to a non-real point and its + // complex conjugate. The point has modulus BND and amplitude rotated + // by 94 degrees from the previous shift. + + xxx = -(sinr * yy) + cosr * xx; + yy = sinr * xx + cosr * yy; + xx = xxx; + sr = bnd * xx; + u = -(2.0 * sr); + + // Second stage calculation, fixed quadratic + fxshfrAk1(DBL_EPSILON, MDP1, 20 * jj, sr, bnd, K, degree, poly, NN, qp, u, fxshfrPar); + + if (fxshfrPar.NZ === 0) { + // If the iteration is unsuccessful, another quadratic is chosen after restoring K + for (let idx = 0; idx < degree; idx++) { + K[idx] = temp[idx]; + } + } else { + // The second stage jumps directly to one of the third stage iterations and + // returns here if successful. Deflate the polynomial, store the zero or + // zeros, and return to the main algorithm. + j = degPar.Degree - degree; + zeroReal[j] = fxshfrPar.szr; + zeroImag[j] = fxshfrPar.szi; + NN -= fxshfrPar.NZ; + degree = NN - 1; + for (let idx = 0; idx < NN; idx++) { + poly[idx] = qp[idx]; + } + if (fxshfrPar.NZ === 1) { + // Single zero found, no additional zeros to store + } else { + zeroReal[j + 1] = fxshfrPar.lzr; + zeroImag[j + 1] = fxshfrPar.lzi; + } + break; + } + } + // Return with failure if no convergence with 20 shifts + if (jj > 20) { + degPar.Degree -= degree; + break; + } + } + // >>>>> End Main Loop <<<<< + } + // --> End Jenkins-Traub + rpSolve(degreePar, p, zeror, zeroi); + + const l = zeroi.length; + /** @type {(string | number)[]} */ + const results = []; + // Format the output + for (i = 0; i < l; i++) { + // We round the imaginary part to avoid having something crazy like 5.67e-16. + const img = round(zeroi[i], decp + 8); + let real = round(Number(zeror[i]), decp + 8); + // Did the rounding pay off? If the rounding did nothing more than chop off a few digits then no. + // If the rounding results in a a number at least 3 digits shorter we'll keep it else we'll keep + // the original otherwise the rounding was worth it. + real = decp - String(real).length > 2 ? real : Number(zeror[i]); + const sign = Number(img) < 0 ? '-' : ''; + + // Remove the zeroes + /** @type {string | number} */ + let realStr = real; + /** @type {string | number} */ + let imgStr = img; + if (real === 0) { + realStr = ''; + } + if (img === 0) { + imgStr = ''; + } + + // Remove 1 as the multiplier and discard imaginary part if there isn't one. + if (Math.abs(Number(img)) === 1) { + imgStr = `${sign}i`; + } else if (img) { + imgStr = `${img}*i`; + } else { + imgStr = ''; + } + + const num = realStr && imgStr ? `${realStr}+${imgStr}` : String(realStr) + String(imgStr); + results[i] = num.replace(/\+-/gu, '-'); + } + return results; + } + }, + roots(symbol) { + if (symbol.isConstant(true, true)) { + return core.Utils.nroots(symbol); + } + const roots = __.proots(symbol).map(x => _.parse(x)); + return core.Vector.fromArray(roots); + }, + /** + * Find root using Newton-Raphson method. + * + * @param {NerdamerSymbolType | ((x: number) => number)} f - Function or symbol + * @param {number} guess - Initial guess + * @param {((x: number) => number) | undefined} [dx] - Optional derivative + * @returns {number | null} + */ + froot(f, guess, dx) { + /** + * @param {number | null} xn + * @returns {number | null} + */ + const newtonraph = function (xn) { + const mesh = 1e-12; + // If the derivative was already provided then don't recalculate. + const df = dx + ? dx + : core.Build.build(core.Calculus.diff(/** @type {NerdamerSymbolType} */ (f).clone())); + // If the function was passed in as a function then don't recalculate. + const fn = f instanceof Function ? f : core.Build.build(f); + const max = 10000; + let done = false; + let safety = 0; + while (!done) { + const x = + /** @type {number} */ (xn) - fn(/** @type {number} */ (xn)) / df(/** @type {number} */ (xn)); + // Absolute values for both x & xn ensures that we indeed have the radius + const r = Math.abs(x) - Math.abs(/** @type {number} */ (xn)); + const delta = Math.abs(r); + xn = x; + + if (delta < mesh) { + done = true; + } else if (safety > max) { + xn = null; + done = true; + } + + safety++; + } + return xn; + }; + return newtonraph(Number(guess)); + }, + /** + * Solve quadratic equation. + * + * @param {NerdamerSymbolType | string} a + * @param {NerdamerSymbolType | string} b + * @param {NerdamerSymbolType | string} c + * @returns {NerdamerSymbolType[]} + */ + quad(a, b, c) { + /** + * @param {NerdamerSymbolType | string} qa + * @param {NerdamerSymbolType | string} qb + * @param {NerdamerSymbolType | string} qc + * @param {number} sign + * @returns {NerdamerSymbolType} + */ + const q = function (qa, qb, qc, sign) { + return /** @type {NerdamerSymbolType} */ ( + _.parse(`-(${qb}+${sign}*sqrt((${qb})^2-4*(${qa})*(${qc})))/(2*${qa})`) + ); + }; + return [q(a, b, c, 1), q(a, b, c, -1)]; + }, + /** + * Returns sum and product given roots. + * + * @param {NerdamerSymbolType | string} a + * @param {NerdamerSymbolType | string} b + * @returns {NerdamerSymbolType[]} + */ + sumProd(a, b) { + return __.quad(String(-b), String(a), '-1').map(x => x.invert()); + }, + coeffs(symbol, wrt, coeffs) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + coeffs ||= [new NerdamerSymbol(0)]; + // We cannot get coeffs for group EX + let vars = variables(symbol); + + // If wrt is not provided and there's only one variable, use it + if (wrt === undefined && vars.length === 1) { + wrt = vars[0]; + } + wrt = String(wrt); + + if (symbol.group === EX && symbol.contains(wrt, true)) { + _.error(`Unable to get coefficients using expression ${symbol.toString()}`); + } + vars = variables(symbol); + + // Check if symbol contains irrational constants that would be lost by Polynomial + // These include pi, e, and sqrt (which are treated as constants but aren't simple numbers) + const hasIrrationalConstants = + symbol.contains('pi') || symbol.contains('e') || symbol.containsFunction('sqrt'); + + if (vars.length === 1 && vars[0] === wrt && !symbol.isImaginary() && !hasIrrationalConstants) { + const a = new Polynomial(symbol).coeffs.map(x => new NerdamerSymbol(x)); + + for (let i = 0, l = a.length; i < l; i++) { + let coeff = a[i]; + const e = coeffs[i]; + if (e) { + coeff = /** @type {NerdamerSymbolType} */ (_.add(e, coeff)); + } + coeffs[i] = coeff; // Transfer it all over + } + } else if ( + vars.length === 1 && + vars[0] === wrt && + !symbol.isImaginary() && + hasIrrationalConstants && + symbol.group === CP + ) { + // Use getCoeffs which properly preserves symbolic constants + // Only for CP (sum) groups - CB (product) groups are handled in the else branch + const a = core.Utils.getCoeffs(symbol, wrt); + + for (let i = 0, l = a.length; i < l; i++) { + let coeff = /** @type {NerdamerSymbolType} */ (a[i]); + const e = coeffs[i]; + if (e) { + coeff = /** @type {NerdamerSymbolType} */ (_.add(e, coeff)); + } + coeffs[i] = coeff; + } + } else { + if (!wrt) { + _.error('Polynomial contains more than one variable. Please specify which variable is to be used!'); + } + // If the variable isn't part of this polynomial then we're looking at x^0 + + if (vars.indexOf(wrt) === -1) { + coeffs[0] = /** @type {NerdamerSymbolType} */ (_.add(symbol, coeffs[0])); + } else { + coeffs ||= [new NerdamerSymbol(0)]; + let coeff; + if (symbol.group === CB) { + const s = symbol.symbols[wrt]; + if (!s) { + _.error('Expression is not a polynomial!'); + } + const p = Number(s.power); + coeff = /** @type {NerdamerSymbolType} */ (_.divide(symbol.clone(), s.clone())); + if (/** @type {NerdamerSymbolType} */ (coeff).contains(wrt, true) || p < 0 || !isInt(p)) { + _.error('Expression is not a polynomial!'); + } + const e = coeffs[p]; + if (e) { + coeff = /** @type {NerdamerSymbolType} */ (_.add(e, coeff)); + } + coeffs[p] = coeff; + } else if (symbol.group === CP) { + symbol.each(x => { + __.coeffs(x.clone(), wrt, coeffs); + }, true); + } + } + } + // Fill holes + for (let i = 0, l = coeffs.length; i < l; i++) { + if (typeof coeffs[i] === 'undefined') { + coeffs[i] = new NerdamerSymbol(0); + } + } + + return coeffs; + }, + /** + * Get's all the powers of a particular polynomial including the denominators. The denominators powers are + * returned as negative. All remaining polynomials are returned as zero order polynomials. for example + * polyPowers(x^2+1/x+y+t) will return [ '-1', 0, '2' ] + * + * @param {NerdamerSymbolType} e + * @param {string} forVariable + * @param {Array} powers + * @returns {Array} An array of the powers + */ + // assumes you've already verified that it's a polynomial + polyPowers(e, forVariable, powers) { + powers ||= []; + const g = e.group; + if (g === PL && forVariable === e.value) { + powers = powers.concat(keys(e.symbols)); + } else if (g === CP) { + for (const s in e.symbols) { + if (!Object.hasOwn(e.symbols, s)) { + continue; + } + const symbol = e.symbols[s]; + const symGroup = symbol.group; + const v = symbol.value; + if (symGroup === S && forVariable === v) { + powers.push(symbol.power); + } else if (symGroup === PL || symGroup === CP) { + powers = __.polyPowers(symbol, forVariable, powers); + } else if (symGroup === CB && symbol.contains(forVariable)) { + const t = symbol.symbols[forVariable]; + if (t) { + powers.push(t.power); + } + } else if (symGroup === N || forVariable !== v) { + powers.push(0); + } + } + } else if (g === CB && e.contains(forVariable)) { + const decomp = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(e, forVariable, true)); + powers.push(decomp.x.power); + } + return core.Utils.arrayUnique(powers).sort(); + }, + // The factor object + Factor: { + // Splits the symbol in symbol and constant + split(symbol) { + let c = new NerdamerSymbol(1); // The constants part + let s = new NerdamerSymbol(1); // The symbolic part + __.Factor.factorInner(symbol, new Factors()).each(x => { + const t = /** @type {NerdamerSymbolType} */ (_.parse(x)); + if (x.isConstant(true)) { + c = /** @type {NerdamerSymbolType} */ (_.multiply(c, t)); + } else { + s = /** @type {NerdamerSymbolType} */ (_.multiply(s, t)); + } + }); + return [c, s]; + }, + mix(o, includeNegatives) { + const factors = keys(o); + const l = factors.length; + const m = []; // Create a row which we'r going to be mixing + for (let i = 0; i < l; i++) { + const factor = Number(factors[i]); + const p = o[factors[i]]; + const ll = m.length; + for (let j = 0; j < ll; j++) { + const t = m[j] * factor; + m.push(t); + if (includeNegatives) { + m.push(-t); + } + } + + for (let j = 1; j <= p; j++) { + m.push(factor ** j); + } + } + return m; + }, + // TODO: this method is to replace common factoring + common(symbol, factors) { + try { + if (symbol.group === CP) { + // This may have the unfortunate side effect of expanding and factoring again + // to only end up with the same result. + // TODO: try to avoid this + // collect the symbols and sort to have the longest first. Thinking is that the longest terms + // has to contain the variable in order for it to be factorable + const expanded = /** @type {NerdamerSymbolType} */ ( + _.expand(symbol.clone(), { expand_denominator: true }) + ); + /** @type {(sym: unknown) => number} */ + const getLength = sym => /** @type {{ length?: number }} */ (sym).length || 1; + const symbols = /** @type {NerdamerSymbolType[]} */ ( + expanded.collectSymbols(null, null, (a, b) => getLength(b) - getLength(a)) + ); + + /** @type {Record} */ + const map = {}; // Create a map of common factors + /** @type {FracType[]} */ + const coeffs = []; + for (let i = 0; i < symbols.length; i++) { + const sym = symbols[i]; + coeffs.push(sym.multiplier.clone()); + sym.each(x => { + const p = Number(x.power); + // This check exits since we have a symbolic power. + // For the future... think about removing this check and modify for symbolic powers + if (isNaN(p)) { + throw new Error('exiting'); + } + // Loop through the symbols and lump together common terms + if (x.value in map) { + if (p < map[x.value][0]) { + map[x.value][0] = p; + } + map[x.value][1].push(x); + } else { + map[x.value] = [p, [x]]; + } + }); + } + // The factor + let factor = new NerdamerSymbol(1); + for (const x in map) { + // If this factor is found in all terms since the length of + // matching variable terms matches the number of original terms + if (map[x][1].length === symbols.length) { + // Generate a symbol and multiply into the factor + factor = /** @type {NerdamerSymbolType} */ ( + _.multiply( + factor, + /** @type {NerdamerSymbolType} */ ( + _.pow(new NerdamerSymbol(x), new NerdamerSymbol(map[x][0])) + ) + ) + ); + } + } + // Get coefficient factor + const c = core.Math2.QGCD.apply(null, coeffs); + + if (!c.equals(1)) { + factors.add(new NerdamerSymbol(c)); + for (let i = 0; i < symbols.length; i++) { + symbols[i].multiplier = symbols[i].multiplier.divide(c); + } + } + + // If we actuall found any factors + if (!factor.equals(1)) { + factors.add(factor); + symbol = new NerdamerSymbol(0); + for (let i = 0; i < symbols.length; i++) { + symbol = /** @type {NerdamerSymbolType} */ ( + _.add(symbol, _.divide(symbols[i], factor.clone())) + ); + } + } + } + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + } + + return symbol; + }, + zeroes(symbol, factors) { + const exit = function () { + throw new core.exceptions.ValueLimitExceededError('Exiting'); + }; + try { + let term; + let sum; + let p; + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol.clone())); + const e = symbol.toString(); + const vars = variables(symbol); + + sum = new NerdamerSymbol(0); + + const terms = []; + /** @type {FracType[]} */ + const powers = []; + + // Start setting each variable to zero + for (let i = 0, l = vars.length; i < vars.length; i++) { + /** @type {Record} */ + const subs = {}; + // We want to create a subs object with all but the current variable set to zero + for (let j = 0; j < l; j++) { + if (i !== j) // Make sure we're not looking at the same variable + { + subs[vars[j]] = 0; + } + } + term = /** @type {NerdamerSymbolType} */ (_.parse(e, subs)); + const tp = term.power; + // The temporary power has to be an integer as well + if (!isInt(tp)) { + exit(); + } + terms.push(term); + powers.push(/** @type {FracType} */ (term.power)); + } + + // Get the gcd. This will be the p in (a^n+b^m)^p + // if the gcd equals 1 meaning n = m then we need a tie breakder + if (core.Utils.allSame(powers)) { + // Get p given x number of terms + const nTerms = symbol.length; + // The number of zeroes determines + const nZeroes = terms.length; + const den = Math.round((Math.sqrt(8 * nTerms - 1) - 3) / 2); + if (nZeroes === 2) { + p = new Frac(Number(powers[0]) / (nTerms - 1)); + } else if (nZeroes === 3 && den !== 0) { + p = new Frac(Number(powers[0]) / den); + } else { + // P is just the gcd of the powers + p = core.Math2.QGCD.apply(null, /** @type {FracType[]} */ (powers)); + } + /* + //get the lowest possible power + //e.g. given b^4+2*a^2*b^2+a^4, the power we're looking for would be 2 + symbol.each(function(x) { + if(x.group === CB) + x.each(function(y) { + if(!p || y.power.lessThan(p)) + //p = Number(y.power); + p = y.power; + }); + else if(!p || x.power.lessThan(p)) + //p = Number(x.power); + p = x.power; + }); + */ + } else { + // P is just the gcd of the powers + p = core.Math2.QGCD.apply(null, powers); + } + + // If we don't have an integer then exit + if (!isInt(p)) { + return symbol; // Nothing to do + // exit(); + } + + // Build the factor + for (let i = 0; i < terms.length; i++) { + const t = terms[i]; + const nFrac = /** @type {FracType} */ (t.power).clone().divide(/** @type {FracType} */ (p)); + const n = Number(nFrac); + // Don't take squareroots of negatives + if ((Number(t.multiplier.num) < 0 || Number(t.multiplier.den) < 0) && n % 2 === 0) { + return symbol; + } + t.multiplier = new Frac(Number(t.multiplier) ** (1 / n)); + t.power = /** @type {FracType} */ (p).clone(); + sum = /** @type {NerdamerSymbolType} */ (_.add(sum, t)); + } + + // By now we have the factor of zeroes. We'll know if we got it right because + // we'll get a remainder of zero each time we divide by it + if (/** @type {NerdamerSymbolType} */ (sum).group !== CP) { + return symbol; + } // Nothing to do + + while (true) { + const d = __.div(symbol.clone(), sum.clone()); + if (/** @type {NerdamerSymbolType} */ (d[1]).equals(0)) { + symbol = /** @type {NerdamerSymbolType} */ (d[0]); + factors.add(sum.clone()); + if (symbol.equals(1)) // We've reached 1 so done. + { + break; + } + } else { + break; + } + } + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + } + return symbol; + }, + factor(symbol, factors) { + core.Utils.checkTimeout(); + const originalFactors = factors ? { ...factors.factors } : null; + const originalLength = factors ? factors.length : 0; + try { + let retval = __.Factor.factorInner(symbol, factors); + retval = retval.pushMinus(); + return retval; + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + + if (factors && originalFactors) { + factors.factors = originalFactors; + factors.length = originalLength; + } + return symbol; + } + }, + factorInner(symbol, factors) { + core.Utils.checkTimeout(); + // Don't try to factor constants, + // do it with Math2.factor + if (symbol.isConstant()) { + if (symbol.isInteger()) { + return core.Math2.factor(Number(symbol.multiplier)); + } + // Return symbol; + } + + const _symbol = /** @type {NerdamerSymbolType} */ (_.parse(symbol)); + + // Functions may have been evaluated in parse() + // STILL don't try to factor constants + // do it with Math2.factor + if (_symbol.isConstant()) { + if (_symbol.isInteger()) { + return core.Math2.factor(Number(_symbol.multiplier)); + } + return symbol; + } + + // Shortcut 0 and 1 + if (_symbol.equals(0) || _symbol.equals(1)) { + return _symbol; + } + + let retval = __.Factor._factor(_symbol, factors); + if (retval.equals(symbol)) { + return retval; + } + + // Shortcut 0 and 1 AGAIN after factor (which does eval) + if (retval.equals(0) || retval.equals(1)) { + return retval; + } + + if (retval.group === CB) { + let t = new NerdamerSymbol(1); + const p = _.parse(retval.power); + // Store the multiplier and strip it + let m = _.parse(retval.multiplier); + + retval.toUnitMultiplier(); + + /* + * NOTE: for sign issues with factor START DEBUGGING HERE + */ + // move the sign to t + if (retval.multiplier.lessThan(0)) { + t.negate(); + retval.negate(); + } + + retval.each(x => { + // Related to #566. Since the symbol's group may not have been properly + // updated, it's easier to just parse the symbol and have the parser + // do the update for us. + + const factored = /** @type {NerdamerSymbolType} */ (_.parse(__.Factor._factor(x))); + m = /** @type {NerdamerSymbolType} */ ( + _.multiply(m, NerdamerSymbol.create(factored.multiplier.toString())) + ); + factored.toUnitMultiplier(); + + if (factored.group === CB) { + let _t = new NerdamerSymbol(1); + factored.each(y => { + const _factored = /** @type {NerdamerSymbolType} */ (_.parse(__.Factor._factor(y))); + if (_factored.group === CB) { + m = /** @type {NerdamerSymbolType} */ ( + _.multiply(m, NerdamerSymbol.create(_factored.multiplier.toString())) + ); + _factored.toUnitMultiplier(); + } + _t = /** @type {NerdamerSymbolType} */ (_.multiply(_t, _factored)); + }); + _t = /** @type {NerdamerSymbolType} */ ( + _.pow(_t, new NerdamerSymbol(factored.power.toString())) + ); + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, _t)); + } else { + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, factored)); + } + }); + + // Put back the multiplier and power + const pow = /** @type {NerdamerSymbolType} */ (_.pow(t, p)); + retval = /** @type {NerdamerSymbolType} */ (_.multiply(m, pow)); + } + return retval; + }, + quadFactor(symbol, factors) { + if (symbol.isPoly() && __.degree(symbol).equals(2)) { + // We've already checked that we're dealing with a polynomial + const v = core.Utils.variables(symbol)[0]; // Get the variable + const coeffs = __.coeffs(symbol, v); + // Factor the lead coefficient + if (coeffs.length < 3) { + return symbol; + } + const cf = __.Factor._factor(coeffs[2].clone()); + // Check if we have factors + if (cf.group === CB) { + const symbols = /** @type {NerdamerSymbolType[]} */ (cf.collectSymbols()); + // If the factors are greater than 2 we're done so exit + if (symbols.length > 2) { + return symbol; + } + // If we have two factors then attempt to factor the polynomial + // let the factors be f1 and f1 + // let the factors be (ax+b)(cx+d) + // let the coefficients be c1x^2+c2x+c3 + // then a(x1)+c(x2)=c2 and x1*x2=c3 + // we can solve for x1 and x2 + const c = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.parse(coeffs[0]), _.parse(symbols[0])) + ); + const b = /** @type {NerdamerSymbolType} */ (_.parse(coeffs[1])).negate(); + const a = /** @type {NerdamerSymbolType} */ (_.parse(symbols[1])); + // Solve the system + const root = __.quad(a, b, c).filter(x => core.Utils.isInt(x)); + // If we have one root then find the other one by dividing the constant + if (root.length === 1) { + const root1 = root[0]; + const root2 = _.divide(coeffs[0], /** @type {NerdamerSymbolType} */ (_.parse(root1))); + if (core.Utils.isInt(root2)) { + // We found them both + factors.add( + /** @type {NerdamerSymbolType} */ ( + _.parse(format('({0})*({1})+({2})', String(symbols[1]), v, String(root2))) + ) + ); + factors.add( + /** @type {NerdamerSymbolType} */ ( + _.parse(format('({0})*({1})+({2})', String(symbols[0]), v, root1)) + ) + ); + symbol = new NerdamerSymbol(1); + } + } + } + // // sanitization: eliminate "-(-x)" + // for (let xk in symbol.symbols) { + // let x = symbol.symbols[xk]; + // if ((x.group === CB || x.group === CP || x.group === PL) && + // x.multiplier.equals(-1)) { + // console.log("replacing "+x) + // symbol[xk] = _.parse(x); + // console.log("with "+symbol[xk]) + // } + // } + } + return symbol; + }, + cubeFactor(symbol, factors) { + if (symbol.isComposite()) { + const symbols = /** @type {NerdamerSymbolType[]} */ (symbol.collectSymbols()); + // The symbol should be in the form of a^3+-b^3. The length + // should therefore only be two. If it's any different from this + // then we're done + if (symbols.length === 2) { + // Store the signs and then strip them from the symbols + let signA = symbols[0].sign(); + let a = symbols[0].clone().abs(); + let signB = symbols[1].sign(); + let b = symbols[1].clone().abs(); + // Check if they're cube + if (a.isCube() && b.isCube()) { + // Keep the negative sign on the right, meaning b is always negative. + if (signA < signB) { + // Swap the signs and then the values + [signA, signB] = [signB, signA]; + [a, b] = [b, a]; + } + + // Get teh roots + const mRootA = _.parse(a.getNth(3)); + const mRootB = _.parse(b.getNth(3)); + + // Remove the cube for both + const x = _.multiply(_.expand(_.pow(a.clone().toUnitMultiplier(), _.parse('1/3'))), mRootA); + const y = _.multiply(_.expand(_.pow(b.clone().toUnitMultiplier(), _.parse('1/3'))), mRootB); + + if (signA === 1 && signB === -1) { + // Apply difference of cubes rule + factors.add(_.parse(format('(({0})-({1}))', String(x), String(y)))); + factors.add(_.parse(format('(({0})^2+({0})*({1})+({1})^2)', String(x), String(y)))); + symbol = new NerdamerSymbol(1); + } else if (signA === 1 && signB === 1) { + // Apply sum of cubes rule + factors.add(_.parse(format('(({0})+({1}))', String(x), String(y)))); + factors.add(_.parse(format('(({0})^2-({0})*({1})+({1})^2)', String(x), String(y)))); + symbol = new NerdamerSymbol(1); + } + } + } + } + + return symbol; + }, + /** + * Internal factorization implementation + * + * @param {NerdamerSymbolType} symbol + * @param {FactorsLike} [factors] + * @returns {NerdamerSymbolType} + */ + _factor(symbol, factors) { + core.Utils.checkTimeout(); + const _g = symbol.group; + // Some items cannot be factored any further so return those right away + if (symbol.group === FN) { + const arg = symbol.args[0]; + if (arg.group === S && arg.isSimple()) { + return symbol; + } + } else if (symbol.group === S && symbol.isSimple()) { + return symbol; + } + + // Expand the symbol to get it in a predictable form. If this step + // is skipped some factors are missed. + // if(symbol.group === CP && !(even(symbol.power) && symbol.multiplier.lessThan(0))) { + if (symbol.group === CP) { + symbol.distributeMultiplier(true); + let t = new NerdamerSymbol(0); + symbol.each(x => { + if ((x.group === CP && x.power.greaterThan(1)) || x.group === CB) { + x = /** @type {NerdamerSymbolType} */ (_.expand(x)); + } + t = /** @type {NerdamerSymbolType} */ (_.add(t, x)); + }); + t.power = symbol.power; + + symbol = t; + } + + if (symbol.group === FN && symbol.fname !== 'sqrt') { + symbol = core.Utils.evaluate(symbol); + } + + // Make a copy of the symbol to return if something goes wrong + const untouched = symbol.clone(); + try { + if (symbol.group === CB) { + const _p = _.parse(symbol.power); + + // Grab the denominator and strip the multiplier and power. Store them in an array + const denArray = __.Simplify.strip(symbol.getDenom()); + const numArray = __.Simplify.strip(symbol.getNum()); + + const den = denArray.pop(); + const num = numArray.pop(); + + // If the numerator equals the symbol then we've hit the simplest form and then we're done + if (num.equals(symbol)) { + return symbol; + } + const nfact = __.Factor.factorInner(num); + const dfact = __.Factor.factorInner(den); + + const n = __.Simplify.unstrip( + /** @type {[NerdamerSymbolType, NerdamerSymbolType]} */ (/** @type {unknown} */ (numArray)), + nfact + ); + const d = __.Simplify.unstrip( + /** @type {[NerdamerSymbolType, NerdamerSymbolType]} */ (/** @type {unknown} */ (denArray)), + dfact + ); + + const retval = /** @type {NerdamerSymbolType} */ (_.divide(n, d)); + + return retval; + } + if (symbol.group === S) { + return symbol; // Absolutely nothing to do + } + + if (symbol.isConstant()) { + if (symbol.equals(1) || symbol.equals(0) || !symbol.isInteger()) { + return symbol.clone(); + } + const ret = core.Math2.factor(Number(symbol.multiplier)); + return ret; + } + + const p = symbol.power.clone(); + + if (isInt(p) && !(p.lessThan(0) && symbol.group === FN)) { + const sign = p.sign(); + symbol.toLinear(); + factors ||= new Factors(); + /** @type {Record} */ + const map = {}; + symbol = /** @type {NerdamerSymbolType} */ (_.parse(core.Utils.subFunctions(symbol, map))); + if (keys(map).length > 0) { + // It might have functions + factors.preAdd = function preAdd(factor) { + const ret = _.parse(factor, core.Utils.getFunctionsSubs(map)); + return /** @type {NerdamerSymbolType} */ (ret); + }; + } + + // Strip the power + if (!symbol.isLinear()) { + factors.pFactor = symbol.power.toString(); + symbol.toLinear(); + } + + const vars = variables(symbol); + // Bypass for imaginary. TODO: find a better solution + if (symbol.isImaginary()) { + vars.push(core.Settings.IMAGINARY); + } + const multiVar = vars.length > 1; + + // Minor optimization. Seems to cut factor time by half in some cases. + if (multiVar) { + let allS = true; + let allUnit = true; + symbol.each(x => { + if (x.group !== S) { + allS = false; + } + if (!x.multiplier.equals(1)) { + allUnit = false; + } + }); + + if (allS && allUnit) { + return /** @type {NerdamerSymbolType} */ ( + _.pow(_.parse(symbol, core.Utils.getFunctionsSubs(map)), _.parse(p)) + ); + } + } + + // Factor the coefficients + const coeffFactors = new Factors(); + + symbol = __.Factor.coeffFactor(symbol, coeffFactors); + + coeffFactors.each(x => { + // If the factor was negative but was within a square then it becomes positive + if (even(Number(p)) && x.lessThan(0)) { + x.negate(); + } + + if (sign < 0) { + x.invert(); + } + factors.add(x); + }); + + // Factor the power + const powerFactors = new Factors(); + symbol = __.Factor.powerFactor(symbol, powerFactors); + powerFactors.each(x => { + if (sign < 0) { + x.invert(); + } + factors.add(x); + }); + + if (multiVar) { + // Try sum and difference of cubes + symbol = __.Factor.cubeFactor(symbol, factors); + + symbol = __.Factor.mfactor(symbol, factors); + + // Put back the sign of power + factors.each(x => { + if (sign < 0) { + x.power.negate(); + } + }); + } else { + // Pass in vars[0] for safety + const v = vars[0]; + + symbol = __.Factor.squareFree(symbol, factors, v); + + const tFactors = new Factors(); + + symbol = __.Factor.trialAndError(symbol, tFactors, v); + + // Generate a symbol based off the last factors + const tfSymbol = tFactors.toSymbol(); + // If nothing was factored then return the factors + if (tfSymbol.equals(untouched)) { + return tfSymbol; + } + + for (const x in tFactors.factors) { + if (!Object.hasOwn(tFactors.factors, x)) { + continue; + } + // Store the current factor in tFactor + const tFactor = tFactors.factors[x]; + factors.add(/** @type {NerdamerSymbolType} */ (_.pow(tFactor, _.parse(p)))); + } + // If we still don't have a factor and it's quadratic then let's just do a quad factor + if (symbol.equals(untouched)) { + symbol = __.Factor.quadFactor(symbol, factors); + } + } + + // Last minute clean up + symbol = /** @type {NerdamerSymbolType} */ (_.parse(symbol, core.Utils.getFunctionsSubs(map))); + + const addPower = factors.length === 1; + + factors.add(/** @type {NerdamerSymbolType} */ (_.pow(symbol, _.parse(p)))); + + let retval = factors.toSymbol(); + + // We may have only factored out the symbol itself so we end up with a factor of one + // where the power needs to be placed back + // e.g. factor((2*y+p)^2). Here we end up having a factor of 1 remaining and a p of 2. + if (addPower && symbol.equals(1) && retval.isLinear()) { + retval = /** @type {NerdamerSymbolType} */ (_.pow(retval, _.parse(p))); + } + + return retval; + } + + return symbol; + } catch (e) { + if (e?.message === 'timeout') { + throw e; + } + // No need to stop the show because something went wrong :). Just return the unfactored. + return untouched; + } + }, + reduce(symbol, factors) { + if (symbol.group === CP && symbol.length === 2) { + const symbols = /** @type {NerdamerSymbolType[]} */ (symbol.collectSymbols()).sort( + (a, b) => Number(b.multiplier) - Number(a.multiplier) + ); + if (/** @type {FracType} */ (symbols[0].power).equals(/** @type {FracType} */ (symbols[1].power))) { + // X^n-a^n + const n = /** @type {NerdamerSymbolType} */ (_.parse(symbols[0].power)); + const a = symbols[0].clone().toLinear(); + const b = symbols[1].clone().toLinear(); + + // Apply rule: (a-b)*sum(a^(n-i)*b^(i-1),1,n) + factors.add(/** @type {NerdamerSymbolType} */ (_.add(a.clone(), b.clone()))); + // Flip the sign + b.negate(); + // Turn n into a number + const nn = Number(n); + // The remainder + let result = new NerdamerSymbol(0); + for (let i = 1; i <= nn; i++) { + const aa = /** @type {NerdamerSymbolType} */ ( + _.pow(a.clone(), _.subtract(n.clone(), new NerdamerSymbol(i))) + ); + const bb = /** @type {NerdamerSymbolType} */ ( + _.pow(b.clone(), _.subtract(new NerdamerSymbol(i), new NerdamerSymbol(1))) + ); + result = /** @type {NerdamerSymbolType} */ ( + _.add(result, /** @type {NerdamerSymbolType} */ (_.multiply(aa, bb))) + ); + } + return result; + } + } + return symbol; + }, + /** + * Makes NerdamerSymbol square free + * + * @param {NerdamerSymbolType} symbol + * @param {Factors} factors + * @param {string} [variable] The variable which is being factored + * @returns {NerdamerSymbolType} + */ + squareFree(symbol, factors, variable) { + if (symbol.isConstant() || symbol.group === S) { + return symbol; + } + + if (!symbol.isPoly()) { + return symbol; + } + + const poly = new Polynomial(symbol, variable); + const sqfr = poly.squareFree(); + const p = sqfr[2]; + // If we found a square then the p entry in the array will be non-unit + if (p !== 1) { + // Make sure the remainder doesn't have factors + const t = sqfr[1].toSymbol(); + t.power = /** @type {FracType} */ (t.power).multiply(new Frac(p)); + // Send the factor to be fatored to be sure it's completely factored + factors.add(__.Factor.factorInner(t)); + + const retval = __.Factor.squareFree(sqfr[0].toSymbol(), factors); + + return retval; + } + + return symbol; + }, + /** + * Factors the powers such that the lowest power is a constant + * + * @param {NerdamerSymbolType} symbol + * @param {Factors} factors + * @returns {NerdamerSymbolType} + */ + powerFactor(symbol, factors) { + // Only PL need apply + if (symbol.group !== PL || symbol.previousGroup === EX) { + return symbol; + } + const k = keys(symbol.symbols); + // We expect only numeric powers so return all else + if (!core.Utils.allNumeric(k)) { + return symbol; + } + + const d = core.Utils.arrayMin(/** @type {number[]} */ (/** @type {unknown} */ (k))); + let retval = new NerdamerSymbol(0); + const q = /** @type {NerdamerSymbolType} */ (_.parse(`${symbol.value}^${d}`)); + symbol.each(x => { + x = /** @type {NerdamerSymbolType} */ (_.divide(x, q.clone())); + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, x)); + }); + + factors.add(q); + return retval; + }, + /** + * Removes GCD from coefficients + * + * @param {NerdamerSymbolType} symbol + * @param {Factors} factors + * @returns {NerdamerSymbolType} + */ + coeffFactor(symbol, factors) { + if (symbol.isComposite()) { + const gcd = core.Math2.QGCD.apply(null, symbol.coeffs()); + + if (gcd.equals(1)) { + // TODO: This should probably go to the prototype + const power = function (sym) { + let p; + if (sym.group === CB) { + p = 0; + sym.each(x => { + p += x.power; + }); + } else { + p = Number(sym.power); + } + return p; + }; + // Factor out negatives from the lead term + const terms = /** @type {NerdamerSymbolType[]} */ ( + symbol.collectSymbols(null, null, null, true) + ).sort((a, b) => { + // Push constants to the back + if (a.isConstant(true)) { + return 1; + } + return Number(b.power) - Number(a.power); + }); + + const LT = terms[0]; + + // Check if the LT is indeed the greatest + if (power(LT) > power(terms[1]) || terms[1].isConstant(true)) { + if (LT.multiplier.lessThan(0)) { + // Although the symbol should always be linear at this point, remove the negative for squares + // to be safe. + factors.add(new NerdamerSymbol(-1)); + + symbol.each(x => { + x.negate(); + }, true); + } + } + } else { + symbol.each(x => { + if (x.isComposite()) { + x.each(y => { + y.multiplier = y.multiplier.divide(gcd); + }); + } else { + x.multiplier = x.multiplier.divide(gcd); + } + }); + symbol.updateHash(); + } + + if (factors) { + factors.add(new NerdamerSymbol(gcd)); + } + } + + return symbol; + }, + /** + * The name says it all :) + * + * @param {NerdamerSymbolType} symbol + * @param {Factors} factors + * @param {string} variable + * @returns {NerdamerSymbolType} + */ + trialAndError(symbol, factors, variable) { + const untouched = symbol.clone(); + try { + // At temp holder for the factors. If all goes well then + // they'll be moved to the actual factors. + const factorArray = []; + + if (symbol.isConstant() || symbol.group === S || !symbol.isPoly()) { + return symbol; + } + let poly = new Polynomial(symbol, variable); + const cnst = poly.coeffs[0]; + const cfactors = core.Math2.ifactor(Number(cnst)); + const roots = __.proots(symbol); + for (let i = 0; i < roots.length; i++) { + let r = roots[i]; + /** @type {number} */ + let p = 1; + if (!isNaN(Number(r))) { + // If it's a number + for (const x in cfactors) { + if (!Object.hasOwn(cfactors, x)) { + continue; + } + // Check it's raised to a power + const n = core.Utils.round(Math.log(Number(x)) / Math.log(Math.abs(Number(r))), 8); + if (isInt(n)) { + r = x; // X must be the root since n gave us a whole + p = Number(n); + break; + } + } + const root = new Frac(Number(r)); + const terms = [new Frac(Number(root.num)).negate()]; + terms[p] = new Frac(Number(root.den)); + // Convert to Frac. The den is coeff of LT and the num is coeff of constant + const div = Polynomial.fromArray(terms, poly.variable).fill(); + const t = poly.divide(div); + if (t[1].equalsNumber(0)) { + // If it's zero we have a root and divide it out + poly = t[0]; + // Factors.add(div.toSymbol()); + factorArray.push(div.toSymbol()); + } + } + } + + if (!poly.equalsNumber(1)) { + poly = __.Factor.search(poly, factors); + } + + // Move the factors over since all went well. + factorArray.forEach(x => { + factors.add(x); + }); + + return poly.toSymbol(); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return untouched; + } + }, + search(poly, factors, base) { + base ||= 10; // I like 10 because numbers exhibit similar behaviours at 10 + const v = poly.variable; // The polynmial variable name + /** + * Attempt to remove a root by division given a number by first creating a polynomial fromt he given + * information + * + * @param {number} c1 - Coeffient for the constant + * @param {number} c2 - Coefficient for the LT + * @param {number} n - The number to be used to construct the polynomial + * @param {number} p - The power at which to create the polynomial + * @returns {null | [Polynomial, Polynomial]} - Returns polynomial array if successful otherwise null + */ + const check = function (c1, c2, n, p) { + const candidate = Polynomial.fit(c1, c2, n, base, p, v); + if (candidate && candidate.coeffs.length > 1) { + const t = poly.divide(candidate); + if (t[1].equalsNumber(0)) { + factors.add(candidate.toSymbol()); + return [t[0], candidate]; + } + } + return null; + }; + const cnst = poly.coeffs[0]; + const cfactors = core.Math2.ifactor(Number(cnst)); + const lc = poly.lc(); + const ltfactors = core.Math2.ifactor(Number(lc)); + const subbed = poly.sub(base); + const isubbed = core.Math2.ifactor(/** @type {number} */ (/** @type {unknown} */ (subbed))); + const nfactors = __.Factor.mix(isubbed, /** @type {number} */ (/** @type {unknown} */ (subbed)) < 0); + let cp = Math.ceil(poly.coeffs.length / 2); + const lcIsNeg = lc.lessThan(0); + const cnstIsNeg = cnst.lessThan(0); + ltfactors['1'] = 1; + cfactors['1'] = 1; + while (cp--) { + for (const x in ltfactors) { + if (!Object.hasOwn(ltfactors, x)) { + continue; + } + for (const y in cfactors) { + if (!Object.hasOwn(cfactors, y)) { + continue; + } + for (let i = 0; i < nfactors.length; i++) { + let factorFound = check(Number(x), Number(y), nfactors[i], cp); + if (factorFound) { + poly = factorFound[0]; + if ( + !core.Utils.isPrime( + /** @type {number} */ (/** @type {unknown} */ (poly.sub(base))) + ) + ) { + poly = __.Factor.search(poly, factors); + } + return poly; + } + if (!factorFound) { + if (lcIsNeg && cnstIsNeg) { + factorFound = check(-Number(x), -Number(y), nfactors[i], cp); + } else if (lcIsNeg) { + factorFound = check(-Number(x), Number(y), nfactors[i], cp); + } // Check a negative lc + else if (cnstIsNeg) { + factorFound = check(Number(x), -Number(y), nfactors[i], cp); + } // Check a negative constant + } + } + } + } + } + return poly; + }, + /** + * Equivalent of square free factor for multivariate polynomials + * + * @param {NerdamerSymbolType} symbol + * @param {Factors} factors + * @returns {NerdamerSymbolType} + */ + mSqfrFactor(symbol, factors) { + if (symbol.group !== FN) { + const vars = variables(symbol).reverse(); + + // Loop through all the variable and remove the partial derivatives + for (let i = 0; i < vars.length; i++) { + let isFactor = false; + do { + if (vars[i] === symbol.value) { + // The derivative tells us nothing since this symbol is already the factor + factors.add(symbol); + symbol = new NerdamerSymbol(1); + continue; + } + + const diff = core.Calculus.diff(symbol, vars[i]); + + const d = __.Factor.coeffFactor(diff); + + if (d.equals(0)) { + break; + } + + // Sometimes nerdamer get too happy about factoring out 1 and -1 + if (d.equals(1) || d.equals(-1)) { + break; + } + + // Trial division to see if factors have whole numbers. + // This can be optimized by stopping as soon as canDivide is false + // this will also need utilize big number at some point + let canDivide = true; + if (d.isConstant() && symbol.isComposite()) { + // Check the coefficients + + symbol.each(x => { + if (Number(x.multiplier) % Number(d.multiplier) !== 0) { + canDivide = false; + } + }, true); + } + + // If we can divide then do so + let div; + if (canDivide) { + const s = symbol.clone(); + div = __.divWithCheck(symbol, d.clone()); + isFactor = /** @type {NerdamerSymbolType} */ (div[1]).equals(0); + + // Break infinite loop for factoring e^t*x-1 + if ( + symbol.equals(/** @type {NerdamerSymbolType} */ (div[0])) && + /** @type {NerdamerSymbolType} */ (div[1]).equals(0) + ) { + // Restore symbol, was mangled in __.div + symbol = s; + break; + } + + if (/** @type {NerdamerSymbolType} */ (div[0]).isConstant()) { + factors.add(/** @type {NerdamerSymbolType} */ (div[0])); + break; + } + } else { + isFactor = false; + } + + if (isFactor) { + factors.add(/** @type {NerdamerSymbolType} */ (div[0])); + symbol = d; + } + } while (isFactor); + } + } + + return symbol; + }, + // Difference of squares factorization + sqdiff(symbol, factors) { + if (symbol.isConstant('all')) { + // Nothing to do + return symbol; + } + + try { + const removeSquare = function (x) { + return core.Utils.block( + 'POSITIVE_MULTIPLIERS', + () => NerdamerSymbol.unwrapPARENS(math.sqrt(math.abs(x))), + true + ); + }; + const separated = core.Utils.separate(symbol.clone()); + if (!separated) { + return symbol; + } + + const objArray = []; + + // Get the unique variables + for (const x in separated) { + if (x !== 'constants') { + objArray.push(separated[x]); + } + } + objArray.sort((a, b) => Number(b.power) - Number(a.power)); + + // If we have the same number of variables as unique variables then we can apply the difference of squares + if (objArray.length === 2) { + let a; + let b; + a = objArray.pop(); + b = objArray.pop(); + + if ( + even(Number(a.power)) && + even(Number(b.power)) && + a.sign() === b.sign() && + a.group === S && + b.group === S + ) { + throw new Error('Unable to factor'); + } + if (a.isComposite() && /** @type {FracType} */ (b.power).equals(2) && a.sign() !== b.sign()) { + // Remove the square from b + b = removeSquare(b); + const f = __.Factor.factorInner( + /** @type {NerdamerSymbolType} */ (_.add(a, separated.constants)) + ); + if (/** @type {FracType} */ (f.power).equals(2)) { + f.toLinear(); + factors.add(/** @type {NerdamerSymbolType} */ (_.subtract(f.clone(), b.clone()))); + factors.add(/** @type {NerdamerSymbolType} */ (_.add(f, b))); + symbol = new NerdamerSymbol(1); + } + } else { + a = a.powSimp(); + b = b.powSimp(); + + if ( + (a.group === S || a.fname === '') && + a.power.equals(2) && + (b.group === S || b.fname === '') && + b.power.equals(2) && + !separated.constants + ) { + if (a.multiplier.lessThan(0)) { + const t = b; + b = a; + a = t; + } + if (a.multiplier.greaterThan(0)) { + a = removeSquare(a); + b = removeSquare(b); + } + + factors.add(/** @type {NerdamerSymbolType} */ (_.subtract(a.clone(), b.clone()))); + factors.add(/** @type {NerdamerSymbolType} */ (_.add(a, b))); + symbol = new NerdamerSymbol(1); + } + } + } + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + } + + return symbol; + }, + // Factoring for multivariate + /** + * Factoring for multivariate polynomials + * + * @param {NerdamerSymbolType} symbol + * @param {FactorsLike} factors + * @returns {NerdamerSymbolType} + */ + mfactor(symbol, factors) { + if (symbol.group === FN) { + if (symbol.fname === 'sqrt') { + const factors2 = new Factors(); + let arg = __.Factor.common(symbol.args[0].clone(), factors2); + arg = __.Factor.coeffFactor(arg, null); + symbol = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.symfunction('sqrt', [arg]), _.parse(symbol.multiplier)) + ); + factors2.each(x => { + symbol = /** @type {NerdamerSymbolType} */ ( + _.multiply(symbol, _.parse(core.Utils.format('sqrt({0})', String(x)))) + ); + }); + } else { + factors.add(symbol); + symbol = new NerdamerSymbol(1); + } + } else { + // Square free factorization + symbol = __.Factor.mSqfrFactor(symbol, factors); + + // Try factor out common factors + // symbol = __.Factor.common(symbol, factors); + + const vars = variables(symbol); + const symbols = /** @type {NerdamerSymbolType[]} */ ( + symbol + .collectSymbols() + .map(x => NerdamerSymbol.unwrapSQRT(/** @type {NerdamerSymbolType} */ (x))) + ); + const sorted = {}; + const maxes = {}; + const l = vars.length; + const n = symbols.length; + // Take all the variables in the symbol and organize by variable name + // e.g. a^2+a^2+b*a -> {a: {a^3, a^2, b*a}, b: {b*a}} + + for (let i = 0; i < l; i++) { + const v = vars[i]; + sorted[v] = new NerdamerSymbol(0); + for (let j = 0; j < n; j++) { + const s = symbols[j]; + if (s.contains(v)) { + const p = + s.value === v + ? /** @type {FracType} */ (s.power).toDecimal() + : /** @type {FracType} */ (s.symbols[v].power).toDecimal(); + if (!maxes[v] || p < maxes[v]) { + maxes[v] = p; + } + sorted[v] = /** @type {NerdamerSymbolType} */ (_.add(sorted[v], s.clone())); + } + } + } + + for (const x in sorted) { + if (!Object.hasOwn(sorted, x)) { + continue; + } + const r = /** @type {NerdamerSymbolType} */ (_.parse(`${x}^${maxes[x]}`)); + const div = /** @type {NerdamerSymbolType} */ (_.divide(sorted[x], r)); + const newFactor = /** @type {NerdamerSymbolType} */ (_.expand(div)); + + if (newFactor.equals(1) || newFactor.equals(-1)) { + break; + } // Why divide by one. Just move + const divided = __.div(symbol.clone(), newFactor); + + if (/** @type {NerdamerSymbolType} */ (divided[0]).equals(0)) { + // Cant factor anymore + break; + } + + // We potentially ended up with fractional coefficients when the + // trial division was performed. We need to remove + // This check will more then likely become superfluous with improvements + // to polynomial division + if (/** @type {NerdamerSymbolType} */ (divided[1]).equals(0)) { + let hasFractions = false; + + /** @type {NerdamerSymbolType} */ (divided[0]).each(elem => { + if (!isInt(elem.multiplier)) { + hasFractions = true; + } + }); + + // The factor isn't really a factor and needs to be put back + if (hasFractions) { + divided[1] = /** @type {NerdamerSymbolType} */ ( + _.expand(_.multiply(divided[1], newFactor)) + ); + // Since the new factor is not just one, we exit. + break; + } + } + + const negNumericFactor = + isInt(newFactor) && /** @type {NerdamerSymbolType} */ (newFactor).lessThan(0); + + if (/** @type {NerdamerSymbolType} */ (divided[1]).equals(0) && !negNumericFactor) { + // We found at least one factor + + // factors.add(newFactor); + const d = __.divWithCheck( + symbol.clone(), + /** @type {NerdamerSymbolType} */ (divided[0]).clone() + ); + const innerR = /** @type {NerdamerSymbolType} */ (d[0]); + + // Nothing left to do since we didn't get a reduction + if (innerR.equals(0)) { + return symbol; + } + + symbol = /** @type {NerdamerSymbolType} */ (d[1]); + // We don't want to just flip the sign. If the remainder is -1 then we accomplished nothing + // and we just return the symbol; + // If r equals zero then there's nothing left to do so we're done + + if (innerR.equals(-1) && !symbol.equals(0)) { + return symbol; + } + + const factor = /** @type {NerdamerSymbolType} */ (divided[0]); + + if (symbol.equals(factor)) { + const rem = __.Factor.reduce(factor, factors); + + if (!symbol.equals(rem)) { + return __.Factor.mfactor(rem, factors); + } + + return rem; + } + factors.add(factor); + // If the remainder of the symbol is zero then we're done. TODO: Rethink this logic a bit. + if (symbol.equals(0)) { + return innerR; + } + + if (innerR.isConstant('all')) { + factors.add(innerR); + return innerR; + } + + symbol = __.Factor.mfactor(innerR, factors); + // // sanitization: eliminate "-(-x)" + // for (let xk in symbol.symbols) { + // let x = symbol.symbols[xk]; + // if ((x.group === CB || x.group === CP || x.group === PL) && + // x.multiplier.equals(-1)) { + // console.log("replacing "+x) + // symbol[xk] = _.parse(x); + // console.log("with "+symbol[xk]) + // } + // } + return symbol; + } + } + } + + // Difference of squares factorization + symbol = __.Factor.sqdiff(symbol, factors); + + // Factors by fishing for zeroes + symbol = __.Factor.zeroes(symbol, factors); + + // // sanitization: eliminate "-(-x)" + // for (let xk in symbol.symbols) { + // let x = symbol.symbols[xk]; + // if ((x.group === CB || x.group === CP || x.group === PL) && + // x.multiplier.equals(-1)) { + // console.log("replacing "+x) + // symbol[xk] = _.parse(x); + // console.log("with "+symbol[xk]) + // } + // } + return symbol; + }, + }, + /** + * Checks to see if a set of "equations" is linear. + * + * @param {Array} s - The set of equations to check + * @returns {boolean} + */ + allLinear(s) { + const l = s.length; + for (let i = 0; i < l; i++) { + if (!__.isLinear(s[i])) { + return false; + } + } + return true; + }, + /* + * Checks to see if the "equation" is linear + * @param {NerdamerSymbolType} e + * @returns {boolean} + */ + isLinear(e) { + let status = false; + const g = e.group; + if (g === PL || g === CP) { + status = true; + for (const s in e.symbols) { + if (!Object.hasOwn(e.symbols, s)) { + continue; + } + const symbol = e.symbols[s]; + const sg = symbol.group; + if (sg === FN || sg === EX) { + status = false; + } + if (sg === CB) { + // Needs further checking since it might be imaginary + status = variables(symbol).length === 1; + } else if (sg === PL || sg === CP) { + status = __.isLinear(symbol); + } else if (symbol.group !== N && symbol.power.toString() !== '1') { + status = false; + break; + } + } + } else if (g === S && /** @type {number} */ (/** @type {unknown} */ (e.power)) === 1) { + status = true; + } + return status; + }, + gcd(...rest) { + let args; + if (rest.length === 1 && rest[0] instanceof core.Vector) { + args = rest[0].elements; + } else { + args = rest; + } + + // Short-circuit early + if (args.length === 0) { + return new NerdamerSymbol(1); + } + if (args.length === 1) { + return args[0]; + } + + let appeared = []; + let evaluate = false; + for (let i = 0; i < args.length; i++) { + const arg = /** @type {NerdamerSymbolType} */ (args[i]); + if (arg.group === FN && arg.fname === 'gcd') { + // Compress gcd(a,gcd(b,c)) into gcd(a,b,c) + args = args.concat(arg.args); + // Do not keep gcd in args + args.splice(i, 1); + } else { + // Look if there are any common variables such that + // gcd(a,b) => gcd(a,b); gcd(a,a) => a + const vars = variables(arg); + if (core.Utils.haveIntersection(vars, appeared)) { + // Ok, there are common variables + evaluate = true; + break; + } else { + appeared = appeared.concat(vars); + } + } + } + + // Appeared.length is 0 when all arguments are group N + if (evaluate || appeared.length === 0) { + // TODO: distribute exponent so that (a^-1*b^-1)^-1 => a*b + if ( + args.every(symbol => + /** @type {NerdamerSymbolType} */ ( + /** @type {NerdamerSymbolType} */ (symbol).getDenom() + ).equals(1) + ) + ) { + let aggregate = /** @type {NerdamerSymbolType} */ (args[0]); + + for (let i = 1; i < args.length; i++) { + aggregate = /** @type {NerdamerSymbolType} */ ( + __.gcd_(/** @type {NerdamerSymbolType} */ (args[i]), aggregate) + ); + } + return aggregate; + } + // Gcd_ cannot handle denominators correctly + return _.divide( + __.gcd.apply( + null, + /** @type {NerdamerSymbolType[]} */ ( + args.map( + symbol => + /** @type {NerdamerSymbolType} */ ( + /** @type {NerdamerSymbolType} */ (symbol).getNum() + ) + ) + ) + ), + __.lcm.apply( + null, + /** @type {NerdamerSymbolType[]} */ ( + args.map( + symbol => + /** @type {NerdamerSymbolType} */ ( + /** @type {NerdamerSymbolType} */ (symbol).getDenom() + ) + ) + ) + ) + ); + } + return _.symfunction('gcd', args); + }, + gcd_(a, b) { + if (a.group === FN || a.group === P) { + a = /** @type {NerdamerSymbolType} */ (core.Utils.block('PARSE2NUMBER', () => _.parse(a))); + } + if (b.group === FN || b.group === P) { + b = /** @type {NerdamerSymbolType} */ (core.Utils.block('PARSE2NUMBER', () => _.parse(b))); + } + + if (b.group === FN) { + b = /** @type {NerdamerSymbolType} */ (core.Utils.block('PARSE2NUMBER', () => _.parse(b))); + } + + if (a.isConstant() && b.isConstant()) { + // Return core.Math2.QGCD(new Frac(Number(a)), new Frac(Number(b))); + return new NerdamerSymbol(core.Math2.QGCD(new Frac(Number(a)), new Frac(Number(b)))); + } + + const den = /** @type {NerdamerSymbolType} */ ( + _.multiply( + /** @type {NerdamerSymbolType} */ (a.getDenom()) || new NerdamerSymbol(1), + /** @type {NerdamerSymbolType} */ (b.getDenom()) || new NerdamerSymbol(1) + ) + ).invert(); + a = /** @type {NerdamerSymbolType} */ (_.multiply(a.clone(), den.clone())); + b = /** @type {NerdamerSymbolType} */ (_.multiply(b.clone(), den.clone())); + + // Feels counter intuitive but it works. Issue #123 (nerdamer("gcd(x+y,(x+y)^2)")) + a = /** @type {NerdamerSymbolType} */ (_.expand(a)); + b = /** @type {NerdamerSymbolType} */ (_.expand(b)); + + if (a.group === CB || b.group === CB) { + const q = /** @type {NerdamerSymbolType} */ (_.divide(a.clone(), b.clone())); // Get the quotient + const t = /** @type {NerdamerSymbolType} */ (_.multiply(b.clone(), q.getDenom().invert())); // Multiply by the denominator + // if they have a common factor then the result will not equal one + if (!t.equals(1)) { + return t; + } + } + + // Just take the gcd of each component when either of them is in group EX + if (a.group === EX || b.group === EX) { + const gcdM = new NerdamerSymbol(core.Math2.QGCD(a.multiplier, b.multiplier)); + const gcdV = __.gcd_( + a.value === CONST_HASH + ? new NerdamerSymbol(1) + : /** @type {NerdamerSymbolType} */ (_.parse(a.value)), + b.value === CONST_HASH + ? new NerdamerSymbol(1) + : /** @type {NerdamerSymbolType} */ (_.parse(b.value)) + ); + const gcdP = __.gcd_( + /** @type {NerdamerSymbolType} */ (_.parse(a.power)), + /** @type {NerdamerSymbolType} */ (_.parse(b.power)) + ); + return _.multiply(gcdM, _.pow(gcdV, gcdP)); + } + + if (a.length < b.length) { + // Swap'm + const t = a; + a = b; + b = t; + } + const varsA = variables(a); + const varsB = variables(b); + + // GCD of a polynomial and a constant: gcd(poly, const) = gcd of coefficients with const + // For symbolic variables, gcd(a, 1) = 1, gcd(a, 0) = a + if ((varsA.length === 1 && varsB.length === 0) || (varsA.length === 0 && varsB.length === 1)) { + // One is a variable/polynomial, one is a constant + const polySymbol = varsA.length === 1 ? a : b; + const constSymbol = varsA.length === 0 ? a : b; + + if (constSymbol.equals(0)) { + return polySymbol; + } + // GCD of polynomial with non-zero constant + // For symbolic case, this is just the gcd of coefficients + return new NerdamerSymbol(core.Math2.QGCD(polySymbol.multiplier, constSymbol.multiplier)); + } + + if (varsA.length === varsB.length && varsA.length === 1 && varsA[0] === varsB[0]) { + const polyA = new Polynomial(a); + const polyB = new Polynomial(b); + return _.divide(polyA.gcd(polyB).toSymbol(), den); + } + // Get the gcd of the multipiers + // get rid of gcd in coeffs + const multipliers = []; + a.each(x => { + multipliers.push(x.multiplier); + }); + b.each(x => { + multipliers.push(x.multiplier); + }); + + let T; + while (!b.equals(0)) { + const t = b.clone(); + a = a.clone(); + T = __.div(a, t); + + b = /** @type {NerdamerSymbolType} */ (T[1]); + if (/** @type {NerdamerSymbolType} */ (T[0]).equals(0)) { + // Return _.multiply(new NerdamerSymbol(core.Math2.QGCD(a.multiplier, b.multiplier)), b); + return _.divide(new NerdamerSymbol(core.Math2.QGCD(a.multiplier, b.multiplier)), den); + } + a = t; + } + + const gcd = core.Math2.QGCD.apply(undefined, multipliers); + + if (!gcd.equals(1)) { + a.each(x => { + x.multiplier = x.multiplier.divide(gcd); + }); + } + + // Return symbolic function for gcd in indeterminate form + if (a.equals(1) && !a.isConstant() && !b.isConstant()) { + return _.divide(_.symfunction('gcd', [a, b]), den); + } + + return _.divide(a, den); + }, + lcm(...rest) { + // https://math.stackexchange.com/a/319310 + // generalization of the 2-variable formula of lcm + + let args; + if (rest.length === 1) { + if (rest[0] instanceof core.Vector) { + args = rest[0].elements; + } else { + _.error('lcm expects either 1 vector or 2 or more arguments'); + } + } else { + args = rest; + } + + // Product of all arguments + // start with new NerdamerSymbol(1) so that prev.clone() which makes unnessesary clones can be avoided + const numer = args.reduce((prev, curr) => _.multiply(prev, curr.clone()), new NerdamerSymbol(1)); + + // Gcd of complementary terms + const denomArgs = + // https://stackoverflow.com/a/18223072 + // take all complementary terms, e.g. + // [a,b,c] => [a*b, b*c, a*c] + // [a,b,c,d] => [a*b*c, a*b*d, a*c*d, b*c*d] + /** @type {NerdamerSymbolType[]} */ ( + (function generateComplementTerms(input, size) { + size = Number(size); + const results = []; + let result; + let mask; + let i; + const total = 2 ** input.length; + for (mask = size; mask < total; mask++) { + result = []; + i = input.length - 1; + + do { + // eslint-disable-next-line no-bitwise -- Bit masking for combinatorial generation + if ((mask & (1 << i)) !== 0) { + result.push(input[i]); + } + } while (i--); + + if (result.length === size) { + results.push(result); + } + } + return results; + // Start with new NerdamerSymbol(1) so that prev.clone() which makes unnessesary clones can be avoided + })(args, args.length - 1).map(x => + x.reduce( + (prev, curr) => /** @type {NerdamerSymbolType} */ (_.multiply(prev, curr.clone())), + new NerdamerSymbol(1) + ) + ) + ); + + let denom; + // Don't eat the gcd term if all arguments are symbols + if (args.every(x => core.Utils.isVariableSymbol(x))) { + denom = _.symfunction('gcd', core.Utils.arrayUnique(denomArgs)); + } else { + denom = __.gcd.apply( + null, + /** @type {[NerdamerSymbolType, NerdamerSymbolType, ...NerdamerSymbolType[]]} */ (denomArgs) + ); + } + // Divide product of all arguments by gcd of complementary terms + const div = _.divide(numer, denom); + return div; + }, + /** + * Divides one expression by another + * + * @param {NerdamerSymbolType} symbol1 + * @param {NerdamerSymbolType} symbol2 + * @returns {NerdamerSymbolType} + */ + divide(symbol1, symbol2) { + let den; + const factored = /** @type {NerdamerSymbolType} */ (__.Factor.factorInner(symbol1.clone())); + den = factored.getDenom(); + if (den.isConstant('all')) { + // Reset the denominator since we're not dividing by it anymore + den = new NerdamerSymbol(1); + } else { + symbol1 = /** @type {NerdamerSymbolType} */ ( + _.expand( + NerdamerSymbol.unwrapPARENS( + /** @type {NerdamerSymbolType} */ (_.multiply(factored, den.clone())) + ) + ) + ); + } + const result = __.div(symbol1, symbol2); + const remainder = /** @type {NerdamerSymbolType} */ (_.divide(result[1], symbol2)); + return /** @type {NerdamerSymbolType} */ ( + _.divide(/** @type {NerdamerSymbolType} */ (_.add(result[0], remainder)), den) + ); + }, + divWithCheck(symbol1, symbol2) { + const fail = [new NerdamerSymbol(0), symbol1.clone()]; + const div = __.div(symbol1, symbol2); + // GM safety check because __.div() produces b.s. sometimes + // see whether multiplication comes out clean + const a = symbol1.clone(); + let b = /** @type {NerdamerSymbolType} */ (_.multiply(div[0].clone(), symbol2.clone())); + b = /** @type {NerdamerSymbolType} */ (_.add(b, div[1].clone())); + let test = /** @type {NerdamerSymbolType} */ (_.subtract(a, b)); + test = /** @type {NerdamerSymbolType} */ (_.expand(test)); + // Test = __.Simplify._simplify(test); + + if (test.equals(0)) { + // Ok, seems good + return div; + } + // False alarm, get the default back + // console.log("nerdamer-prime: div failed: " + test); + return fail; + }, + div(symbol1, symbol2) { + // If all else fails then assume that division failed with + // a remainder of zero and the original quotient + const fail = [new NerdamerSymbol(0), symbol1.clone()]; + + try { + // Division by constants + if (symbol2.isConstant('all')) { + symbol1.each(x => { + x.multiplier = x.multiplier.divide(symbol2.multiplier); + }); + return [symbol1, new NerdamerSymbol(0)]; + } + // So that factorized symbols don't affect the result + symbol1 = /** @type {NerdamerSymbolType} */ (_.expand(symbol1)); + symbol2 = /** @type {NerdamerSymbolType} */ (_.expand(symbol2)); + // Special case. May need revisiting + if (symbol1.group === S && symbol2.group === CP) { + const x = symbol1.value; + const f = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(symbol2.clone(), x, true)); + if (symbol1.isLinear() && f.x && f.x.isLinear() && symbol2.isLinear()) { + const k = NerdamerSymbol.create(symbol1.multiplier); + return [ + /** @type {NerdamerSymbolType} */ (_.divide(k.clone(), f.a.clone())), + /** @type {NerdamerSymbolType} */ (_.divide(_.multiply(k, f.b), f.a)).negate(), + ]; + } + } + if (symbol1.group === S && symbol2.group === S) { + const r = /** @type {NerdamerSymbolType} */ (_.divide(symbol1.clone(), symbol2.clone())); + if (r.isConstant()) // We have a whole + { + return [r, new NerdamerSymbol(0)]; + } + return [new NerdamerSymbol(0), symbol1.clone()]; + } + const symbol1HasFunc = symbol1.hasFunc(); + const symbol2HasFunc = symbol2.hasFunc(); + let parseFuncs = false; + let subs; + + // Substitute out functions so we can treat them as regular variables + if (symbol1HasFunc || symbol2HasFunc) { + parseFuncs = true; + /** @type {Record} */ + const map = {}; + symbol1 = /** @type {NerdamerSymbolType} */ (_.parse(core.Utils.subFunctions(symbol1, map))); + symbol2 = /** @type {NerdamerSymbolType} */ (_.parse(core.Utils.subFunctions(symbol2, map))); + subs = core.Utils.getFunctionsSubs(map); + } + // Get a list of the variables + const vars = core.Utils.arrayUnique(variables(symbol1).concat(variables(symbol2))); + let quot; + let rem; + let den; + + // Treat imaginary numbers as variables + if (symbol1.isImaginary() || symbol2.isImaginary()) { + vars.push(core.Settings.IMAGINARY); + } + + if (vars.length === 1) { + const q = new Polynomial(symbol1).divide(new Polynomial(symbol2)); + quot = q[0].toSymbol(); + rem = q[1].toSymbol(); + } else { + vars.push(CONST_HASH); // This is for the numbers + const reconvert = function (arr) { + let symbol = new NerdamerSymbol(0); + for (let i = 0; i < arr.length; i++) { + const x = arr[i].toSymbol(); + symbol = /** @type {NerdamerSymbolType} */ (_.add(symbol, x)); + } + return symbol; + }; + + // Silly Martin. This is why you document. I don't remember now + const getUniqueMax = function (term, any) { + const max = Math.max.apply(null, term.terms); + let count = 0; + let idx; + + if (!any) { + for (let i = 0; i < term.terms.length; i++) { + if (term.terms[i].equals(max)) { + idx = i; + count++; + } + if (count > 1) { + return undefined; + } + } + } + if (any) { + for (let i = 0; i < term.terms.length; i++) { + if (term.terms[i].equals(max)) { + idx = i; + break; + } + } + } + return [max, idx, term]; + }; + + const tMap = core.Utils.toMapObj(vars); + const initSort = function (a, b) { + return b.sum.subtract(a.sum); + }; + + const s1 = symbol1.tBase(tMap).sort(initSort); + const s2 = symbol2.tBase(tMap).sort(initSort); + + // Tries to find an LT in the dividend that will satisfy division + const getDet = function (s, lookat) { + lookat ||= 0; + const det = s[lookat]; + const l = s.length; + if (!det) { + return undefined; + } + // Eliminate the first term if it doesn't apply + let umax = getUniqueMax(det); + for (let i = lookat + 1; i < l; i++) { + const term = s[i]; + const isEqual = det.sum.equals(term.sum); + if (!isEqual && umax) { + break; + } + if (isEqual) { + // Check the differences of their maxes. The one with the biggest difference governs + // e.g. x^2*y^3 vs x^2*y^3 is unclear but this isn't the case in x*y and x^2 + let max1; + let max2; + let idx1; + let idx2; + const l2 = det.terms.length; + for (let j = 0; j < l2; j++) { + const item1 = det.terms[j]; + const item2 = term.terms[j]; + if (typeof max1 === 'undefined' || item1.greaterThan(max1)) { + max1 = item1; + idx1 = j; + } + if (typeof max2 === 'undefined' || item2.greaterThan(max2)) { + max2 = item2; + idx2 = j; + } + } + // Check their differences + const d1 = max1.subtract(term.terms[idx1]); + const d2 = max2.subtract(det.terms[idx2]); + if (d2 > d1) { + umax = [max2, idx2, term]; + break; + } + if (d1 > d2) { + umax = [max1, idx1, det]; + break; + } + } else { + // Check if it's a suitable pick to determine the order + umax = getUniqueMax(term); + // If(umax) return umax; + if (umax) { + break; + } + } + umax = getUniqueMax(term); // Calculate a new unique max + } + + // If still no umax then any will do since we have a tie + if (!umax) { + return getUniqueMax(s[0], true); + } + let e; + let idx; + for (let i = 0; i < s2.length; i++) { + const cterm = s2[i].terms; + // Confirm that this is a good match for the denominator + idx = umax[1]; + if (idx === cterm.length - 1) { + return undefined; + } + e = cterm[idx]; + if (!e.equals(0)) { + break; + } + } + if (e.equals(0)) { + return getDet(s, ++lookat); + } // Look at the next term + + return umax; + }; + + const isLarger = function (a, b) { + if (!a || !b) { + return false; + } // It's empty so... + for (let i = 0; i < a.terms.length; i++) { + if (a.terms[i].lessThan(b.terms[i])) { + return false; + } + } + return true; + }; + + const target = isLarger(s1[0], s2[0]) && s1[0].count > s2[0].count ? s2 : s1; // Since the num is already larger than we can get the det from denom + const det = getDet(target); // We'll begin by assuming that this will let us know which term + const quotient = []; + if (det) { + let leadVar = det[1]; + const canDivide = function (a, b) { + if (a[0].sum.equals(b[0].sum)) { + return a.length >= b.length; + } + return true; + }; + + const tryBetterLeadVar = function (sym1, sym2, leadVarParam) { + const checked = []; + for (let i = 0; i < sym1.length; i++) { + const t = sym1[i]; + for (let j = 0; j < t.terms.length; j++) { + const cf = checked[j]; + const tt = t.terms[j]; + if (i === 0) { + checked[j] = tt; + } // Add the terms for the first one + else if (cf && !cf.equals(tt)) { + checked[j] = undefined; + } + } + } + for (let i = 0; i < checked.length; i++) { + const t = checked[i]; + if (t && !t.equals(0)) { + return i; + } + } + return leadVarParam; + }; + const sf = function (a, b) { + const l1 = a.len(); + const l2 = b.len(); + const blv = b.terms[leadVar]; + const alv = a.terms[leadVar]; + if (l2 > l1 && blv.greaterThan(alv)) { + return l2 - l1; + } + return blv.subtract(alv); + }; + + // Check to see if there's a better leadVar + leadVar = tryBetterLeadVar(s1, s2, leadVar); + // Reorder both according to the max power + s1.sort(sf); // Sort them both according to the leading variable power + s2.sort(sf); + + // Try to adjust if den is larger + const fdt = s2[0]; + const fnt = s1[0]; + + den = new MVTerm(new Frac(1), [], fnt.map); + if (fdt.sum.greaterThan(fnt.sum) && fnt.len() > 1) { + for (let i = 0; i < fnt.terms.length; i++) { + const d = fdt.terms[i].subtract(fnt.terms[i]); + if (d.equals(0)) { + den.terms[i] = new Frac(0); + } else { + const nd = d.add(new Frac(1)); + den.terms[i] = d; + for (let j = 0; j < s1.length; j++) { + s1[j].terms[i] = s1[j].terms[i].add(nd); + } + } + } + } + + let dividendLarger = isLarger(s1[0], s2[0]); + + let safety = 0; + const max = 200; + + while (dividendLarger && canDivide(s1, s2)) { + if (safety++ > max) { + throw new core.exceptions.InfiniteLoopError('Unable to compute!'); + } + + const q = s1[0].divide(s2[0]); + + quotient.push(q); // Add what's divided to the quotient + s1.shift(); // The first one is guaranteed to be gone so remove from dividend + for (let i = 1; i < s2.length; i++) { + // Loop through the denominator + const t = s2[i].multiply(q).generateImage(); + const l2 = s1.length; + // If we're subtracting from 0 + if (l2 === 0) { + t.coeff = t.coeff.neg(); + s1.push(t); + s1.sort(sf); + } + + for (let j = 0; j < l2; j++) { + const cur = s1[j]; + if (cur.getImg() === t.getImg()) { + cur.coeff = cur.coeff.subtract(t.coeff); + if (cur.coeff.equals(0)) { + core.Utils.remove(s1, j); + j--; // Adjust the iterator + } + break; + } + if (j === l2 - 1) { + t.coeff = t.coeff.neg(); + s1.push(t); + s1.sort(sf); + } + } + } + dividendLarger = isLarger(s1[0], s2[0]); + + if (!dividendLarger && s1.length >= s2.length) { + // One more try since there might be a terms that is larger than the LT of the divisor + for (let i = 1; i < s1.length; i++) { + dividendLarger = isLarger(s1[i], s2[0]); + if (dividendLarger) { + // Take it from its current position and move it to the front + s1.unshift(core.Utils.remove(s1, i)); + break; + } + } + } + } + } + + quot = reconvert(quotient); + rem = reconvert(s1); + + if (typeof den !== 'undefined') { + den = den.toSymbol(); + quot = _.divide(quot, den.clone()); + rem = _.divide(rem, den); + } + } + + // Put back the functions + if (parseFuncs) { + quot = _.parse(quot.text(), subs); + rem = _.parse(rem.text(), subs); + } + + return [quot, rem]; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return fail; + } + }, + line(v1, v2, x) { + if (core.Utils.isArray(v1)) { + v1 = core.Utils.convertToVector(/** @type {ExpressionParam[]} */ (v1)); + } + if (core.Utils.isArray(v2)) { + v2 = core.Utils.convertToVector(/** @type {ExpressionParam[]} */ (v2)); + } + const xVar = /** @type {NerdamerSymbolType} */ (_.parse(x || 'x')); + if (!core.Utils.isVector(v1) || !core.Utils.isVector(v2)) { + _.error(`Line expects a vector! Received "${v1}" & "${v2}"`); + } + const vec1 = /** @type {VectorType} */ (v1); + const vec2 = /** @type {VectorType} */ (v2); + const dx = _.subtract( + /** @type {NerdamerSymbolType} */ (vec2.e(1)).clone(), + /** @type {NerdamerSymbolType} */ (vec1.e(1)).clone() + ); + const dy = _.subtract( + /** @type {NerdamerSymbolType} */ (vec2.e(2)).clone(), + /** @type {NerdamerSymbolType} */ (vec1.e(2)).clone() + ); + const m = _.divide(dy, dx); + const a = _.multiply(xVar, /** @type {NerdamerSymbolType} */ (m).clone()); + const b = _.multiply(/** @type {NerdamerSymbolType} */ (vec1.e(1)).clone(), m); + return _.add(_.subtract(a, b), /** @type {NerdamerSymbolType} */ (vec1.e(2)).clone()); + }, + PartFrac: { + /** + * Creates a template for partial fraction decomposition + * + * @param {NerdamerSymbolType} den - The denominator + * @param {NerdamerSymbolType} denomFactors - Factored form of denominator + * @param {NerdamerSymbolType[]} fArray - Array to collect factor components + * @param {NerdamerSymbolType} v - The variable + * @returns {[NerdamerSymbolType[], (NerdamerSymbolType | VectorType | MatrixType)[], number[]]} + */ + createTemplate(den, denomFactors, fArray, v) { + // Clean up the denominator function by factors so it reduces nicely + den = __.Factor.factorInner(den); + + // Clean up factors. This is so inefficient but factors are wrapped in parens for safety + den.each((x, key) => { + if (x.group === FN && x.fname === '' && x.args[0].group === S) { + const y = x.args[0]; + if (den.symbols) { + delete den.symbols[key]; + den.symbols[y.value] = y; + } else { + den = x.args[0]; + } + } + }); + + let f; + let p; + let deg; + const factors = /** @type {NerdamerSymbolType[]} */ (denomFactors.collectFactors?.() || []); + const factorsVec = []; // A vector for the template + const degrees = []; + const m = new NerdamerSymbol(1); + + for (let i = 0; i < factors.length; i++) { + // Loop through the factors + const factor = NerdamerSymbol.unwrapPARENS(factors[i]); + // If in he for P^n where P is polynomial and n = integer + if (factor.power.greaterThan(1)) { + p = Number(factor.power); + f = factor.clone().toLinear(); // Remove the power so we have only the function + deg = Number(__.degree(f, v)); // Get the degree of f + // expand the factor + for (let j = 0; j < p; j++) { + const efactor = /** @type {NerdamerSymbolType} */ ( + _.pow(f.clone(), new NerdamerSymbol(j + 1)) + ); + fArray.push(efactor.clone()); + const d = _.divide(den.clone(), efactor.clone()); + degrees.push(deg); + factorsVec.push(d); + } + } else { + /* + Possible bug. + Removed: causes 1/(20+24*x+4*x^2) to result in (-1/64)*(5+x)^(-1)+(1/64)*(1+x)^(-1) + else if(factor.isConstant('all')) { + m = _.multiply(m, factor); + } + */ + // get the degree of the factor so we tack it on tot he factor. This should probably be an array + // but for now we note it on the symbol + deg = Number(__.degree(factor, v)); + fArray.push(factor); + let d = _.divide(den.clone(), factor.clone()); + d = /** @type {NerdamerSymbolType} */ ( + _.expand(NerdamerSymbol.unwrapPARENS(/** @type {NerdamerSymbolType} */ (d))) + ); + degrees.push(deg); + factorsVec.push(d); + } + } + // Put back the constant + fArray = /** @type {NerdamerSymbolType[]} */ (fArray.map(x => _.multiply(x, m.clone()))); + return [fArray, factorsVec, degrees]; + }, + /** + * Performs partial fraction decomposition + * + * @param {NerdamerSymbolType} symbol - The expression to decompose + * @param {NerdamerSymbolType} [v] - The variable + * @param {boolean} [asArray] - Whether to return as array + * @returns {NerdamerSymbolType | NerdamerSymbolType[] | VectorType | MatrixType} + */ + partfrac(symbol, v, asArray) { + const vars = variables(symbol); + + v ||= /** @type {NerdamerSymbolType} */ (_.parse(vars[0])); // Make wrt optional and assume first variable + try { + let nterms; + let div; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let r; + let num = /** @type {NerdamerSymbolType} */ (_.expand(symbol.getNum())); + const den = /** @type {NerdamerSymbolType} */ (_.expand(symbol.getDenom().toUnitMultiplier())); + // Move the entire multipier to the numerator + num.multiplier = symbol.multiplier; + // We only have a meaningful change if n factors > 1. This means that + // the returned group will be a CB + // collect the terms wrt the x + const vValue = v.value; + nterms = num.groupTerms(vValue); + // Divide out wholes if top is larger + if (Number(__.degree(num, v)) >= Number(__.degree(den, v))) { + div = __.div(num.clone(), /** @type {NerdamerSymbolType} */ (_.expand(den.clone()))); + r = /** @type {NerdamerSymbolType} */ (div[0]); // Remove the wholes + num = /** @type {NerdamerSymbolType} */ (div[1]); // Work with the remainder + nterms = num.groupTerms(vValue); // Recalculate the nterms + } else { + r = new NerdamerSymbol(0); + } + + if (Number(__.degree(den, v)) === 1) { + const q = /** @type {NerdamerSymbolType} */ (_.divide(num, den)); + if (asArray) { + return [r, q]; + } + return _.add(r, q); + } + // First factor the denominator. This means that the strength of this + // algorithm depends on how well we can factor the denominator. + const ofactors = __.Factor.factorInner(den); + // Create the template. This method will create the template for solving + // the partial fractions. So given x/(x-1)^2 the template creates A/(x-1)+B/(x-1)^2 + const template = __.PartFrac.createTemplate(den.clone(), ofactors, [], v); + const tfactors = template[0]; // Grab the factors + const factorsVec = template[1]; // Grab the factor vectors + const degrees = template[2]; // Grab the degrees + // make note of the powers of each term + /** @type {number[]} */ + const powers = [nterms.length]; + // Create the dterms vector + /** @type {NerdamerSymbolType[][]} */ + const dterms = []; + /** @type {NerdamerSymbolType[]} */ + const factors = []; + /** @type {NerdamerSymbolType[]} */ + const ks = []; + /** @type {NerdamerSymbolType} */ + let factor; + /** @type {number} */ + let deg; + factorsVec.forEach((x, idx) => { + factor = tfactors[idx]; + deg = degrees[idx]; + for (let i = 0; i < deg; i++) { + factors.push(factor.clone()); + const k = NerdamerSymbol.create(vValue, i); + const t = /** @type {NerdamerSymbolType} */ ( + _.expand(/** @type {NerdamerSymbolType} */ (_.multiply(x, k.clone()))) + ).groupTerms(vValue); + // Make a note of the power which corresponds to the length of the array + const p = t.length; + powers.push(p); + dterms.push(t); + ks.push(k.clone()); + } + }); + // Get the max power + const max = core.Utils.arrayMax(/** @type {number[]} */ (powers)); + + // Fill the holes and create a matrix + const c = new core.Matrix(core.Utils.fillHoles(nterms, max)).transpose(); + // For each of the factors we do the same + const M = new core.Matrix(); + for (let i = 0; i < dterms.length; i++) { + M.elements.push(core.Utils.fillHoles(dterms[i], max)); + } + + // Solve the system of equations + const partials = /** @type {MatrixType} */ (_.multiply(M.transpose().invert(), c)); + // The results are backwards to reverse it + // partials.elements.reverse(); + // convert it all back + if (asArray) { + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ + const retval = [r]; + partials.each((e, i) => { + const term = _.multiply(ks[i], _.divide(e, factors[i])); + retval.push(term); + }); + return /** @type {NerdamerSymbolType[]} */ (retval); + } + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let retval = r; + partials.each((e, i) => { + const term = _.multiply(ks[i], _.divide(e, factors[i])); + retval = _.add(retval, term); + }); + return retval; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // Try to group symbols + try { + if (symbol.isComposite()) { + // Group denominators + const denominators = {}; + + symbol.each(x => { + const d = x.getDenom(); + const n = x.getNum(); + const existing = denominators[d]; + denominators[d] = existing ? _.add(existing, n) : n; + }); + + let t = new NerdamerSymbol(0); + + for (const x in denominators) { + if (!Object.hasOwn(denominators, x)) { + continue; + } + t = /** @type {NerdamerSymbolType} */ (_.add(t, _.divide(denominators[x], _.parse(x)))); + } + + symbol = t; + } + } catch (e2) { + if (e2.message === 'timeout') { + throw e2; + } + } + } + return symbol; + }, + }, + /** + * Computes the degree of a polynomial + * + * @param {NerdamerSymbolType} symbol - The polynomial + * @param {NerdamerSymbolType} [v] - The variable + * @param {{ nd: NerdamerSymbolType[]; sd: (NerdamerSymbolType | FracType)[]; depth: number }} [o] - Options for + * tracking + * @returns {NerdamerSymbolType} + */ + degree(symbol, v, o) { + o ||= { + nd: [], // Numeric degrees (stored as NerdamerSymbol) + sd: [], // Symbolic degrees + depth: 0, // Call depth + }; + + if (!v) { + const vars = variables(symbol); + // The user must specify the variable for multivariate + if (vars.length > 1) { + throw new Error('You must specify the variable for multivariate polynomials!'); + } + // If it's empty then we're dealing with a constant + if (vars.length === 0) { + return new NerdamerSymbol(0); + } + // Assume the variable for univariate + v = _.parse(vars[0]); + } + + // Store the group + const g = symbol.group; + // We're going to trust the user and assume no EX. Calling isPoly + // would eliminate this but no sense in checking twice. + if (symbol.isComposite()) { + symbol = symbol.clone(); + symbol.distributeExponent(); + symbol.each(x => { + o.depth++; // Mark a depth increase + __.degree(x, v, o); + o.depth--; // We're back + }); + } else if (symbol.group === CB) { + symbol.each(x => { + o.depth++; + __.degree(x, v, o); + o.depth++; + }); + } else if (g === EX && symbol.value === v.value) { + o.sd.push(symbol.power.clone()); + } else if (g === S && symbol.value === v.value) { + o.nd.push(/** @type {NerdamerSymbolType} */ (_.parse(symbol.power))); + } else { + o.nd.push(new NerdamerSymbol(0)); + } + + // Get the max out of the array - arrayMax uses valueOf() on each symbol to compare numerically + /** @type {number | undefined} */ + const deg = + o.nd.length > 0 + ? core.Utils.arrayMax(/** @type {number[]} */ (/** @type {unknown} */ (o.nd))) + : undefined; + + if (o.depth === 0 && o.sd.length > 0) { + if (deg !== undefined) { + // Convert numeric deg back to symbol for the max function + o.sd.unshift(/** @type {NerdamerSymbolType} */ (_.parse(deg))); + } + return /** @type {NerdamerSymbolType} */ ( + _.symfunction('max', /** @type {NerdamerSymbolType[]} */ (o.sd)) + ); + } + // Convert numeric degree to symbol + return /** @type {NerdamerSymbolType} */ (_.parse(deg ?? 0)); + }, + /** + * Attempts to complete the square of a polynomial + * + * @param {NerdamerSymbolType} symbol + * @param {string | NerdamerSymbolType} v - The variable to complete the square with respect to + * @param {boolean} raw + * @returns {object | NerdamerSymbol[]} + * @throws {Error} + */ + sqComplete(symbol, v, raw) { + if (!core.Utils.isSymbol(v)) { + v = _.parse(v); + } + const stop = function (msg) { + msg ||= 'Stopping'; + throw new core.exceptions.ValueLimitExceededError(msg); + }; + // If not CP then nothing to do + if (!symbol.isPoly(true)) { + stop('Must be a polynomial!'); + } + + // Declare vars + const br = core.Utils.inBrackets; + // Make a copy + symbol = symbol.clone(); + const deg = core.Algebra.degree(symbol, v); // Get the degree of polynomial + // must be in form ax^2 +/- bx +/- c + if (!deg.equals(2)) { + stop(`Cannot complete square for degree ${deg.text()}`); + } + // Get the coeffs + const coeffs = core.Algebra.coeffs(symbol, v); + const a = coeffs[2]; + // Store the sign + const sign = coeffs[1].sign(); + // Divide the linear term by two and square it + const b = _.divide(coeffs[1], new NerdamerSymbol(2)); + // Add the difference to the constant + const c = _.pow(b.clone(), new NerdamerSymbol(2)); + const sqrtA = math.sqrt(a); + const e = _.divide(math.sqrt(c), sqrtA.clone()); + // Calculate d which is the constant + const d = _.subtract(coeffs[0], _.pow(e.clone(), new NerdamerSymbol(2))); + if (raw) { + return [a, b, d]; + } + // Compute the square part + const sym = _.parse(br(`${sqrtA.clone()}*${v}${sign < 0 ? '-' : '+'}${e}`)); + return { + a: sym, + c: d, + f: _.add(_.pow(sym.clone(), new NerdamerSymbol(2)), d.clone()), + }; + }, + Simplify: { + /** + * @param {NerdamerSymbolType} symbol + * @returns {[NerdamerSymbolType, NerdamerSymbolType, NerdamerSymbolType]} + */ + strip(symbol) { + const c = /** @type {NerdamerSymbolType} */ (_.parse(symbol.multiplier)); + symbol.toUnitMultiplier(); + const p = /** @type {NerdamerSymbolType} */ (_.parse(symbol.power)); + symbol.toLinear(); + return [c, p, symbol]; + }, + /** + * @param {[NerdamerSymbolType, NerdamerSymbolType] | NerdamerSymbolType[]} cp + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + unstrip(cp, symbol) { + const c = cp[0]; + const p = cp[1]; + const result = /** @type {NerdamerSymbolType} */ (_.multiply(c, _.pow(symbol, p))); + return result; + }, + /** + * @param {NerdamerSymbolType} num + * @param {NerdamerSymbolType} den + * @returns {NerdamerSymbolType} + */ + complexSimp(num, den) { + const r1 = num.realpart(); + const i1 = num.imagpart(); + const r2 = den.realpart(); + const i2 = den.imagpart(); + // Apply complex arithmatic rule + const ac = _.multiply(r1.clone(), r2.clone()); + const bd = _.multiply(i1.clone(), i2.clone()); + const bc = _.multiply(r2.clone(), i1); + const ad = _.multiply(r1, i2.clone()); + const cd = _.add(_.pow(r2, new NerdamerSymbol(2)), _.pow(i2, new NerdamerSymbol(2))); + + return /** @type {NerdamerSymbolType} */ ( + _.divide(_.add(_.add(ac, bd), _.multiply(_.subtract(bc, ad), NerdamerSymbol.imaginary())), cd) + ); + }, + /** + * Simplify trigonometric expressions. + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + trigSimp(symbol) { + let workDone = true; + let iterations = 0; + while (workDone && symbol.containsFunction(['cos', 'sin', 'tan'])) { + iterations++; + workDone = false; + symbol = symbol.clone(); + // Remove power and multiplier + const symArray = __.Simplify.strip(symbol); + symbol = symArray.pop(); + // The default return value is the symbol + let retval = symbol.clone(); + + // Rewrite the symbol + if (symbol.group === CP) { + let sym = new NerdamerSymbol(0); + symbol.each(x => { + // Rewrite the function + const tr = __.Simplify.trigSimp(x.fnTransform()); + sym = /** @type {NerdamerSymbolType} */ (_.add(sym, tr)); + }, true); + + // Put back the power and multiplier and return + retval = /** @type {NerdamerSymbolType} */ ( + _.pow( + _.multiply(new NerdamerSymbol(symbol.multiplier), sym), + new NerdamerSymbol(/** @type {FracType} */ (symbol.power)) + ) + ); + workDone = retval.text() !== symbol.text(); + } else if (symbol.group === CB) { + const n = symbol.getNum(); + const d = symbol.getDenom(); + + // Try for tangent or fractions with tangent + if ( + n.fname === 'sin' && + d.fname === 'cos' && + n.args[0].equals(d.args[0]) && + /** @type {FracType} */ (n.power).equals(/** @type {FracType} */ (d.power)) + ) { + retval = /** @type {NerdamerSymbolType} */ ( + _.parse( + core.Utils.format( + '(({1})/({0}))*tan({2})^({3})', + d.multiplier, + n.multiplier, + n.args[0], + n.power + ) + ) + ); + workDone = true; + } else if ( + n.fname === 'tan' && + d.fname === 'sin' && + n.args[0].equals(d.args[0]) && + /** @type {FracType} */ (n.power).equals(/** @type {FracType} */ (d.power)) + ) { + retval = /** @type {NerdamerSymbolType} */ ( + _.parse( + core.Utils.format( + '(({1})/({0}))*cos({2})^(-({3}))', + d.multiplier, + n.multiplier, + n.args[0], + n.power + ) + ) + ); + workDone = true; + } else { + let t = new NerdamerSymbol(1); + const state = { workDone }; + retval.each(x => { + if (x.fname === 'tan') { + x = _.parse( + core.Utils.format( + '({0})*sin({1})^({2})/cos({1})^({2})', + x.multiplier, + __.Simplify._simplify(x.args[0]), + x.power + ) + ); + state.workDone = true; + } else if (x.containsFunction(['cos', 'sin', 'tan'])) { + // Rewrite the function + const y = __.Simplify.trigSimp(x); + if (!x.equals(y)) { + x = y; + state.workDone = true; + } + } + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, x)); + }); + workDone = state.workDone; + retval = /** @type {NerdamerSymbolType} */ (t); + } + } else if ((symbol.fname === 'cos' || symbol.fname === 'sin') && symbol.args[0].group === CP) { + // Capture cos(x-pi/2) => sin(x) and sin(x+pi/2) = cos(x) + // but generalized + // test the sum for presence of a "n*pi/2" summands + let count = 0; + let newArg = new NerdamerSymbol(0); + const piOverTwo = _.parse('pi/2'); + symbol.args[0].each(x => { + let c = /** @type {NerdamerSymbolType} */ (_.divide(x.clone(), piOverTwo.clone())); + c = __.Simplify._simplify(c); + c = core.Utils.evaluate(c); + if (isInt(c)) { + count += c.multiplier.num.toJSNumber(); + } else { + newArg = /** @type {NerdamerSymbolType} */ (_.add(newArg, x)); + } + }); + if (count) { + count += symbol.fname === 'cos' ? 1 : 0; + count %= 4; + count += count < 0 ? 4 : 0; + // Console.log(count); + // debugger; + const results = ['sin({0})', 'cos({0})', '-sin({0})', '-cos({0})']; + const s = core.Utils.format(results[count], String(newArg)); + retval = _.parse(s); + workDone = true; + } else if (Object.keys(symbol.args[0].symbols).length > 1) { + // Apply sin(a+-b) => sin(a)cos(b)+-cos(a)sin(b) + // and cos(a+-b) => cos(a)cos(b)-+sin(a)sin(b) + const arg = symbol.args[0].clone(); + const summands = Object.values(arg.symbols); + const a = summands[0]; + const b = summands.slice(1); + const bStr = b.map(x => `(${x.text()})`).join('+'); + let s; + if (symbol.fname === 'sin') { + s = core.Utils.format('sin({0})cos({1})+sin({1})cos({0})', a, bStr); + } else { + s = core.Utils.format('cos({0})cos({1})-sin({1})sin({0})', a, bStr); + } + retval = _.parse(s); + workDone = true; + } + } else if ( + (symbol.fname === 'cos' || symbol.fname === 'sin') && + symbol.args[0].multiplier.sign() === -1 + ) { + // Sin(-x) => -sin(x), cos(-x) => cos(x) + // remove the minus from the argument + const newArg = symbol.args[0].clone().negate(); + // Make the new trig call + let s = core.Utils.format(`${symbol.fname}({0})`, newArg); + if (symbol.fname === 'sin') { + s = `-${s}`; + } + retval = _.parse(s); + // Continue with the simpler form + workDone = true; + } + if (symbol.fname === 'sin' && symbol.args[0].multiplier.equals(2) && !symbol.args[0].equals(2)) { + // Sin(2x) => 2sin(x)cos(x) + // remove the minus from the argument + const newArg = symbol.args[0].clone().toUnitMultiplier(); + // Make the new trig call + const s = core.Utils.format('2sin({0})cos({0})', newArg); + retval = _.parse(s); + // Continue with the simpler form + workDone = true; + } + + retval = __.Simplify.unstrip(symArray, retval).distributeMultiplier(); + symbol = retval; + // Safety check: prevent infinite loops + if (iterations > 10) { + break; + } + } + + return symbol; + }, + logArgSimp(fn, term) { + // Console.log("----- log term: "+ term.text()); + // note: use symbol.equals + if (term.value === '1' || term.value === String(1)) { + return new NerdamerSymbol(0); + } + // Work on all factors of the arg term + // inintialize the sum + let r = new NerdamerSymbol(0); + // First up: the numerator's multiplier + const m = term.multiplier.clone(); + // Console.log("---- multiplier: "+m); + term.toUnitMultiplier(); + // Console.log("term with unit multiplier: "+term); + + if (!m.equals(1)) { + const a = core.Utils.format('({0}({1}))', fn, m); + // Console.log("m transformed: "+a); + r = /** @type {NerdamerSymbolType} */ (_.add(r, _.parse(a))); + // Console.log("m r: "+r.text()); + } + // Now each factor, with its power + // console.log("---- term factors"); + if (term.group === CB) { + // Product + term.each(x => { + x = x.clone(); + const p = x.power.clone(); + // Note: there will be no multiplier + // strip modifies the original + __.Simplify.strip(x); + // Console.log("factor: "+m+" * "+x+"^"+p+" = "+original); + const a = core.Utils.format('(({1})*{0}({2}))', fn, p, x); + // Console.log("factor transformed: "+a); + r = /** @type {NerdamerSymbolType} */ (_.add(r, _.parse(a))); + // Console.log("running sum: "+r.text()); + }); + } else { + // Everything else + const x = term.clone(); + const p = x.power.clone(); + // Note: there will be no multiplier + // strip modifies the original + __.Simplify.strip(x); + // Console.log("factor: "+m+" * "+x+"^"+p+" = "+original); + const a = core.Utils.format('(({1})*{0}({2}))', fn, p, x); + // Console.log("factor transformed: "+r+"+"+a); + r = /** @type {NerdamerSymbolType} */ (_.add(r, _.parse(a))); + // Console.log("running sum: "+r.text()); + } + // Console.log("result: "+r.text()); + return r; + }, + logSimp(symbol) { + if (symbol.group === FN && (symbol.fname === 'log' || symbol.fname === 'log10')) { + // Console.log(); + // console.log("Initial: "+symbol.text()); + // remove power and multiplier + const _original = symbol.clone(); + const symArray = __.Simplify.strip(symbol); + symbol = symArray.pop(); + + // Work on the argument + const arg = symbol.args[0].clone(); + const n = arg.getNum().clone(); + // Console.log("n: "+n.text()); + const d = arg.getDenom().clone(); + // Console.log("d: "+d.text()); + const fn = symbol.fname; + + let retval = __.Simplify.logArgSimp(fn, n); + if (!d.equals(1)) { + const rd = __.Simplify.logArgSimp(fn, d); + retval = /** @type {NerdamerSymbolType} */ (_.subtract(retval, rd)); + } + + retval = __.Simplify.unstrip(symArray, retval).distributeMultiplier(); + symbol = retval; + // Console.log("result: "+symbol.text()); + } else if (symbol.containsFunction(['log', 'log10'])) { + for (const termkey in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, termkey)) { + continue; + } + const term = symbol.symbols[termkey]; + symbol.symbols[termkey] = __.Simplify.logSimp(term); + } + } + + return symbol; + }, + /** + * Compresses sqrt expressions in fractions. + * + * @param {NerdamerSymbolType} symbol The symbol + * @param {NerdamerSymbolType} num Numerator + * @param {NerdamerSymbolType} den Denominator + * @returns {NerdamerSymbolType} + */ + _sqrtCompression(symbol, num, den) { + // Return symbol; + // preserve power and multiplier + const symArray = __.Simplify.strip(symbol); + + // Helper functions + const isABS = s => s.fname === 'abs'; + const getArg = s => s.args[0]; + const absArg = s => (isABS(s) ? getArg(s) : null); + const isUnit = s => s.type === S && s.value.startsWith('baseunit_'); + + // Main workhorse function + const cancel = (a, sqrt) => { + const sqrtArg = getArg(sqrt); + // Abs(x):sqrt(x) => sqrt(x) + if (sqrtArg.equals(absArg(a))) { + return [sqrt, null]; + } + // Unit(x):sqrt(x) => sqrt(x) + if (sqrtArg.equals(a) && isUnit(a)) { + return [sqrt, null]; + } + + // N*sqrt(a):d*sqrt(x) => (n/d)*sqrt(a/x) + // if (a.isSQRT()) { + // let newArg = getArg(a); + // let m = new NerdamerSymbol(a.multiplier); + // m = _.divide(m, sqrt.multiplier); + // newArg = _.divide(newArg, sqrtArg); + // const combinedSqrt = core.Utils.format('sqrt({0})', newArg); + // const result = _.multiply(new NerdamerSymbol(m), _.parse(combinedSqrt)); + // return [result, null]; + // } + + // nothing to be done + return [null, sqrt]; + }; + + let workDone; + let totalWorkDone = false; + + const cancelTerms = (top, bottom) => { + for (let i = 0; i < top.length; i++) { + // Examine the first top symbol + let sqrt = top[i]; + if (!sqrt.isSQRT()) { + continue; + } + // It's a sqrt. try to cancel it against each + // bottom term + for (let j = 0; j < bottom.length; j++) { + let term = bottom[j]; + [term, sqrt] = cancel(term, sqrt); + if (term !== null) { + // We found a match, substitute the remains and exit here + bottom[j] = term; + workDone = true; + totalWorkDone = true; + break; + } + } + // Whatever remains of sqrt gets put back + top[i] = sqrt; + top = top.filter(x => x); + bottom = bottom.filter(x => x); + } + return [top, bottom]; + }; + + // Look for sqrt terms in products in num and den + // if we find any, combine them with other terms + + // first, collect all factors in numerator and denominator + let numSymbols = num.collectFactors(); + let denSymbols = den.collectFactors(); + + // Now cancel terms until nothing to cancel was found + do { + workDone = false; + [numSymbols, denSymbols] = cancelTerms(numSymbols, denSymbols); + [denSymbols, numSymbols] = cancelTerms(denSymbols, numSymbols); + } while (workDone); + + if (totalWorkDone) { + // Reassemble the fraction symbol + symbol = /** @type {NerdamerSymbolType} */ ( + numSymbols.reduce( + (acc, s) => (acc = /** @type {NerdamerSymbolType} */ (_.multiply(acc, s))), + new NerdamerSymbol(1) + ) + ); + symbol = /** @type {NerdamerSymbolType} */ ( + denSymbols.reduce( + (acc, s) => (acc = /** @type {NerdamerSymbolType} */ (_.divide(acc, s))), + symbol + ) + ); + } + + // Add power etc. back in + symbol = __.Simplify.unstrip(symArray, symbol); + + return symbol; + }, + + fracSimp(symbol) { + // Try a quick simplify of imaginary numbers + let den = symbol.getDenom(); + let num = symbol.getNum(); + + if (num.isImaginary() && den.isImaginary()) { + symbol = __.Simplify.complexSimp(num, den); + } + + if (symbol.isComposite()) { + if (/** @type {FracType} */ (symbol.power).gt(1)) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + + const symbols = symbol.collectSymbols(); + // Assumption 1. + // since it's a composite, it has a length of at least 1 + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let retval; + /** @type {NerdamerSymbolType} */ + let a; + /** @type {NerdamerSymbolType} */ + let b; + /** @type {NerdamerSymbolType} */ + let d1; + /** @type {NerdamerSymbolType} */ + let d2; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let n1; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let n2; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let s; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let x; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let y; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let c; + a = /** @type {NerdamerSymbolType} */ (symbols.pop()); // Grab the first symbol + // loop through each term and make denominator common + while (symbols.length) { + b = /** @type {NerdamerSymbolType} */ (symbols.pop()); // Grab the second symbol + d1 = /** @type {NerdamerSymbolType} */ (_.parse(a.getDenom())); + d2 = /** @type {NerdamerSymbolType} */ (_.parse(b.getDenom())); + n1 = a.getNum(); + n2 = b.getNum(); + c = _.multiply(d1.clone(), d2.clone()); + x = _.multiply(n1, d2); + y = _.multiply(n2, d1); + s = _.add(x, y); + a = /** @type {NerdamerSymbolType} */ (_.divide(s, c)); + } + den = /** @type {NerdamerSymbolType} */ (_.expand(a.getDenom())); + num = /** @type {NerdamerSymbolType} */ (_.expand(a.getNum())); + // Simplify imaginary + if (num.isImaginary() && den.isImaginary()) { + retval = __.Simplify.complexSimp(num, den); + } else { + retval = _.divide(num, den); + } + + // We've already hit the simplest form so return that + if (/** @type {NerdamerSymbolType} */ (retval).equals(symbol)) { + return symbol; + } + + // Otherwise simplify it some more + return __.Simplify._simplify(retval); + } + symbol = __.Simplify._sqrtCompression( + symbol, + /** @type {NerdamerSymbolType} */ (num), + /** @type {NerdamerSymbolType} */ (den) + ); + symbol = /** @type {NerdamerSymbolType} */ (__.Simplify.simpleFracSimp(symbol)); + return symbol; + }, + simpleFracSimp(symbol) { + let den = /** @type {NerdamerSymbolType} */ (symbol.getDenom()); + let num = /** @type {NerdamerSymbolType} */ (symbol.getNum()); + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let retval; + den = /** @type {NerdamerSymbolType} */ (_.expand(den)); + num = /** @type {NerdamerSymbolType} */ (_.expand(num)); + // Simplify imaginary + if (num.isImaginary() && den.isImaginary()) { + retval = __.Simplify.complexSimp(num, den); + } else { + retval = _.divide(num, den); + } + // We've already hit the simplest form so return that + if (/** @type {NerdamerSymbolType} */ (retval).equals(symbol)) { + return symbol; + } + // Otherwise simplify it some more + // retval = __.Simplify._simplify(retval); + return retval; + }, + ratSimp(symbol) { + if (symbol.group === CB) { + const den = symbol.getDenom(); + const num = symbol.getNum().distributeMultiplier(); + const d = __.Simplify.fracSimp(den); + const n = __.Simplify.fracSimp(num); + symbol = /** @type {NerdamerSymbolType} */ (_.divide(n, d)); + } + return symbol; + }, + sqrtSimp(symbol, _sym_array) { + let retval; + let workDone = false; + + const original = symbol.clone(); + try { + // Debuglevel(1); + // debugout("input: "+symbol.toString()); + + if (symbol.isSQRT()) { + // Symbol is itself sqrt + // save outer multiplier + const mOuter = symbol.multiplier.clone(); + + // Now factor it + const sqrtArg = symbol.args[0].clone(); + const factored = __.Factor.factorInner(sqrtArg); + + // Get a sanitized version of the argument's multiplier + const m = _.parse(factored.multiplier); + // And its sign + const sign = m.sign(); + + // Make an initial return value + retval = new NerdamerSymbol(1); + let arg; + + if (factored.group === CB) { + // Monomial arg + let rem = new NerdamerSymbol(1); + + factored.each(x => { + x = _.parse(x); + if (x.group === N) { + const trial = _.sqrt(x.clone()); + + // Multiply back sqrt if it's an integer otherwise just put back the number + if (isInt(trial)) { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, trial)); + } else { + rem = /** @type {NerdamerSymbolType} */ (_.multiply(rem, x)); + } + } else { + rem = /** @type {NerdamerSymbolType} */ (_.multiply(rem, x)); + } + }); + const t = /** @type {NerdamerSymbolType} */ (_.multiply(rem, _.parse(sign))); + arg = /** @type {NerdamerSymbolType} */ (_.sqrt(t.clone())); + + // Expand if it's imaginary + if (arg.isImaginary()) { + arg = /** @type {NerdamerSymbolType} */ ( + _.sqrt(/** @type {NerdamerSymbolType} */ (_.expand(t.clone()))) + ); + } + } else { + // Put together the argument with the sign + // but without the multiplier + arg = factored.clone().toUnitMultiplier(); + arg = /** @type {NerdamerSymbolType} */ (_.multiply(arg, new NerdamerSymbol(sign))); + arg = /** @type {NerdamerSymbolType} */ (_.sqrt(arg)); + } + + // Put the result back + retval = _.multiply(retval, arg); + // Put back the multiplier + retval = _.pow(retval, _.parse(symbol.power)); + retval = _.multiply(retval, _.sqrt(m.abs())); + retval = _.multiply(retval, _.parse(mOuter)); + workDone = true; + } else if (symbol.isComposite() && symbol.isLinear()) { + // Polynomial or CP => sum of things + retval = new NerdamerSymbol(0); + symbol.each(x => { + retval = _.add(retval, __.Simplify.sqrtSimp(x)); + }, true); + // Put back the multiplier and power + retval = _.pow(retval, _.parse(symbol.power)); + retval = _.multiply(retval, _.parse(symbol.multiplier)); + workDone = true; + } else if (symbol.group === CB) { + // Monomial + retval = new NerdamerSymbol(1); + symbol.each(x => { + const simp = __.Simplify.sqrtSimp(x); + retval = _.multiply(retval, simp); + }); + // Put back the power and multiplier + retval = _.pow(retval, _.parse(symbol.power)); + retval = _.multiply(retval, _.parse(symbol.multiplier)); + workDone = true; + } + + if (!workDone) { + if (retval && !isInt(retval)) { + // If we can't even pull an integer out, revert + // to the cautious fallback + retval = null; + } + } + + // Fallback: original symbol + retval ||= _.parse(symbol); + // Debugout("result: "+retval.toString()); + // debugout(""); + return retval; + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + // Error in sqrtsimp - return original symbol + return original; + } finally { + // Debuglevel(-1); + } + }, + /** + * Unused. The goal is to substitute out patterns but it currently doesn't work. + * + * @param {NerdamerSymbolType} symbol + * @returns {Array} The symbol and the matched patterns + */ + patternSub(symbol) { + const patterns = {}; + + const hasCP = function (sym) { + let found = false; + sym.each(x => { + if (x.group === CP) { + found = true; + } else if (x.symbols) { + found = hasCP(x); + } + }); + + return found; + }; + + const collect = function (sym) { + // We loop through each symbol looking for anything in the simplest + // form of ax+byz+... + sym.each(x => { + // Items of group N,P,S, need to apply + if (!x.symbols && x.group !== FN) { + return; + } + + // Check to see if it has any symbols of group CP + // Get the patterns in that symbol instead if it has anything of group CP + if (hasCP(x)) { + collect(x); + } else if (!patterns[x.value]) { + const u = core.Utils.getU(symbol); + // Get a u value and mark it for subsitution + patterns[x.value] = u; + symbol = symbol.sub(x.value, u); + } + }, true); + }; + + // Collect a list of patterns + collect(symbol); + + return [symbol, patterns]; + }, + simplify(symbol) { + if (symbol.simplify) { + return symbol.simplify(); + } + let retval = __.Simplify._simplify(symbol); + retval = retval.pushMinus(); + retval = _.parse(retval); + return retval; + }, + _simplify(symbol) { + // Debuglevel(1); + // debugout("input to _simplify: "+symbol.text()); + // try { + // remove the multiplier to make calculation easier; + const symArray = __.Simplify.strip(/** @type {NerdamerSymbolType} */ (symbol).clone()); + symbol = /** @type {NerdamerSymbolType | VectorType | MatrixType} */ (symArray.pop()); + // Remove gcd from denominator + symbol = __.Simplify.fracSimp(/** @type {NerdamerSymbolType} */ (symbol)); + // Nothing more to do + if ( + /** @type {NerdamerSymbolType} */ (symbol).isConstant() || + /** @type {NerdamerSymbolType} */ (symbol).group === core.groups.S + ) { + symArray.push(/** @type {NerdamerSymbolType} */ (symbol)); + const ret = __.Simplify.unstrip(symArray, /** @type {NerdamerSymbolType} */ (symbol)); + // Debugout("final result: "+ret.text()); + return ret; + } + // Console.log("array: "+symArray); + + // let patterns; + + let simplified = /** @type {NerdamerSymbolType} */ (symbol).clone(); // Make a copy + + // [simplified, patterns] = __.Simplify.patternSub(symbol); + + // Simplify sqrt within the symbol + // todo: why does this break calculus tests? + simplified = /** @type {NerdamerSymbolType} */ (__.Simplify.sqrtSimp(simplified, symArray)); + + // Try trig simplificatons e.g. cos(x)^2+sin(x)^2 + simplified = __.Simplify.trigSimp(simplified); + + // Try log simplificatons e.g. log(a/b)=> log(a)-log(b) + simplified = __.Simplify.logSimp(simplified); + + // Simplify common denominators + simplified = /** @type {NerdamerSymbolType} */ (__.Simplify.ratSimp(simplified)); + + // First go for the "cheapest" simplification which may eliminate + // your problems right away. factor -> evaluate. Remember + // that there's no need to expand since factor already does that + + // console.log("before factor: "+simplified.text()); + simplified = __.Factor.factorInner(simplified); + // Console.log("after factor: "+simplified.text()); + + // If the simplified is a sum then we can make a few more simplifications + // e.g. simplify(1/(x-1)+1/(1-x)) as per issue #431 + // console.log("before sums: "+simplified.text()); + if (simplified.group === core.groups.CP && simplified.isLinear()) { + const m = simplified.multiplier.clone(); + simplified.toUnitMultiplier(); // Strip the multiplier + let r = new NerdamerSymbol(0); + // Return the sum of simplifications + simplified.each(x => { + const s = __.Simplify._simplify(x); + r = /** @type {NerdamerSymbolType} */ (_.add(r, s)); + }); + simplified = r; + // Mult on back the multiplier we saved here + simplified = /** @type {NerdamerSymbolType} */ (_.multiply(simplified, new NerdamerSymbol(m))); + if (simplified.multiplier.equals(-1)) { + simplified.distributeMultiplier(); + } + // Place back original multiplier and return + simplified = __.Simplify.unstrip(symArray, simplified); + // Debugout("final result: "+simplified.text()); + return simplified; + } + + // Place back original multiplier and return + simplified = __.Simplify.unstrip(symArray, simplified); + // Debugout("final result: "+simplified.text()); + return simplified; + // } finally { + // // debuglevel(-1); + // } + }, + }, + + Classes: { + Polynomial, + Factors: /** @type {FactorsConstructor} */ (/** @type {unknown} */ (Factors)), + MVTerm, + }, + }); + + // Add a link to simplify + core.Expression.prototype.simplify = function simplify() { + core.Utils.armTimeout(); + try { + let retval; + // Equation? + if (typeof this.symbol.LHS === 'undefined') { + retval = new core.Expression(__.Simplify.simplify(this.symbol)); + } else { + // Don't have access to equation here, so we clone instead + const eq = this.symbol.clone(); + eq.LHS = __.Simplify.simplify(eq.LHS); + eq.RHS = __.Simplify.simplify(eq.RHS); + retval = eq; + } + return retval; + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + return this; + } finally { + core.Utils.disarmTimeout(); + } + }; + + core.Collection.prototype.simplify = function simplify() { + this.elements = this.elements.map(e => __.Simplify.simplify(e)); + return this; + }; + + core.Matrix.prototype.simplify = function simplify() { + this.elements = this.elements.map(row => row.map(e => __.Simplify.simplify(e))); + return this; + }; + + nerdamer.useAlgebraDiv = function useAlgebraDiv() { + const _originalDivide = (__.divideFn = _.divide); + let calls = 0; // Keep track of how many calls were made + _.divide = function divide(a, b) { + calls++; + let ans; + if (calls === 1) // Check if this is the first call. If it is use algebra divide + { + ans = core.Algebra.divide(/** @type {NerdamerSymbolType} */ (a), /** @type {NerdamerSymbolType} */ (b)); + } // Otherwise use parser divide + else { + ans = divide(a, b); + } + calls = 0; // Reset the number of calls back to none + return ans; + }; + }; + + nerdamer.useParserDiv = function useParserDiv() { + if (__.divideFn) { + _.divide = __.divideFn; + } + delete __.divideFn; + }; + + nerdamer.register([ + { + name: 'factor', + visible: true, + numargs: 1, + build() { + return __.Factor.factor; + }, + }, + { + name: 'simplify', + visible: true, + numargs: 1, + build() { + return __.Simplify.simplify; + }, + }, + { + name: 'gcd', + visible: true, + numargs: [1], + build() { + return __.gcd; + }, + }, + { + name: 'lcm', + visible: true, + numargs: [1], + build() { + return __.lcm; + }, + }, + { + name: 'roots', + visible: true, + numargs: -1, + build() { + return __.roots; + }, + }, + { + name: 'divide', + visible: true, + numargs: 2, + build() { + return __.divide; + }, + }, + { + name: 'div', + visible: true, + numargs: 2, + build() { + return __.div; + }, + }, + { + name: 'partfrac', + visible: true, + numargs: [1, 2], + build() { + return __.PartFrac.partfrac; + }, + }, + { + name: 'deg', + visible: true, + numargs: [1, 2], + build() { + return __.degree; + }, + }, + { + name: 'coeffs', + visible: true, + numargs: [1, 2], + build() { + const f = function (...args) { + const coeffs = __.coeffs(/** @type {NerdamerSymbolType} */ (args[0]), args[1]); + return new core.Vector(coeffs); + }; + return f; + }, + }, + ]); + + // Register coeffs with direct access to nerdamer that preserves symbolic constants + // The standard updateAPI wrapper uses PARSE2NUMBER which converts pi, e, sqrt(2) to rationals + // This version parses arguments without PARSE2NUMBER to preserve symbolic constants + /** @type {any} */ (nerdamer).coeffs = function coeffs(...args) { + const parser = core.PARSER; + // Parse arguments WITHOUT PARSE2NUMBER to preserve symbolic constants like pi, e, sqrt(2) + for (let i = 0; i < args.length; i++) { + if (typeof args[i] === 'string') { + args[i] = parser.parse(/** @type {string} */ (args[i])); + } else if (args[i] && /** @type {ExpressionType} */ (args[i]).symbol) { + // It's an Expression, get the symbol + args[i] = /** @type {ExpressionType} */ (args[i]).symbol.clone(); + } else if (core.Utils.isSymbol(args[i])) { + args[i] = /** @type {NerdamerSymbolType} */ (args[i]).clone(); + } + } + const resultCoeffs = __.coeffs(/** @type {NerdamerSymbolType} */ (args[0]), args[1]); + return new core.Expression(/** @type {VectorType} */ (new core.Vector(resultCoeffs))); + }; + + nerdamer.register([ + { + name: 'line', + visible: true, + numargs: [2, 3], + build() { + return __.line; + }, + }, + { + name: 'sqcomp', + visible: true, + numargs: [1, 2], + build() { + const f = function (x, v) { + try { + v ||= variables(x)[0]; + const sq = __.sqComplete(x.clone(), v); + return /** @type {{ f: NerdamerSymbol; a: NerdamerSymbol; c: NerdamerSymbol }} */ (sq).f; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return x; + } + }; + return f; + }, + }, + ]); + nerdamer.updateAPI(); +})(); diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/Calculus.js b/tools/ui/src/lib/vendors/nerdamer-prime/Calculus.js new file mode 100644 index 000000000..f5aa22d41 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/Calculus.js @@ -0,0 +1,3487 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * Source : https://github.com/jiggzson/nerdamer + */ + +// Type imports for JSDoc ====================================================== +// These typedefs provide type aliases for the interfaces defined in index.d.ts. +// They enable proper type checking when working with the classes defined in this file. +// +// Usage patterns: +// - For return types: @returns {NerdamerSymbolType} +// - For parameters: @param {NerdamerSymbolType} symbol +// - For variable declarations: /** @type {NerdamerSymbolType} */ +// +// Note: When casting local class instances to interface types, use the pattern: +// /** @type {InterfaceType} */ (/** @type {unknown} */ (localInstance)) +// This is needed because TypeScript sees local classes and interfaces as separate types. + +/** + * Core type aliases from index.d.ts + * + * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType + * + * @typedef {import('./index').NerdamerCore.Frac} FracType + * + * @typedef {import('./index').NerdamerCore.Vector} VectorType + * + * @typedef {import('./index').NerdamerCore.Matrix} MatrixType + * + * @typedef {import('./index').NerdamerCore.Parser} ParserType + * + * @typedef {import('./index').NerdamerCore.Collection} CollectionType + * + * @typedef {import('./index').NerdamerCore.Settings} SettingsType + * + * @typedef {import('./index').NerdamerExpression} ExpressionType + * + * @typedef {typeof import('./index')} NerdamerType + * + * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface + * + * @typedef {import('./index').NerdamerCore.Math2} Math2Interface + * + * @typedef {import('./index').NerdamerCore.Core} CoreType + * + * @typedef {import('./index').ExpressionParam} ExpressionParam + * + * @typedef {import('./index').ArithmeticOperand} ArithmeticOperand + * + * @typedef {import('./index').ExpandOptions} ExpandOptions + * + * @typedef {import('./index').NerdamerCore.FactorSubModule} FactorSubModuleType + * + * @typedef {import('./index').NerdamerCore.PartFracSubModule} PartFracSubModuleType + * + * @typedef {import('./index').NerdamerCore.AlgebraClassesSubModule} AlgebraClassesSubModuleType + * + * @typedef {import('./index').NerdamerCore.Factors} FactorsType + * + * @typedef {import('./index').NerdamerCore.SimplifySubModule} SimplifySubModuleType + * + * @typedef {import('./index').NerdamerCore.CalculusModule} CalculusModuleType + * + * Constructor types + * + * @typedef {import('./index').NerdamerCore.FracConstructor} FracConstructor + * + * @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor + * + * @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor + * + * @typedef {import('./index').NerdamerCore.MatrixConstructor} MatrixConstructor + * + * @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType + * + * @typedef {import('./index').NerdamerCore.IntegrationOptions} IntegrationOptions + */ + +// Check if nerdamer exists globally (browser) or needs to be required (Node.js) +let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined; +if (typeof module !== 'undefined' && nerdamer === undefined) { + nerdamer = require('./nerdamer.core.js'); + require('./Algebra.js'); +} + +/** @returns {CalculusModuleType} */ +(function initCalculusModule() { + const core = nerdamer.getCore(); + const _ = core.PARSER; + const { Frac } = core; + const { Settings } = core; + const { isSymbol } = core.Utils; + const { FN } = core.groups; + const { NerdamerSymbol } = core; + const { text } = core.Utils; + const { inBrackets } = core.Utils; + const { isInt } = core.Utils; + const { format } = core.Utils; + const { even } = core.Utils; + const { evaluate } = core.Utils; + const { N } = core.groups; + const { S } = core.groups; + const { PL } = core.groups; + const { CP } = core.groups; + const { CB } = core.groups; + const { EX } = core.groups; + const { P } = core.groups; + const { LOG } = Settings; + const EXP = 'exp'; + const ABS = 'abs'; + const SQRT = 'sqrt'; + const SIN = 'sin'; + const COS = 'cos'; + const TAN = 'tan'; + const SEC = 'sec'; + const CSC = 'csc'; + const COT = 'cot'; + const ASIN = 'asin'; + const ACOS = 'acos'; + const ATAN = 'atan'; + const ASEC = 'asec'; + const ACSC = 'acsc'; + const ACOT = 'acot'; + const SINH = 'sinh'; + const COSH = 'cosh'; + const TANH = 'tanh'; + const CSCH = 'csch'; + const SECH = 'sech'; + const COTH = 'coth'; + const ASECH = 'asech'; + const ACSCH = 'acsch'; + const ACOTH = 'acoth'; + + /** + * Check if a symbol's power is itself a symbol with group S or CB + * + * @param {NerdamerSymbolType} sym + * @returns {boolean} + */ + function hasPowerGroupSOrCB(sym) { + return isSymbol(sym.power) && (sym.power.group === S || sym.power.group === CB); + } + + // Custom errors + function NoIntegralFound(msg) { + this.message = msg || ''; + } + NoIntegralFound.prototype = new Error(); + + // Preparations + NerdamerSymbol.prototype.hasIntegral = function hasIntegral() { + return this.containsFunction('integrate'); + }; + // Transforms a function + NerdamerSymbol.prototype.fnTransform = function fnTransform() { + if (this.group !== FN) { + return this; + } + let retval; + const a = this.args[0]; + const m = new NerdamerSymbol(this.multiplier); + const sym = this.clone().toUnitMultiplier(); + if (this.isLinear()) { + switch (this.fname) { + case SINH: + retval = _.parse(format('(e^({0})-e^(-({0})))/2', a)); + break; + case COSH: + retval = _.parse(format('(e^({0})+e^(-({0})))/2', a)); + break; + case TANH: + retval = _.parse(format('(e^({0})-e^(-({0})))/(e^({0})+e^(-({0})))', a)); + break; + case TAN: + retval = _.parse(format('sin({0})/cos({0})', a)); + break; + case CSC: + retval = _.parse(format('1/sin({0})', a)); + break; + case SEC: + retval = _.parse(format('1/cos({0})', a)); + break; + default: + retval = sym; + } + } else if (this.power.equals(2)) { + switch (this.fname) { + case SIN: + retval = _.parse(format('1/2-cos(2*({0}))/2', a)); + break; + case COS: + retval = _.parse(format('1/2+cos(2*({0}))/2', a)); + break; + case TAN: + // Retval = _.parse(format('(1-cos(2*({0})))/(1+cos(2*({0})))', a)); + retval = _.parse(format('sin({0})^2/cos({0})^2', a)); + break; + case COSH: + retval = _.parse(format('1/2+cosh(2*({0}))/2', a)); + break; + case SINH: + retval = _.parse(format('-1/2+cosh(2*({0}))/2', a)); + break; + case TANH: + retval = _.parse(format('(1+cosh(2*({0})))/(-1+cosh(2*({0})))', a)); + break; + case SEC: + retval = _.parse(format('(1-cos(2*({0})))/(1+cos(2*({0})))+1', a)); + break; + default: + retval = sym; + } + } else if (this.fname === SEC) { + retval = _.parse(format('1/cos({0})^({1})', this.args[0], this.power)); + } else if (this.fname === CSC) { + retval = _.parse(format('1/sin({0})^({1})', this.args[0], this.power)); + } else if (this.fname === TAN) { + if (this.power.lessThan(0)) { + retval = _.parse(format('cos({0})^(-({1}))/sin({0})^({1})', this.args[0], this.power.negate())); + } else { + retval = _.parse(format('sin({0})^({1})/cos({0})^({1})', this.args[0], this.power)); + } + } else if (this.fname === SIN && this.power.lessThan(0)) { + retval = _.parse(format('csc({0})^(-({1}))', this.args[0], this.power.negate())); + } else if (this.fname === COS && this.power.lessThan(0)) { + retval = _.parse(format('sec({0})^(-({1}))', this.args[0], this.power.negate())); + } else if (this.fname === SIN && this.power.equals(3)) { + retval = _.parse(format('(3*sin({0})-sin(3*({0})))/4', this.args[0])); + } else if (this.fname === COS && this.power.equals(3)) { + retval = _.parse(format('(cos(3*({0}))+3*cos({0}))/4', this.args[0])); + } + // Cos(a*x)^(2*n) or sin(a*x)^(2*n) + else if ((this.fname === COS || this.fname === SIN) && even(this.power)) { + const n = this.power / 2; + // Convert to a double angle + const cloned = /** @type {NerdamerSymbolType} */ (this.clone().toLinear()); + const doubleAngle = /** @type {NerdamerSymbolType} */ (_.pow(cloned, _.parse(2))); + const transformed = /** @type {NerdamerSymbolType} */ ( + _.expand(_.pow(doubleAngle.fnTransform(), _.parse(n))) + ); + + retval = new NerdamerSymbol(0); + + transformed.each(s => { + const t = s.fnTransform(); + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, t)); + }, true); + } else { + retval = sym; + } + + return _.multiply(retval, m); + }; + + NerdamerSymbol.prototype.hasTrig = function hasTrig() { + if (this.isConstant(true) || this.group === S) { + return false; + } + if (this.fname && (core.Utils.inTrig(this.fname) || core.Utils.inInverseTrig(this.fname))) { + return true; + } + if (this.symbols) { + for (const x in this.symbols) { + if (this.symbols[x].hasTrig()) { + return true; + } + } + } + return false; + }; + + core.Expression.prototype.hasIntegral = function hasIntegral() { + return this.symbol.hasIntegral(); + }; + /** + * Attempts to rewrite a symbol under one common denominator + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + core.Utils.toCommonDenominator = function toCommonDenominator(symbol) { + // Transform x/a+x -> (ax+x)/a + if (symbol.isComposite() && symbol.isLinear()) { + const m = new NerdamerSymbol(symbol.multiplier); + let denominator = new NerdamerSymbol(1); + let numerator = new NerdamerSymbol(0); + symbol.each(x => { + denominator = /** @type {NerdamerSymbolType} */ (_.multiply(denominator, x.getDenom())); + }, true); + + // Remove the denomitor in each term + symbol.each(x => { + const num = x.getNum(); + const den = x.getDenom(); + const factor = /** @type {NerdamerSymbolType} */ (_.multiply(num, _.divide(denominator.clone(), den))); + numerator = /** @type {NerdamerSymbolType} */ (_.add(numerator, factor)); + }); + const retval = /** @type {NerdamerSymbolType} */ ( + _.multiply( + m, + core.Algebra.divide( + /** @type {NerdamerSymbolType} */ (_.expand(numerator)), + /** @type {NerdamerSymbolType} */ (_.expand(denominator)) + ) + ) + ); + return retval; + } + return symbol; + }; + // A function to check if a function name is an inverse trig function + core.Utils.inInverseTrig = function inInverseTrig(x) { + const invTrigFns = [ASIN, ACOS, ATAN, ACSC, ASEC, ACOT]; + return invTrigFns.indexOf(x) !== -1; + }; + // A function to check if a function name is a trig function + core.Utils.inTrig = function inTrig(x) { + const trigFns = [COS, SIN, TAN, SEC, CSC, COT]; + return trigFns.indexOf(x) !== -1; + }; + + core.Utils.inHtrig = function inHtrig(x) { + const trigFns = [SINH, COSH, TANH, ACSCH, ASECH, ACOTH]; + return trigFns.indexOf(x) !== -1; + }; + + // Matrix functions + core.Matrix.jacobian = function jacobian(eqns, vars) { + const result = new core.Matrix(); + // Get the variables if not supplied + vars ||= core.Utils.arrayGetVariables(eqns); + + vars.forEach((v, i) => { + eqns.forEach((eq, j) => { + const e = core.Calculus.diff(eq.clone(), v); + result.set(j, i, e); + }); + }); + + return result; + }; + + core.Matrix.prototype.max = function max() { + let maxValue = new NerdamerSymbol(0); + this.each(x => { + const e = x.abs(); + if (e.gt(maxValue)) { + maxValue = e; + } + }); + return maxValue; + }; + + core.Matrix.cMatrix = function cMatrix(value, vars) { + const m = new core.Matrix(); + // Make an initial guess + vars.forEach((v, i) => { + m.set(i, 0, _.parse(value)); + }); + return m; + }; + + /** + * Checks if all elements in an array are function symbols + * + * @param {NerdamerSymbolType[]} arr + * @returns {boolean} + */ + const allFunctions = (core.Utils.allFunctions = function allFunctions(arr) { + for (let i = 0, l = arr.length; i < l; i++) { + if (arr[i].group !== FN) { + return false; + } + } + return true; + }); + /** + * Transforms cos(a)*sin(b) into (sin(a+b)-sin(a-b))/2 + * + * @param {NerdamerSymbolType} symbol1 + * @param {NerdamerSymbolType} symbol2 + * @returns {NerdamerSymbolType} + */ + const cosAsinBtransform = (core.Utils.cosAsinBtranform = function cosAsinBtranform(symbol1, symbol2) { + const a = symbol1.args[0]; + const b = symbol2.args[0]; + return /** @type {NerdamerSymbolType} */ (_.parse(format('(sin(({0})+({1}))-sin(({0})-({1})))/2', a, b))); + }); + /** + * Transforms cos(a)*sin(a) into sin(2a)/2 + * + * @param {NerdamerSymbolType} symbol1 + * @param {NerdamerSymbolType} symbol2 + * @returns {NerdamerSymbolType} + */ + const cosAsinAtransform = (core.Utils.cosAsinAtranform = function cosAsinAtranform(symbol1, symbol2) { + // TODO: temporary fix for integrate(e^x*sin(x)*cos(x)^2). + // we technically know how to do this transform but more is needed for correct output + if (Number(symbol2.power) !== 1) { + return /** @type {NerdamerSymbolType} */ (_.multiply(symbol1, symbol2)); + } + const a = symbol1.args[0]; + return /** @type {NerdamerSymbolType} */ (_.parse(format('(sin(2*({0})))/2', a))); + }); + /** + * Transforms sin(a)*sin(b) into (cos(a+b)-cos(a-b))/2 + * + * @param {NerdamerSymbolType} symbol1 + * @param {NerdamerSymbolType} symbol2 + * @returns {NerdamerSymbolType} + */ + const sinAsinBtransform = (core.Utils.cosAsinBtranform = function cosAsinBtranform(symbol1, symbol2) { + const a = symbol1.args[0]; + const b = symbol2.args[0]; + return /** @type {NerdamerSymbolType} */ (_.parse(format('(cos(({0})+({1}))-cos(({0})-({1})))/2', a, b))); + }); + /** + * Transforms an array of trig functions into simplified form + * + * @param {NerdamerSymbolType[]} arr + * @returns {NerdamerSymbolType} + */ + const trigTransform = (core.Utils.trigTransform = function trigTransform(arr) { + /** @type {Record} */ + const map = {}; + let symbol; + let t; + let retval = new NerdamerSymbol(1); + for (let i = 0, l = arr.length; i < l; i++) { + symbol = arr[i]; + + if (symbol.group === FN) { + const { fname } = symbol; + + if (fname === COS && map[SIN]) { + if (map[SIN].args[0].toString() === symbol.args[0].toString()) { + t = cosAsinAtransform(symbol, map[SIN]); + } else { + t = cosAsinBtransform(symbol, map[SIN]); + } + delete map[SIN]; + + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, t)); + } else if (fname === SIN && map[COS]) { + if (map[COS].args[0].toString() === symbol.args[0].toString()) { + t = cosAsinAtransform(symbol, map[COS]); + } else { + t = cosAsinBtransform(symbol, map[COS]); + } + delete map[COS]; + + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, t)); + } else if (fname === SIN && map[SIN]) { + if (map[SIN].args[0].toString() === symbol.args[0].toString()) { + // This should actually be redundant code but let's put just in case + t = /** @type {NerdamerSymbolType} */ (_.multiply(symbol, map[SIN])); + delete map[SIN]; + } else { + t = sinAsinBtransform(symbol, map[SIN]); + delete map[SIN]; + } + + retval = t; + } else { + map[fname] = symbol; + } + } else { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, symbol)); + } + } + + // Put back the remaining functions + for (const x in map) { + if (!Object.hasOwn(map, x)) { + continue; + } + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, map[x])); + } + + return retval; + }); + + core.Settings.integration_depth = 10; + + core.Settings.max_lim_depth = 10; + + /** @type {CalculusModuleType} */ + const __ = (core.Calculus = { + version: '1.4.6', + + /** + * Computes the sum of a function over an index range + * + * @param {NerdamerSymbolType} fn + * @param {NerdamerSymbolType} index + * @param {NerdamerSymbolType} start + * @param {NerdamerSymbolType} end + * @returns {NerdamerSymbolType} + */ + sum(fn, index, start, end) { + if (!(index.group === core.groups.S)) { + throw new core.exceptions.NerdamerTypeError(`Index must be symbol. ${text(index)} provided`); + } + const indexName = index.value; + let retval; + if (core.Utils.isNumericSymbol(start) && core.Utils.isNumericSymbol(end)) { + const modifier = /** @type {'' | 'PARSE2NUMBER'} */ ( + Number(end) - Number(start) < 200 ? '' : 'PARSE2NUMBER' + ); + const startNum = Number(start); + const endNum = Number(end); + retval = core.Utils.block(modifier, () => { + const f = fn.text(); + /** @type {Record} */ + const subs = { '~': true }; // Lock subs. Is this even being used? + let result = new core.NerdamerSymbol(0); + + for (let i = startNum; i <= endNum; i++) { + subs[indexName] = new NerdamerSymbol(i); + const ans = _.parse(f, /** @type {Record} */ (subs)); + result = /** @type {NerdamerSymbolType} */ (_.add(result, ans)); + } + return result; + }); + } else { + retval = _.symfunction('sum', [fn, new NerdamerSymbol(indexName), start, end]); + } + + return retval; + }, + /** + * Computes the product of a function over an index range + * + * @param {NerdamerSymbolType} fn + * @param {NerdamerSymbolType} index + * @param {NerdamerSymbolType} start + * @param {NerdamerSymbolType} end + * @returns {NerdamerSymbolType} + */ + product(fn, index, start, end) { + if (!(index.group === core.groups.S)) { + throw new core.exceptions.NerdamerTypeError(`Index must be symbol. ${text(index)} provided`); + } + const indexName = index.value; + let retval; + if (core.Utils.isNumericSymbol(start) && core.Utils.isNumericSymbol(end)) { + const modifier = /** @type {'' | 'PARSE2NUMBER'} */ ( + Number(end) - Number(start) < 200 ? '' : 'PARSE2NUMBER' + ); + retval = core.Utils.block(modifier, () => { + const startNum = Number(start); + const endNum = Number(end.multiplier); + + const f = fn.text(); + /** @type {Record} */ + const subs = {}; + let result = new core.NerdamerSymbol(1); + + for (let i = startNum; i <= endNum; i++) { + subs[indexName] = new NerdamerSymbol(i); + result = /** @type {NerdamerSymbolType} */ ( + _.multiply(result, _.parse(f, /** @type {Record} */ (subs))) + ); + } + return result; + }); + } else { + retval = _.symfunction('product', [fn, new NerdamerSymbol(indexName), start, end]); + } + + return retval; + }, + /** + * Computes the derivative of a symbol + * + * @param {NerdamerSymbolType | VectorType | MatrixType} symbol + * @param {NerdamerSymbolType | string} [wrt] + * @param {NerdamerSymbolType | number} [nth] + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + diff(symbol, wrt, nth) { + if (core.Utils.isVector(symbol)) { + const vector = new core.Vector([]); + symbol.each(x => { + vector.elements.push(__.diff(/** @type {NerdamerSymbolType} */ (x), wrt, nth)); + }); + return vector; + } + if (core.Utils.isMatrix(symbol)) { + const matrix = new core.Matrix(); + symbol.each((x, i, j) => { + matrix.set(i, j, __.diff(/** @type {NerdamerSymbolType} */ (x), wrt, nth)); + }); + return matrix; + } + const sym = /** @type {NerdamerSymbolType & { LHS?: NerdamerSymbolType; RHS?: NerdamerSymbolType }} */ ( + symbol + ); + if (sym.LHS && sym.RHS) { + // Equation, diff both sides + const result = new core.Equation( + /** @type {NerdamerSymbolType} */ (__.diff(sym.LHS.clone(), wrt, nth)), + /** @type {NerdamerSymbolType} */ (__.diff(sym.RHS.clone(), wrt, nth)) + ); + return /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (result)); + } + + let d = isSymbol(wrt) ? wrt.text() : wrt; + // The nth derivative + nth = /** @type {number} */ (isSymbol(nth) ? nth.multiplier.toDecimal() : nth || 1); + + if (d === undefined) { + d = core.Utils.variables(/** @type {NerdamerSymbolType} */ (symbol))[0]; + } + + // Unwrap sqrt + if (sym.group === FN && sym.fname === SQRT) { + const s = sym.args[0]; + const sp = /** @type {FracType} */ (sym.power).clone(); + // These groups go to zero anyway so why waste time? + if (s.group !== N || s.group !== P) { + s.power = isSymbol(s.power) + ? /** @type {NerdamerSymbolType | FracType} */ ( + _.multiply(_.multiply(s.power, new NerdamerSymbol(1 / 2)), new NerdamerSymbol(sp)) + ) + : s.power.multiply(new Frac(0.5)).multiply(sp); + s.multiplier = s.multiplier.multiply(sym.multiplier); + } + + symbol = s; + } + + if (symbol.group === FN && !isSymbol(symbol.power)) { + const a = derive(_.parse(symbol)); + const b = __.diff(symbol.args[0].clone(), d); + symbol = _.multiply(a, b); // Chain rule + } else { + symbol = derive(symbol); + } + + if (nth > 1) { + nth--; + symbol = __.diff(symbol, wrt, nth); + } + + return symbol; + + // Equivalent to "derivative of the outside". + function polydiff(s) { + if (s.value === d || s.contains(d, true)) { + s.multiplier = s.multiplier.multiply(s.power); + s.power = s.power.subtract(new Frac(1)); + if (s.power.equals(0)) { + s = new NerdamerSymbol(s.multiplier); + } + } + + return s; + } + + function derive(s) { + const g = s.group; + let _a; + let b; + let cp; + + if (g === N || (g === S && s.value !== d) || g === P) { + s = new NerdamerSymbol(0); + } else if (g === S) { + s = polydiff(s); + } else if (g === CB) { + const m = s.multiplier.clone(); + s.toUnitMultiplier(); + const retval = _.multiply(productRule(s), polydiff(s)); + retval.multiplier = retval.multiplier.multiply(m); + return retval; + } else if (g === FN && s.power.equals(1)) { + // Table of known derivatives + const m = s.multiplier.clone(); + s.toUnitMultiplier(); + + switch (s.fname) { + case LOG: + cp = s.clone(); + s = s.args[0].clone(); // Get the arguments + s.power = s.power.negate(); + s.multiplier = cp.multiplier.divide(s.multiplier); + break; + case COS: + // Cos -> -sin + s.fname = SIN; + s.multiplier.negate(); + break; + case SIN: + // Sin -> cos + s.fname = COS; + break; + case TAN: + // Tan -> sec^2 + s.fname = SEC; + s.power = new Frac(2); + break; + case SEC: + // Use a clone if this gives errors + s = qdiff(s, TAN); + break; + case CSC: + s = qdiff(s, '-cot'); + break; + case COT: + s.fname = CSC; + s.multiplier.negate(); + s.power = new Frac(2); + break; + case ASIN: + s = _.parse(`(sqrt(1-(${text(s.args[0])})^2))^(-1)`); + break; + case ACOS: + s = _.parse(`-(sqrt(1-(${text(s.args[0])})^2))^(-1)`); + break; + case ATAN: + s = _.parse(`(1+(${text(s.args[0])})^2)^(-1)`); + break; + case ABS: + // Depending on the complexity of the symbol it's easier to just parse it into a new symbol + // this should really be readdressed soon + b = s.args[0].clone(); + b.toUnitMultiplier(); + s = _.parse(`${inBrackets(text(s.args[0]))}/abs${inBrackets(text(b))}`); + break; + case 'parens': + // See product rule: f'.g goes to zero since f' will return zero. This way we only get back + // 1*g' + s = new NerdamerSymbol(1); + break; + case 'cosh': + // Cosh -> -sinh + s.fname = 'sinh'; + break; + case 'sinh': + // Sinh -> cosh + s.fname = 'cosh'; + break; + case TANH: + // Tanh -> sech^2 + s.fname = SECH; + s.power = new Frac(2); + break; + case SECH: + // Use a clone if this gives errors + s = qdiff(s, '-tanh'); + break; + case CSCH: { + const cschArg = String(s.args[0]); + s = _.parse(`-coth(${cschArg})*csch(${cschArg})`); + break; + } + case COTH: { + const cothArg = String(s.args[0]); + s = _.parse(`-csch(${cothArg})^2`); + break; + } + case 'asinh': + s = _.parse(`(sqrt(1+(${text(s.args[0])})^2))^(-1)`); + break; + case 'acosh': + s = _.parse(`(sqrt(-1+(${text(s.args[0])})^2))^(-1)`); + break; + case 'atanh': + s = _.parse(`(1-(${text(s.args[0])})^2)^(-1)`); + break; + case ASECH: { + const asechArg = String(s.args[0]); + s = _.parse(`-1/(sqrt(1/(${asechArg})^2-1)*(${asechArg})^2)`); + break; + } + case ACOTH: + s = _.parse(`-1/((${s.args[0]})^2-1)`); + break; + case ACSCH: { + const arg = String(s.args[0]); + s = _.parse(`-1/(sqrt(1/(${arg})^2+1)*(${arg})^2)`); + break; + } + case ASEC: { + const arg = String(s.args[0]); + s = _.parse(`1/(sqrt(1-1/(${arg})^2)*(${arg})^2)`); + break; + } + case ACSC: { + const arg = String(s.args[0]); + s = _.parse(`-1/(sqrt(1-1/(${arg})^2)*(${arg})^2)`); + break; + } + case ACOT: + s = _.parse(`-1/((${s.args[0]})^2+1)`); + break; + case 'S': { + const arg = String(s.args[0]); + s = _.parse(`sin((pi*(${arg})^2)/2)`); + break; + } + case 'C': { + const arg = String(s.args[0]); + s = _.parse(`cos((pi*(${arg})^2)/2)`); + break; + } + case 'Si': { + const arg = s.args[0]; + s = _.parse(`sin(${arg})/(${arg})`); + break; + } + case 'Shi': { + const arg = s.args[0]; + s = _.parse(`sinh(${arg})/(${arg})`); + break; + } + case 'Ci': { + const arg = s.args[0]; + s = _.parse(`cos(${arg})/(${arg})`); + break; + } + case 'Chi': { + const arg = s.args[0]; + s = _.parse(`cosh(${arg})/(${arg})`); + break; + } + case 'Ei': { + const arg = s.args[0]; + s = _.parse(`e^(${arg})/(${arg})`); + break; + } + case 'Li': { + const arg = s.args[0]; + s = _.parse(`1/${Settings.LOG}(${arg})`); + break; + } + case 'erf': + s = _.parse(`(2*e^(-(${s.args[0]})^2))/sqrt(pi)`); + break; + case 'atan2': { + const x_ = String(s.args[0]); + const y_ = String(s.args[1]); + s = _.parse(`(${y_})/((${y_})^2+(${x_})^2)`); + break; + } + case 'sign': + s = new NerdamerSymbol(0); + break; + case 'sinc': + s = _.parse(format('(({0})*cos({0})-sin({0}))*({0})^(-2)', s.args[0])); + break; + case Settings.LOG10: + s = _.parse(`1/((${s.args[0]})*${Settings.LOG}(10))`); + break; + default: + s = _.symfunction('diff', [s, wrt]); + } + s.multiplier = s.multiplier.multiply(m); + } else if (g === EX || (g === FN && isSymbol(s.power))) { + let value; + if (g === EX) { + value = s.value; + } else if (g === FN && s.contains(d)) { + value = s.fname + inBrackets(text(s.args[0])); + } else { + value = s.value + inBrackets(text(s.args[0])); + } + b = __.diff(_.multiply(_.parse(LOG + inBrackets(value)), s.power.clone()), d); + s = _.multiply(s, b); + } else if (g === FN && !s.power.equals(1)) { + b = s.clone(); + b.toLinear(); + b.toUnitMultiplier(); + s = _.multiply(polydiff(s.clone()), derive(b)); + } else if (g === CP || g === PL) { + // Note: Do not use `parse` since this puts back the sqrt and causes a bug as in #610. Use clone. + const c = s.clone(); + let result = new NerdamerSymbol(0); + for (const x in s.symbols) { + if (!Object.hasOwn(s.symbols, x)) { + continue; + } + result = /** @type {NerdamerSymbolType} */ (_.add(result, __.diff(s.symbols[x].clone(), d))); + } + s = _.multiply(polydiff(c), result); + } + + s.updateHash(); + + return s; + } + function qdiff(s, val, altVal) { + return _.multiply(s, _.parse(val + inBrackets(altVal || text(s.args[0])))); + } + function productRule(s) { + // Grab all the symbols within the CB symbol + const symbols = s.collectSymbols(); + let result = new NerdamerSymbol(0); + const l = symbols.length; + // Loop over all the symbols + for (let i = 0; i < l; i++) { + let df = __.diff(symbols[i].clone(), d); + for (let j = 0; j < l; j++) { + // Skip the symbol of which we just pulled the derivative + if (i !== j) { + // Multiply out the remaining symbols + df = /** @type {NerdamerSymbolType} */ (_.multiply(df, symbols[j].clone())); + } + } + // Add the derivative to the result + result = /** @type {NerdamerSymbolType} */ (_.add(result, df)); + } + return result; // Done + } + }, + integration: { + /** + * Performs u-substitution for integration. + * + * @param {NerdamerSymbolType[]} symbols - Array of symbols to work with + * @param {string} dx - Variable of integration + * @returns {NerdamerSymbolType | VectorType | MatrixType | undefined} + */ + u_substitution(symbols, dx) { + // May cause problems if person is using this already. Will need + // to find algorithm for detecting conflict + const u = '__u__'; + + function tryCombo(a, b, f) { + const d = __.diff(b, dx); + const q = f ? f(a, b) : _.divide(a.clone(), d); + if (!q.contains(dx, true)) { + return q; + } + return null; + } + function doFnSub(fname, arg) { + let subbed = /** @type {NerdamerSymbolType} */ ( + __.integrate(_.symfunction(fname, [new NerdamerSymbol(u)]), u, 0) + ); + subbed = subbed.sub(new NerdamerSymbol(u), arg); + subbed.updateHash(); + return subbed; + } + + const a = symbols[0].clone(); + const b = symbols[1].clone(); + const g1 = a.group; + const g2 = b.group; + let Q; + if (g1 === FN && g2 !== FN) { + // E.g. 2*x*cos(x^2) + const arg = a.args[0]; + Q = tryCombo(b, arg.clone()); + if (Q) { + return _.multiply(Q, doFnSub(a.fname, arg)); + } + Q = tryCombo(b, a); + if (Q) { + return __.integration.poly_integrate(a); + } + } else if (g2 === FN && g1 !== FN) { + // E.g. 2*(x+1)*cos((x+1)^2 + const arg = b.args[0]; + Q = tryCombo(a, arg.clone()); + if (Q) { + return _.multiply(Q, doFnSub(b.fname, arg)); + } + } else if (g1 === FN && g2 === FN) { + Q = tryCombo(a.clone(), b.clone()); + if (Q) { + return _.multiply(__.integration.poly_integrate(b), Q); + } + Q = tryCombo(b.clone(), a.clone()); + if (Q) { + return _.multiply(__.integration.poly_integrate(b), Q); + } + } else if (g1 === EX && g2 !== EX) { + const p = a.power; + Q = tryCombo(b, isSymbol(p) ? p.clone() : new NerdamerSymbol(p)); + if (!Q) { + // One more try + const dc = __.integration.decompose_arg(isSymbol(p) ? p.clone() : new NerdamerSymbol(p), dx); + // Consider the possibility of a^x^(n-1)*x^n dx + const xp = /** @type {NerdamerSymbolType} */ (__.diff(dc[2].clone(), dx)); + const dc2 = __.integration.decompose_arg(xp.clone(), dx); + // If their powers equal, so if dx*p == b + if ( + /** @type {NerdamerSymbolType} */ (_.multiply(dc[1], dc2[1])).power.equals( + /** @type {FracType} */ (b.power) + ) + ) { + const m = _.divide(dc[0].clone(), dc2[0].clone()); + + let newVal = _.multiply( + m.clone(), + _.pow(new NerdamerSymbol(a.value), _.multiply(dc[0], new NerdamerSymbol(u))) + ); + newVal = _.multiply(newVal, new NerdamerSymbol(u)); + return /** @type {NerdamerSymbolType} */ (__.integration.by_parts(newVal, u, 0, {})).sub( + u, + dc[1].clone() + ); + } + } + const integrated = /** @type {NerdamerSymbolType} */ ( + __.integrate(a.sub(/** @type {NerdamerSymbolType} */ (p.clone()), new NerdamerSymbol(u)), u, 0) + ); + const retval = _.multiply( + integrated.sub(new NerdamerSymbol(u), /** @type {NerdamerSymbolType} */ (p)), + Q + ); + + return retval; + } else if (g2 === EX && g1 !== EX) { + const p = b.power; + Q = tryCombo(a, /** @type {NerdamerSymbolType} */ (p.clone())); + const integrated = /** @type {NerdamerSymbolType} */ ( + __.integrate(b.sub(/** @type {NerdamerSymbolType} */ (p), new NerdamerSymbol(u)), u, 0) + ); + return _.multiply(integrated.sub(new NerdamerSymbol(u), /** @type {NerdamerSymbolType} */ (p)), Q); + } else if (a.isComposite() || b.isComposite()) { + const f = function (sym1, sym2) { + const d = __.diff(sym2, dx); + const A = /** @type {FactorSubModuleType} */ (core.Algebra.Factor).factorInner(sym1); + const B = /** @type {FactorSubModuleType} */ (core.Algebra.Factor).factorInner( + /** @type {NerdamerSymbolType} */ (d) + ); + const q = _.divide(A, B); + return q; + }; + const f1 = a.isComposite() ? a.clone().toLinear() : a.clone(); + const f2 = b.isComposite() ? b.clone().toLinear() : b.clone(); + Q = tryCombo(f1.clone(), f2.clone(), f); + if (Q) { + return _.multiply(__.integration.poly_integrate(b), Q); + } + Q = tryCombo(f2.clone(), f1.clone(), f); + if (Q) { + return _.multiply(__.integration.poly_integrate(a), Q); + } + } + return undefined; + }, + // Simple integration of a single polynomial x^(n+1)/(n+1) + /** + * @param {NerdamerSymbolType} x + * @returns {NerdamerSymbolType} + */ + poly_integrate(x) { + const p = x.power.toString(); + const m = x.multiplier.toDecimal(); + const s = x.toUnitMultiplier().toLinear(); + if (Number(p) === -1) { + return /** @type {NerdamerSymbolType} */ ( + _.multiply(new NerdamerSymbol(m), _.symfunction(LOG, [s])) + ); + } + return /** @type {NerdamerSymbolType} */ (_.parse(format('({0})*({1})^(({2})+1)/(({2})+1)', m, s, p))); + }, + // If we're just spinning wheels we want to stop. This is why we + // wrap integration in a try catch block and call this to stop. + /** + * @param {string} [msg] + * @returns {never} + */ + stop(msg) { + msg ||= 'Unable to compute integral!'; + core.Utils.warn(msg); + throw new NoIntegralFound(msg); + }, + /** + * @param {NerdamerSymbolType} input + * @param {NerdamerSymbolType | string} dx + * @param {number} depth + * @param {IntegrationOptions} opt + * @returns {NerdamerSymbolType} + */ + partial_fraction(input, dx, depth, opt) { + // TODO: This whole thing needs to be rolled into one but for now I'll leave it as two separate parts + if (!isSymbol(dx)) { + dx = /** @type {NerdamerSymbolType} */ (_.parse(dx)); + } + + let result; + result = new NerdamerSymbol(0); + const partialFractions = /** @type {NerdamerSymbolType} */ ( + /** @type {PartFracSubModuleType} */ (core.Algebra.PartFrac).partfrac( + input, + /** @type {NerdamerSymbolType} */ (dx) + ) + ); + + if (partialFractions.group === CB && partialFractions.isLinear()) { + // Perform a quick check to make sure that all partial fractions are linear + partialFractions.each(x => { + if (!x.isLinear()) { + __.integration.stop(); + } + }); + partialFractions.each(x => { + result = /** @type {NerdamerSymbolType} */ (_.add(result, __.integrate(x, dx, depth, opt))); + }); + } else { + result = /** @type {NerdamerSymbolType} */ ( + _.add(result, __.integrate(partialFractions, dx, depth, opt)) + ); + } + return result; + }, + get_udv(symbol) { + const parts = [ + [ + /* L*/ + ], + [ + /* I*/ + ], + [ + /* A*/ + ], + [ + /* T*/ + ], + [ + /* E*/ + ], + ]; + // First we sort them + const setSymbol = function (x) { + const g = x.group; + if (g === FN) { + const { fname } = x; + if (core.Utils.inTrig(fname) || core.Utils.inHtrig(fname)) { + parts[3].push(x); + } else if (core.Utils.inInverseTrig(fname)) { + parts[1].push(x); + } else if (fname === LOG) { + parts[0].push(x); + } else { + __.integration.stop(); + } + } else if (g === S || (x.isComposite() && x.isLinear()) || (g === CB && x.isLinear())) { + parts[2].push(x); + } else if (g === EX || (x.isComposite() && !x.isLinear())) { + parts[4].push(x); + } else { + __.integration.stop(); + } + }; + + if (symbol.group === CB) { + symbol.each(x => { + setSymbol(NerdamerSymbol.unwrapSQRT(x, true)); + }); + } else { + setSymbol(symbol); + } + let u; + let dv = new NerdamerSymbol(1); + // Compile u and dv + for (let i = 0; i < 5; i++) { + const part = parts[i]; + let t; + const l = part.length; + if (l > 0) { + if (l > 1) { + t = new NerdamerSymbol(1); + for (let j = 0; j < l; j++) { + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, part[j].clone())); + } + } else { + t = part[0].clone(); + } + + if (u) { + dv = /** @type {NerdamerSymbolType} */ (_.multiply(dv, t)); // Everything else belongs to dv + } else { + u = t; // The first u encountered gets chosen + u.multiplier = u.multiplier.multiply(symbol.multiplier); // The first one gets the mutliplier + } + } + } + + return [u, dv]; + }, + + trig_sub(symbol, dx, depth, opt, parts, _symbols) { + parts ||= __.integration.decompose_arg(symbol.clone().toLinear(), dx); + const _b = parts[3]; + const _ax = parts[2]; + const a = parts[0]; + const x = parts[1]; + if (x.power.equals(2) && a.greaterThan(0)) { + // Use tan(x) + const t = core.Utils.getU(symbol); // Get an appropriate u + const u = _.parse(TAN + inBrackets(t)); // U + const du = _.parse(`${SEC + inBrackets(t)}^2`); // Du + const f = _.multiply(symbol.sub(x, u), du); + const integral = /** @type {NerdamerSymbolType} */ (__.integrate(f, t, depth, opt)).sub(u, x); + core.Utils.clearU(/** @type {string} */ (/** @type {unknown} */ (u))); + return integral; + } + return undefined; + }, + + /** + * Integration by parts + * + * @param {NerdamerSymbolType} symbol + * @param {string} dx + * @param {number} depth + * @param {IntegrationOptions} o + * @returns {NerdamerSymbolType} + */ + by_parts(symbol, dx, depth, o) { + o.previous ||= []; + let retval; + // First LIATE + const udv = __.integration.get_udv(symbol); + const u = udv[0]; + const dv = udv[1]; + let du = NerdamerSymbol.unwrapSQRT( + /** @type {NerdamerSymbolType} */ (_.expand(__.diff(u.clone(), dx))), + true + ); + const c = du.clone().stripVar(/** @type {string} */ (dx)); + // Strip any coefficients + du = /** @type {NerdamerSymbolType} */ (_.divide(du, c.clone())); + const v = __.integrate(dv.clone(), dx, depth || 0); + const vdu = /** @type {NerdamerSymbolType} */ (_.multiply(v.clone(), du)); + const vduS = vdu.toString(); + // Currently only supports e^x*(some trig) + if (o.previous.indexOf(vduS) !== -1 && core.Utils.inTrig(u.fname) && dv.isE()) { + // We're going to exploit the fact that vdu can never be constant + // to work out way out of this cycle. We'll return the length of + // the this.previous array until we're back at level one + o.is_cyclic = true; + // Return the integral. + return new NerdamerSymbol(1); + } + o.previous.push(vduS); + + const uv = _.multiply(u, v); + // Clear the multiplier so we're dealing with a bare integral + const m = vdu.multiplier.clone(); + vdu.toUnitMultiplier(); + const integralVdu = _.multiply(__.integrate(vdu.clone(), dx, depth, o), c); + integralVdu.multiplier = integralVdu.multiplier.multiply(m); + retval = _.subtract(uv, integralVdu); + // We know that there cannot be constants so they're a holdover from a cyclic integral + if (o.is_cyclic) { + // Start popping the previous stack so we know how deep in we are + o.previous.pop(); + if (o.previous.length === 0) { + retval = /** @type {NerdamerSymbolType} */ (_.expand(retval)); + let rem = new NerdamerSymbol(0); + retval.each(x => { + if (!x.contains(dx)) { + rem = /** @type {NerdamerSymbolType} */ (_.add(rem, x.clone())); + } + }); + // Get the actual uv + retval = /** @type {NerdamerSymbolType} */ ( + _.divide(_.subtract(retval, rem.clone()), _.subtract(new NerdamerSymbol(1), rem)) + ); + } + } + + return /** @type {NerdamerSymbolType} */ (retval); + }, + /* + * Dependents: [Solve, integrate] + */ + + decompose_arg: core.Utils.decompose_fn, + }, + // TODO: nerdamer.integrate('-e^(-a*t)*sin(t)', 't') -> gives incorrect output + /** + * Integrates a symbol with respect to a variable. + * + * @param {NerdamerSymbolType | VectorType} originalSymbol - The symbol to integrate + * @param {string | NerdamerSymbolType} [dt] - The variable to integrate with respect to + * @param {number} [depth] - Recursion depth for integration + * @param {object} [opt] - Configuration options + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + integrate(originalSymbol, dt, depth, opt) { + // Add support for integrating vectors + if (core.Utils.isVector(originalSymbol)) { + const vector = new core.Vector([]); + originalSymbol.each( + /** @param {NerdamerSymbolType} x */ + x => { + vector.elements.push(/** @type {NerdamerSymbolType} */ (__.integrate(x, dt))); + } + ); + return vector; + } + + // Assume integration wrt independent variable if expression only has one variable + if (!dt) { + const vars = core.Utils.variables(originalSymbol); + if (vars.length === 1) { + dt = vars[0]; + } + // Defaults to x + dt ||= 'x'; + } + if (!isNaN(parseFloat(/** @type {string} */ (dt)))) { + _.error(`variable expected but received ${dt}`); + } + // Get rid of constants right away + if (originalSymbol.isConstant(true)) { + return _.multiply(originalSymbol.clone(), _.parse(dt)); + } + + // Configurations options for integral. This is needed for tracking extra options + // e.g. cyclic integrals or additional settings + opt ||= {}; + return core.Utils.block( + 'PARSE2NUMBER', + () => { + // Make a note of the original symbol. Set only if undefined + depth ||= 0; + const dx = isSymbol(dt) ? dt.toString() : dt; + // We don't want the symbol in sqrt form. x^(1/2) is prefererred + let symbol = NerdamerSymbol.unwrapSQRT(originalSymbol.clone(), true); + const g = symbol.group; + let retval; + + try { + // We stop integration after x amount of recursive calls + if (++depth > core.Settings.integration_depth) { + __.integration.stop('Maximum depth reached. Exiting!'); + } + + // Constants. We first eliminate anything that doesn't have dx. Everything after this has + // to have dx or else it would have been taken care of below + if (!symbol.contains(dx, true)) { + retval = _.multiply(symbol.clone(), _.parse(dx)); + } + // E.g. 2*x + else if (g === S) { + retval = __.integration.poly_integrate(symbol, dx, depth); + } else if (g === EX) { + if ( + symbol.previousGroup === FN && + !(symbol.fname === 'sqrt' || symbol.fname === Settings.PARENTHESIS) + ) { + __.integration.stop(); + } + // Check the base + if (symbol.contains(dx) && symbol.previousGroup !== FN) { + // If the symbol also contains dx then we stop since we currently + // don't know what to do with it e.g. x^x + if (/** @type {NerdamerSymbolType} */ (symbol.power).contains(dx)) { + __.integration.stop(); + } else { + const t = /** @type {NerdamerSymbolType} */ ( + __.diff(symbol.clone().toLinear(), dx) + ); + if (t.contains(dx)) { + __.integration.stop(); + } + // Since at this point it's the base only then we do standard single poly integration + // e.g. x^y + retval = __.integration.poly_integrate(symbol); + } + } + // E.g. a^x or 9^x + else { + const a = /** @type {NerdamerSymbolType} */ (__.diff(symbol.power.clone(), dx)); + if (a.contains(dx)) { + const aa = a.stripVar(dx); + const x = /** @type {NerdamerSymbolType} */ (_.divide(a.clone(), aa.clone())); + if (x.group === S && x.isLinear()) { + aa.multiplier = aa.multiplier.divide(new Frac(2)); + return _.parse( + format( + '({2})*(sqrt(pi)*erf(sqrt(-{0})*{1}))/(2*sqrt(-{0}))', + aa, + dx, + symbol.multiplier + ) + ); + } + __.integration.stop(); + } + if (symbol.isE()) { + if (a.isLinear()) { + retval = symbol; + } else if ( + a.isE() && + isSymbol(a.power) && + a.power.group === S && + a.power.power.equals(1) + ) { + const powerSym = isSymbol(symbol.power) + ? symbol.power + : new NerdamerSymbol(symbol.power); + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.symfunction('Ei', [powerSym.clone()]), powerSym) + ); + } else { + __.integration.stop(); + } + } else { + const d = _.symfunction(LOG, [_.parse(symbol.value)]); + retval = _.divide(symbol, d); + } + retval = _.divide(retval, a); + } + } else if (symbol.isComposite() && symbol.isLinear()) { + const m = _.parse(symbol.multiplier); + symbol.toUnitMultiplier(); + retval = new NerdamerSymbol(0); + symbol.each(elem => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add(retval, __.integrate(elem, dx, depth)) + ); + }); + retval = /** @type {NerdamerSymbolType} */ (_.multiply(m, retval)); + } else if (g === CP) { + if (symbol.power.greaterThan(1)) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + if (symbol.power.equals(1)) { + retval = new NerdamerSymbol(0); + symbol.each(elem => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add(retval, __.integrate(elem, dx, depth)) + ); + }, true); + } else { + const p = Number(symbol.power); + const m = symbol.multiplier.clone(); // Temporarily remove the multiplier + symbol.toUnitMultiplier(); + const // Below we consider the form ax+b + fn = symbol.clone().toLinear(); // Get just the pure function without the power + const decomp = __.integration.decompose_arg(fn, dx); + // I have no idea why I used bx+a and not ax+b. TODO change this to something that makes sense + const b = decomp[3]; + const ax = decomp[2]; + const a = decomp[0]; + const x = decomp[1]; + if (p === -1 && x.group !== PL && x.power.equals(2)) { + const bIsPositive = isInt(b) ? Number(b) > 0 : true; + // We can now check for atan + if (x.group === S && x.power.equals(2) && bIsPositive) { + /// /then we have atan + // abs is redundants since the sign appears in both denom and num. + /** + * @param {NerdamerSymbolType} s + * @returns {NerdamerSymbolType} + */ + const unwrapAbs = function (s) { + let result = new NerdamerSymbol(1); + s.each(elem => { + result = /** @type {NerdamerSymbolType} */ ( + _.multiply(result, elem.fname === 'abs' ? elem.args[0] : elem) + ); + }); + return result; + }; + let A = a.clone(); + let B = b.clone(); + A = /** @type {NerdamerSymbolType} */ (_.pow(A, new NerdamerSymbol(1 / 2))); + B = /** @type {NerdamerSymbolType} */ (_.pow(B, new NerdamerSymbol(1 / 2))); + // Unwrap abs + + const d = _.multiply(unwrapAbs(B), unwrapAbs(A)); + const f = _.symfunction(ATAN, [ + _.divide(_.multiply(a, x.toLinear()), d.clone()), + ]); + retval = _.divide(f, d); + } else if (x.group === S && x.isLinear()) { + retval = _.divide(__.integration.poly_integrate(symbol), a); + // 1/(x^4+1) + } else if (x.power.equals(4)) { + // https://www.freemathhelp.com/forum/threads/55678-difficult-integration-int-1-(1-x-4)-dx + const br = inBrackets; + // Apply rule: ax^4+b = (√ax^2+√2∜a∜bx+√b)(√ax^2-√2∜a∜bx+√b) + // get quadratic factors + const A = _.parse(`${SQRT + br(String(a))}*${dx}^2`); + const B = _.parse( + `${SQRT + br(String(2))}*${br(String(a))}^${br('1/4')}*${br(String(b))}^${br('1/4')}*${dx}` + ); + const C = _.parse(SQRT + br(String(b))); + const f1 = _.add(_.add(A.clone(), B.clone()), C.clone()); + const f2 = _.add(_.subtract(A, B), C); + // Calculate numerators: [D+E, D-E] -> [√2*b^(3/4)+√b∜ax, √2*b^(3/4)-√b∜ax] + const D = _.parse(`${SQRT + br(String(2))}*${br(String(b))}^${br('3/4')}`); + const E = _.parse( + `${SQRT + br(String(b))}*${br(String(b))}^${br('1/4')}*${dx}` + ); + // Let F = 2b√2∜b + const F = _.parse( + `${2}*${br(String(b))}*${SQRT}${br(String(2))}*${br(String(b))}^${br('1/4')}` + ); + // Calculate the factors + const L1 = _.divide( + _.subtract(D.clone(), E.clone()), + _.multiply(F.clone(), f2) + ); + const L2 = _.divide(_.add(D, E), _.multiply(F, f1.clone())); + retval = _.add( + __.integrate(L1, dx, depth, opt), + __.integrate(L2, dx, depth, opt) + ); + // Let's try partial fractions + } else { + retval = __.integration.partial_fraction(symbol, dx, depth); + } + } else if (p === -1 / 2) { + // Detect asin and atan + if (x.group === S && x.power.equals(2)) { + if (ax.multiplier.lessThan(0) && !b.multiplier.lessThan(0)) { + a.negate(); + // It's asin + if (b.isConstant() && a.isConstant()) { + const d = _.symfunction(SQRT, [a.clone()]); + const d2 = _.symfunction(SQRT, [_.multiply(a.clone(), b)]); + retval = _.divide( + _.symfunction(ASIN, [_.divide(ax.toLinear(), d2)]), + d + ); + } + // I'm not sure about this one. I'm trusting Wolfram Alpha here + else { + const sqrtA = _.symfunction(SQRT, [a]); + const sqrtAx = _.multiply(sqrtA.clone(), x.clone().toLinear()); + retval = _.divide( + _.symfunction(ATAN, [ + _.divide(sqrtAx, _.symfunction(SQRT, [fn.clone()])), + ]), + sqrtA + ); + } + } else { + /* WHAT HAPPENS HERE???? e.g. integrate(3/sqrt(-a+b*x^2),x) or integrate(3/sqrt(a+b*x^2),x)*/ + __.integration.stop(); + } + } else { + // This would be a case like 1/(sqrt(1-x^3) or 1/(1-(x+1)^2) + __.integration.stop(); + } + } else if (p === 1 / 2 && x.power.equals(2) && a.greaterThan(0)) { + // TODO: Revisit + // should become (sinh(2*acosh(x))/4-acosh(x)/2)) + __.integration.stop(); + } else if (x.isLinear() && x.group !== PL) { + retval = _.divide(__.integration.poly_integrate(symbol), a); + } else if (x.power.equals(2) && a.greaterThan(0)) { + // 1/(a*x^2+b^2)^n + // strip the value of b so b = 1 + const sqa = _.parse(SQRT + inBrackets(a)); // Strip a so b = 1 + const sqb = _.parse(SQRT + inBrackets(b)); + const aob = /** @type {NerdamerSymbolType} */ ( + _.multiply(sqa.clone(), sqb.clone()) + ).invert(); + const bsqi = _.pow( + b, + new NerdamerSymbol(/** @type {FracType} */ (symbol.power).toDecimal()) + ); + const uv = core.Utils.getU(symbol); + const u = _.multiply(aob, x.clone().toLinear()); + // Use symfunction instead of _.parse(ATAN + inBrackets(u)) to preserve + // exact fractions. String concatenation triggers valueOf() which converts + // fractions to decimals, and parsing them back loses precision. + // e.g., 1/3 → "0.333..." → 321685687669321/965057063007964 + const v = _.symfunction(ATAN, [u]); + // The conversion will be 1+tan(x)^2 -> sec(x)^2 + // since the denominator is now (sec(x)^2)^n and the numerator is sec(x)^2 + // then the remaining sec will be (n-1)*2; + const n = (Math.abs(Number(/** @type {FracType} */ (symbol.power))) - 1) * 2; + // 1/sec(x)^n can now be converted to cos(x)^n and we can pull the integral of that + const integral = /** @type {NerdamerSymbolType} */ ( + __.integrate(_.parse(`${COS + inBrackets(uv)}^${n}`)) + ); + core.Utils.clearU(uv); + return _.multiply(integral.sub(uv, v), bsqi); + } else if ( + symbol.group !== CB && + !(/** @type {FracType} */ (symbol.power).lessThan(0)) + ) { + retval = __.integration.by_parts(symbol, dx, depth, opt); + } else { + const f = symbol.clone().toLinear(); + const factored = /** @type {FactorSubModuleType} */ ( + core.Algebra.Factor + ).factorInner(f); + const wasFactored = factored.toString() !== f.toString(); + if (core.Algebra.degree(f, _.parse(dx)).equals(2) && !wasFactored) { + try { + const sq = core.Algebra.sqComplete(f, dx); + const u = core.Utils.getU(f); + const f1 = sq.f.sub(sq.a, u); + const fx = _.pow(f1, _.parse(symbol.power)); + retval = /** @type {NerdamerSymbolType} */ (__.integrate(fx, u)).sub( + u, + sq.a + ); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + __.integration.stop(); + } + } else { + retval = __.integration.partial_fraction(symbol, dx, depth, opt); + } + } + retval.multiplier = retval.multiplier.multiply(m); + } + } else if (g === FN) { + const arg = symbol.args[0]; + const m = symbol.multiplier.clone(); + symbol.toUnitMultiplier(); + const decomp = __.integration.decompose_arg(arg, dx); + // Easies way I can think of to get the coefficient and to make sure + // that the symbol is linear wrt dx. I'm not actually trying to get the + // derivative + const a = decomp[0]; + const x = decomp[1]; + const { fname } = symbol; + // Log is a special case that can be handled with integration by parts + if (fname === LOG || fname === ASIN || fname === ACOS || (fname === ATAN && x.isLinear())) { + /* Integration by parts */ + const p = symbol.power.toString(); + if (isInt(p)) { + depth -= Number(p); + } // It needs more room to find the integral + + if (arg.isComposite()) { + // Integral u du + const u = core.Utils.getU(symbol); + const f = _.pow(_.parse(LOG + inBrackets(u)), new NerdamerSymbol(p)); + const du = __.diff(arg, dx); + const uDu = _.multiply(f, du); + const integral = /** @type {NerdamerSymbolType} */ ( + __.integrate(uDu, u, depth, opt) + ); + retval = _.multiply(_.parse(m), integral.sub(u, arg)); + } else { + retval = _.multiply(_.parse(m), __.integration.by_parts(symbol, dx, depth, opt)); + } + } else if (fname === TAN && symbol.power.lessThan(0)) { + // Convert to cotangent + const sym = symbol.clone(); + sym.power.negate(); + sym.fname = COT; + return _.multiply(_.parse(m), __.integrate(sym, dx, depth)); + } else { + if (!a.contains(dx, true) && symbol.isLinear()) { + // Perform a deep search for safety + // first handle the special cases + if (fname === ABS) { + // REVISIT **TODO** + const absX = /** @type {NerdamerSymbolType} */ ( + _.divide(arg.clone(), a.clone()) + ); + if (absX.group === S && !absX.power.lessThan(0)) { + if (core.Utils.even(/** @type {FracType} */ (absX.power))) { + retval = __.integrate(arg, dx, depth); + } else { + const integrated = /** @type {NerdamerSymbolType} */ ( + __.integrate(absX, dx, depth) + ); + integrated.power = /** @type {FracType} */ (integrated.power).subtract( + new Frac(1) + ); + retval = _.multiply( + _.multiply(_.symfunction(ABS, [absX.toLinear()]), integrated), + a + ); + } + } else { + __.integration.stop(); + } + } else { + const ag = symbol.args[0].group; + const decomposed = __.integration.decompose_arg(arg, dx); + + if ( + !(ag === CP || ag === S || ag === CB) || + !(/** @type {FracType} */ (decomposed[1].power).equals(1)) || + arg.hasFunc('') + ) { + __.integration.stop(); + } + /** TODO */ // ASIN, ACOS, ATAN + switch (fname) { + case COS: + retval = _.symfunction(SIN, [arg]); + break; + case SIN: + retval = _.symfunction(COS, [arg]); + retval.negate(); + break; + case TAN: + retval = _.parse(format(`${Settings.LOG}(sec({0}))`, arg)); + break; + case SEC: + retval = _.parse(format(`${Settings.LOG}(tan({0})+sec({0}))`, arg)); + break; + case CSC: + retval = _.parse(format(`-${Settings.LOG}(csc({0})+cot({0}))`, arg)); + break; + case COT: + retval = _.parse(format(`${Settings.LOG}(sin({0}))`, arg)); + break; + case SINH: + retval = _.symfunction(COSH, [arg]); + break; + case COSH: + retval = _.symfunction(SINH, [arg]); + break; + case TANH: + retval = _.parse(format(`${Settings.LOG}(cosh({0}))`, arg)); + break; + case ASEC: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + case ACSC: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + case ACOT: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + // Inverse htrig + case ASECH: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + case ACSCH: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + case ACOTH: + retval = __.integration.by_parts(symbol, dx, depth, opt); + break; + // End inverse htrig + // htrigh + case SECH: + retval = _.parse(format('atan(sinh({0}))', arg)); + break; + case CSCH: + retval = _.parse(format(`${Settings.LOG}(tanh(({0})/2))`, arg)); + break; + case COTH: + retval = _.parse(format(`${Settings.LOG}(sinh({0}))`, arg)); + break; + // End htrig + case EXP: + retval = __.integrate(_.parse(format('e^({0})', arg)), dx, depth); + break; + case 'S': { + const sArg = symbol.args[0].clone(); + const sDc = __.integration.decompose_arg(sArg, dx); + const _sX_ = sDc[1]; // Unused, x is used in format string + const sA_ = sDc[0]; + const sB_ = sDc[3]; + retval = _.parse( + format( + '(cos((1/2)*pi*(({1})+({0})*({2}))^2)+pi*(({1})+({0})*({2}))*S(({1})+({0})*({2})))/(({0})*pi)', + sA_, + sB_, + x + ) + ); + break; + } + case 'C': { + const cArg = symbol.args[0].clone(); + const cDc = __.integration.decompose_arg(cArg, dx); + const cX_ = cDc[1]; + const cA_ = cDc[0]; + const cB_ = cDc[3]; + retval = _.parse( + format( + '(pi*(({1})+({0})*({2}))*C(({1})+({0})*({2}))-sin((1/2)*pi*(({1})+({0})*({2}))^2))/(({0})*pi)', + cA_, + cB_, + cX_ + ) + ); + break; + } + case 'erf': { + const erfArg = symbol.args[0].clone(); + const erfDc = __.integration.decompose_arg(erfArg, dx); + const erfX_ = erfDc[1]; + const erfA_ = erfDc[0]; + retval = _.parse( + format( + 'e^(-(({2}))^2)/(({0})*sqrt(pi))+(1/({0})+({1}))*erf(({2}))', + erfA_, + erfX_, + erfArg + ) + ); + break; + } + case 'sign': + retval = _.multiply(symbol.clone(), arg.clone()); + break; + default: + __.integration.stop(); + } + + retval = _.divide(retval, a); + } + } else if (x.isLinear()) { + if (fname === COS || fname === SIN) { + const p = Number(symbol.power); + // Check to see if it's negative and then just transform it to sec or csc + if (p < 0) { + symbol.fname = fname === SIN ? CSC : SEC; + symbol.invert().updateHash(); + retval = __.integrate(symbol, dx, depth); + } else { + const _innerArg = symbol.args[0]; + const rd = symbol.clone(); // Cos^(n-1) + const rd2 = symbol.clone(); // Cos^(n-2) + const q = new NerdamerSymbol((p - 1) / p); // + const na = /** @type {NerdamerSymbolType} */ ( + _.multiply(a.clone(), new NerdamerSymbol(p)) + ).invert(); // 1/(n*a) + rd.power = /** @type {FracType} */ (rd.power).subtract(new Frac(1)); + rd2.power = /** @type {FracType} */ (rd2.power).subtract(new Frac(2)); + + const t = _.symfunction(fname === COS ? SIN : COS, [arg.clone()]); + if (fname === SIN) { + t.negate(); + } + retval = _.add( + _.multiply(_.multiply(na, rd), t), + _.multiply(q, __.integrate(_.parse(rd2), dx, depth)) + ); + } + } + // Tan(x)^n or cot(x)^n + else if (fname === TAN || fname === COT) { + // http://www.sosmath.com/calculus/integration/moretrigpower/moretrigpower.html + if (symbol.args[0].isLinear(dx)) { + const n = /** @type {FracType} */ (symbol.power) + .subtract(new Frac(1)) + .toString(); + let r = symbol.clone().toUnitMultiplier(); + const w = _.parse( + format( + `${fname === COT ? '-' : ''}1/({2}*{0})*{3}({1})^({0})`, + n, + arg, + a, + fname + ) + ); + r.power = /** @type {FracType} */ (r.power).subtract(new Frac(2)); + if (r.power.equals(0)) { + r = /** @type {NerdamerSymbolType} */ (_.parse(r)); + } + retval = /** @type {NerdamerSymbolType} */ ( + _.subtract(w, __.integrate(r, dx, depth)) + ); + } + } + // Sec(x)^n or csc(x)^n + else if (fname === SEC || fname === CSC) { + // http://www.sosmath.com/calculus/integration/moretrigpower/moretrigpower.html + const n1 = /** @type {FracType} */ (symbol.power) + .subtract(new Frac(1)) + .toString(); + const n2 = /** @type {FracType} */ (symbol.power) + .subtract(new Frac(2)) + .toString(); + const f2 = fname === SEC ? TAN : COT; + let r = symbol.clone().toUnitMultiplier(); + const parseStr = format( + `${fname === CSC ? '-' : ''}1/({0}*{1})*{4}({3})^({2})*{5}({3})`, + a, + n1, + n2, + arg, + fname, + f2 + ); + const w = _.parse(parseStr); + r.power = /** @type {FracType} */ (r.power).subtract(new Frac(2)); + if (r.power.equals(0)) { + r = /** @type {NerdamerSymbolType} */ (_.parse(r)); + } + retval = /** @type {NerdamerSymbolType} */ ( + _.add( + w, + _.multiply( + new NerdamerSymbol(Number(n2) / Number(n1)), + __.integrate(r, dx, depth) + ) + ) + ); + } else if ((fname === COSH || fname === SINH) && symbol.power.equals(2)) { + retval = __.integrate(symbol.fnTransform(), dx, depth); + } else { + __.integration.stop(); + } + } else { + __.integration.stop(); + } + + retval.multiplier = retval.multiplier.multiply(m); + } + } else if (g === PL) { + retval = __.integration.partial_fraction(symbol, dx, depth); + } else if (g === CB) { + const den = symbol.getDenom(); + if (den.group === S) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + + // Separate the coefficient since all we care about are symbols containing dx + let coeff = symbol.stripVar(dx); + // Now get only those that apply + let cfsymbol = /** @type {NerdamerSymbolType} */ (_.divide(symbol.clone(), coeff.clone())); // A coeff free symbol + // peform a correction for stripVar. This is a serious TODO! + if (coeff.contains(dx)) { + cfsymbol = /** @type {NerdamerSymbolType} */ (_.multiply(cfsymbol, coeff)); + coeff = new NerdamerSymbol(1); + } + + // If we only have one symbol left then let's not waste time. Just pull the integral + // and let the chips fall where they may + if (cfsymbol.group === CB) { + // We collect the symbols and sort them descending group, descending power, descending alpabethically + const symbols = cfsymbol + .collectSymbols() + .sort( + /** + * @param {NerdamerSymbolType} s1 + * @param {NerdamerSymbolType} s2 + */ + (s1, s2) => { + if (s1.group === s2.group) { + if (Number(s1.power) === Number(s2.power)) { + if (s1 < s2) { + return 1; + } // I want sin first + + return -1; + } + return Number(s2.power) - Number(s1.power); // Descending power + } + return s2.group - s1.group; // Descending groups + } + ) + .map( + /** + * @param {NerdamerSymbolType} elem + * @returns {NerdamerSymbolType} + */ + elem => { + const unwrapped = NerdamerSymbol.unwrapSQRT(elem, true); + if (unwrapped.fname === EXP) { + return /** @type {NerdamerSymbolType} */ ( + _.parse( + format('({1})*e^({0})', unwrapped.args[0], unwrapped.multiplier) + ) + ); + } + return /** @type {NerdamerSymbolType} */ (unwrapped); + } + ); + const l = symbols.length; + if (Number(symbol.power) < 0) { + if (l === 2) { + return __.integrate( + /** @type {NerdamerSymbolType} */ (_.expand(symbol)), + dx, + depth, + opt + ); + } + } + // Otherwise the denominator is one lumped together symbol + // Generate an image for + else if (l === 2) { + // Try u substitution + try { + retval = __.integration.u_substitution(symbols, dx); + } catch (e) { + /* Failed :`(*/ + if (e.message === 'timeout') { + throw e; + } + } + + if (!retval) { + // No success with u substitution so let's try known combinations + // are they two functions + const g1 = symbols[0].group; + const g2 = symbols[1].group; + let sym1 = symbols[0]; + let sym2 = symbols[1]; + const fn1 = sym1.fname; + const fn2 = sym2.fname; + // Reset the symbol minus the coeff + symbol = /** @type {NerdamerSymbolType} */ ( + _.multiply(sym1.clone(), sym2.clone()) + ); + if (g1 === FN && g2 === FN) { + if (fn1 === LOG || fn2 === LOG) { + retval = __.integration.by_parts(symbol.clone(), dx, depth, opt); + } else { + symbols.sort((s1, s2) => (s2.fname > s1.fname ? 1 : -1)); + const arg1 = sym1.args[0]; + // Make sure the arguments are suitable. We don't know how to integrate non-linear arguments + if ( + !arg1.isLinear() || + !(arg1.group === CP || arg1.group === CB || arg1.group === S) + ) { + __.integration.stop(); + } + + const decomp = __.integration.decompose_arg(arg1, dx); + const x = decomp[1]; + const a = decomp[0]; + if (!x.isLinear()) // Again... linear arguments only wrt x + { + __.integration.stop(); + } + + // They have to have the same arguments and then we have cleared all the check to + // make sure we can integrate FN & FN + const arg2 = sym2.args[0]; + // Make sure that their argument matches + if (arg1.equals(arg2)) { + if ((fn1 === SIN && fn2 === COS) || (fn1 === COS && fn2 === SIN)) { + if (/** @type {FracType} */ (sym1.power).lessThan(0)) { + __.integration.stop(); + } // We don't know how to handle, sin(x)^n/cos(x)^m where m > n, yet + // if it's in the form sin(x)^n*cos(x)^n then we can just return tan(x)^n which we know how to integrate + if ( + fn1 === SIN && + /** @type {FracType} */ (sym1.power) + .add(/** @type {FracType} */ (sym2.power)) + .equals(0) + ) { + sym1.fname = TAN; + sym1.updateHash(); + retval = __.integrate(sym1, dx, depth); + } else if ( + even(/** @type {FracType} */ (sym1.power)) && + fn2 === COS && + /** @type {FracType} */ (sym2.power).lessThan(0) + ) { + // Transform sin^(2*n) to (1-cos^2)^n + const n = Number(sym1.power) / 2; + const newSym = _.parse( + format('(1-cos({0})^2)^({1})', sym1.args[0], n) + ); + retval = __.integrate( + _.expand(_.multiply(newSym, sym2.clone())), + dx, + depth, + opt + ); + } else if ( + even(/** @type {FracType} */ (sym1.power)) && + fn2 === SIN && + /** @type {FracType} */ (sym2.power).lessThan(0) + ) { + // Transform cos^(2*n) to (1-sin^2)^n + const n = Number(sym1.power) / 2; + const newSym = _.parse( + format('(1-sin({0})^2)^({1})', sym1.args[0], n) + ); + retval = __.integrate( + _.expand(_.multiply(newSym, sym2.clone())), + dx, + depth, + opt + ); + } else { + const p1Even = core.Utils.even( + /** @type {FracType} */ (sym1.power) + ); + const p2Even = core.Utils.even( + /** @type {FracType} */ (sym2.power) + ); + retval = new NerdamerSymbol(0); + if (!p1Even || !p2Even) { + let u; + let r; + // Since cos(x) is odd it carries du. If sin was odd then it would be the other way around + // know that p1 satifies the odd portion in this case. If p2 did than it would contain r + if (p1Even) { + u = sym1; + r = sym2; + } else { + // U = sin(x) + u = sym2; + r = sym1; + } + // Get the sign of du. In this case r carries du as stated before and D(cos(x),x) = -sin(x) + const sign = u.fname === COS ? -1 : 1; + const n = Number(r.power); + // Remove the du e.g. cos(x)^2*sin(x)^3 dx -> cos(x)^2*sin(x)^2*sin(x). We're left with two + // even powers afterwards which can be transformed + const k = (n - 1) / 2; + // Make the transformation cos(x)^2 = 1 - sin(x)^2 + const trigTrans = _.parse( + `(1-${u.fname}${core.Utils.inBrackets( + arg1.toString() + )}^2)^${k}` + ); + const sym = _.expand( + _.multiply( + new NerdamerSymbol(sign), + _.multiply(u.clone(), trigTrans) + ) + ); + // We can now just loop through and integrate each since it's now just a polynomial with functions + sym.each(elem => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add( + retval, + __.integration.poly_integrate(elem.clone()) + ) + ); + }); + } else { + // Performs double angle transformation + const doubleAngle = function (s) { + const pow = s.power; + const k = pow / 2; + let e; + if (s.fname === COS) { + e = `((1/2)+(cos(2*(${s.args[0]}))/2))^${k}`; + } else { + e = `((1/2)-(cos(2*(${s.args[0]}))/2))^${k}`; + } + + return _.parse(e); + }; + // They're both even so transform both using double angle identities and we'll just + // be able to integrate by the sum of integrals + const daA = doubleAngle(sym1); + const daB = doubleAngle(sym2); + const t = _.multiply(daA, daB); + const sym = _.expand(t); + sym.each(elem => { + retval = _.add( + retval, + __.integrate(elem, dx, depth) + ); + }); + return _.multiply(retval, coeff); + } + } + } + // Tan(x)*sec(x)^n + else if ( + fn1 === SEC && + fn2 === TAN && + x.isLinear() && + sym2.isLinear() + ) { + retval = _.parse( + format('sec({0})^({1})/({1})', sym1.args[0], sym1.power) + ); + } else if (fn1 === TAN && fn2 === SEC && x.isLinear()) { + // Remaining: tan(x)^3*sec(x)^6 + if (sym1.isLinear() && sym2.isLinear()) { + retval = _.divide(_.symfunction(SEC, [arg1.clone()]), a); + } else if (even(/** @type {FracType} */ (sym1.power))) { + const p = Number(sym1.power) / 2; + // Transform tangent + const t = _.parse( + format('(sec({0})^2-1)^({1})', sym1.args[0], p) + ); + retval = __.integrate( + _.expand(_.multiply(t, sym2)), + dx, + depth + ); + } else { + __.integration.stop(); + } + } else if (fn1 === SEC && fn2 === COS) { + sym1.fname = COS; + sym1.invert().updateHash(); + retval = __.integrate(_.multiply(sym1, sym2), dx, depth); + } else if (fn1 === SIN && fn2 === CSC) { + sym2.fname = SIN; + sym2.invert().updateHash(); + retval = __.integrate(_.multiply(sym1, sym2), dx, depth); + } + // Tan/cos + else if ( + fn1 === TAN && + (fn2 === COS || fn2 === SIN) && + sym2.power.lessThan(0) + ) { + const t = _.multiply(sym1.fnTransform(), sym2); + retval = __.integrate(_.expand(t), dx, depth); + } else { + const t = _.multiply(sym1.fnTransform(), sym2.fnTransform()); + retval = __.integrate(_.expand(t), dx, depth); + } + } + // TODO: In progress + else if ((fn1 === SIN || fn1 === COS) && (fn2 === SIN || fn2 === COS)) { + if (sym1.isLinear() && sym2.isLinear()) { + // If in the form cos(a*x)*sin(b*x) + if (sym1.args[0].isLinear() && sym2.args[0].isLinear()) { + // Use identity (sin(b*x+a*x)+sin(b*x-a*x))/2 + let ax; + let bx; + if (fn2 === SIN) { + ax = sym1.args[0]; + bx = sym2.args[0]; + } else { + bx = sym1.args[0]; + ax = sym2.args[0]; + } + + // Make the transformation + const f = _.parse( + format( + '(sin(({1})+({0}))+sin(({1})-({0})))/2', + ax.toString(), + bx.toString() + ) + ); + + // Integrate it + retval = __.integrate(f, dx, depth); + } else { + const transformed = trigTransform(symbols); + retval = __.integrate(_.expand(transformed), dx, depth); + } + } else { + let transformed = new NerdamerSymbol(1); + symbols.forEach(s => { + const transformedS = s.fnTransform(); + transformed = /** @type {NerdamerSymbolType} */ ( + _.multiply(transformed, transformedS) + ); + }); + const t = /** @type {NerdamerSymbolType} */ ( + _.expand(transformed) + ); + + retval = /** @type {NerdamerSymbolType} */ ( + __.integrate(t, dx, depth) + ); + + if (retval.hasIntegral()) { + retval = __.integrate( + trigTransform( + /** @type {NerdamerSymbolType[]} */ ( + transformed.collectSymbols() + ) + ), + dx, + depth + ); + } + } + } else { + __.integration.stop(); + } + } + } else if (g1 === FN && g2 === S) { + const sym1IsLinear = sym1.isLinear(); + if (sym1.fname === COS && sym1IsLinear && sym2.power.equals(-1)) { + retval = _.symfunction('Ci', [sym1.args[0]]); + } else if (sym1.fname === COS && sym2.power.equals(-1)) { + retval = __.integrate( + _.multiply(sym1.fnTransform(), sym2.clone()), + dx, + depth + ); + } else if (sym1.fname === COSH && sym1IsLinear && sym2.power.equals(-1)) { + retval = _.symfunction('Chi', [sym1.args[0]]); + } else if (sym1.fname === COSH && sym2.power.equals(-1)) { + retval = __.integrate( + _.multiply(sym1.fnTransform(), sym2.clone()), + dx, + depth + ); + } else if (sym1.fname === SIN && sym1IsLinear && sym2.power.equals(-1)) { + retval = _.symfunction('Si', [sym1.args[0]]); + } else if (sym1.fname === SIN && sym2.power.equals(-1)) { + retval = __.integrate( + _.multiply(sym1.fnTransform(), sym2.clone()), + dx, + depth + ); + } else if (sym1.fname === SINH && sym1IsLinear && sym2.power.equals(-1)) { + retval = _.symfunction('Shi', [sym1.args[0]]); + } else if (sym1.fname === SINH && sym2.power.equals(-1)) { + retval = __.integrate( + _.multiply(sym1.fnTransform(), sym2.clone()), + dx, + depth + ); + } else if (sym1.fname === LOG && sym2.power.equals(-1)) { + // Log(x)^n/x = log(x)^(n+1)/(n+1) + retval = __.integration.poly_integrate(sym1, dx, depth); + } else if (sym1.fname === 'erf') { + if (sym2.power.equals(1)) { + const dc = __.integration.decompose_arg(sym1.args[0], dx); + const a_ = dc[0]; + const x_ = dc[1]; + const arg = sym1.args[0].toString(); + retval = _.parse( + format( + '(e^(-(({2}))^2)*(sqrt(pi)*e^((({2}))^2)*(2*({0})^2*({1})^2-3)*erf(({2}))+2*({0})*({1})-2))/(4*sqrt(pi)*({0})^2)', + a_, + x_, + arg + ) + ); + } + } else { + // Since group S is guaranteed convergence we need not worry about tracking depth of integration + retval = __.integration.by_parts(symbol, dx, depth, opt); + } + } else if (g1 === EX && g2 === S) { + const x = + fn1 === LOG ? __.integration.decompose_arg(sym1.args[0], dx)[1] : null; + if ( + sym1.isE() && + hasPowerGroupSOrCB(sym1) && + /** @type {FracType} */ (sym2.power).equals(-1) + ) { + retval = _.symfunction('Ei', [ + /** @type {NerdamerSymbolType} */ (sym1.power.clone()), + ]); + } else if (fn1 === LOG && x.value === sym2.value) { + retval = __.integration.poly_integrate(sym1); + } else { + retval = __.integration.by_parts(symbol, dx, depth, opt); + } + } else if (g1 === PL && g2 === S) { + // First try to reduce the top + if ( + sym2.value === sym1.value && + /** @type {FracType} */ (sym1.power).equals(-1) + ) { + // Find the lowest power in the denominator + const pd = Math.min.apply(null, core.Utils.keys(sym1.symbols)); + // Get the lowest common value between denominator and numerator + const pc = Math.min(pd, Number(sym2.power)); + // Reduce both denominator and numerator by that factor + const factor = sym2.clone(); + factor.power = new Frac(pc); + sym2 = /** @type {NerdamerSymbolType} */ ( + _.divide(sym2, factor.clone()) + ); // Reduce the denominator + let t = new NerdamerSymbol(0); + sym1.each(elem => { + t = /** @type {NerdamerSymbolType} */ ( + _.add(t, _.divide(elem.clone(), factor.clone())) + ); + }); + t.multiplier = sym1.multiplier; + symbol = /** @type {NerdamerSymbolType} */ (_.divide(sym2, t)); + } else { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + retval = __.integration.partial_fraction(symbol, dx, depth); + } else if (g1 === CP && g2 === S) { + const f = sym1.clone().toLinear(); + const fIsLinear = core.Algebra.degree(f, _.parse(dx)).equals(1); + // Handle cases x^(2*n)/sqrt(1-x^2) + if (sym1.power.equals(-1 / 2)) { + const decomp = __.integration.decompose_arg( + sym1.clone().toLinear(), + dx + ); + const a = decomp[0].negate(); + const x = decomp[1]; + const b = decomp[3]; + const p1 = Number(sym1.power); + const p2 = Number(sym2.power); + if (isInt(p2) && core.Utils.even(p2) && x.power.equals(2)) { + // If the substitution + let c = _.divide( + _.multiply( + _.pow(b.clone(), new NerdamerSymbol(2)), + _.symfunction(SQRT, [_.divide(b.clone(), a.clone())]) + ), + _.pow(a.clone(), new NerdamerSymbol(2)) + ); + c = _.multiply(c, _.symfunction(SQRT, [b]).invert()); + const dummy = _.parse('sin(u)'); + dummy.power = /** @type {FracType} */ (dummy.power).multiply( + /** @type {FracType} */ (sym2.power) + ); + const integral = /** @type {NerdamerSymbolType} */ ( + __.integrate(dummy, 'u', depth) + ); + const bksub = _.parse(`${ASIN}(${SQRT}(${a}/${b})*${dx})`); + retval = _.multiply( + c, + integral.sub(new NerdamerSymbol('u'), bksub) + ); + } else if (p1 === -1 / 2) { + const uTransform = function (func, subst) { + const intg = _.parse( + /** @type {NerdamerSymbolType} */ ( + __.integrate(func, dx, depth, opt) + ).sub(dx, format(subst, dx)) + ); + if (!intg.hasIntegral()) { + return intg; + } + return undefined; + }; + if (p2 === -1) { + retval = uTransform( + /** @type {NerdamerSymbolType} */ ( + _.expand( + _.expand( + _.pow( + _.multiply(sym1.invert(), sym2.invert()), + new NerdamerSymbol(2) + ) + ) + ) + ).invert(), + 'sqrt(1-1/({0})^2)' + ); + } else if (p2 === -2) { + // Apply transformation to see if it matches asin(x) + retval = uTransform( + /** @type {NerdamerSymbolType} */ ( + _.sqrt( + /** @type {NerdamerSymbolType} */ ( + _.expand( + /** @type {NerdamerSymbolType} */ ( + _.divide( + /** @type {NerdamerSymbolType} */ ( + _.pow( + symbol, + new NerdamerSymbol(2) + ) + ).invert(), + _.pow( + new NerdamerSymbol(dx), + new NerdamerSymbol(2) + ) + ) + ).negate() + ) + ) + ) + ).invert(), + 'sqrt(1-1/({0})^2)' + ); + } + } + } else if (sym1.power.equals(-1) && sym2.isLinear() && fIsLinear) { + retval = __.integration.partial_fraction(symbol, dx, depth); + } else if (!sym1.power.lessThan(0) && isInt(sym1.power)) { + // Sum of integrals + const expanded = _.expand(sym1); + retval = new NerdamerSymbol(0); + expanded.each(elem => { + if (elem.group === PL) { + elem.each(inner => { + retval = _.add( + retval, + __.integrate(_.multiply(sym2.clone(), inner), dx, depth) + ); + }); + } else { + retval = _.add( + retval, + __.integrate(_.multiply(sym2.clone(), elem), dx, depth) + ); + } + }); + } else if (sym1.power.lessThan(-2)) { + retval = __.integration.by_parts(symbol, dx, depth, opt); + } else if (sym1.power.lessThan(0) && sym2.power.greaterThan(1)) { + const decomp = __.integration.decompose_arg( + sym1.clone().toLinear(), + dx + ); + const _a = decomp[0].negate(); + const x = decomp[1]; + const b = decomp[3]; + const fn = sym1.clone().toLinear(); + + if (x.group !== PL && x.isLinear()) { + const p = Number(sym2.power); + const du = '_u_'; + const u = new NerdamerSymbol(du); + // Pull the integral with the subsitution + const U = _.expand( + _.divide( + _.pow( + _.subtract(u.clone(), b.clone()), + new NerdamerSymbol(p) + ), + u.clone() + ) + ); + /** @type {Record} */ + const scope = {}; + + // Generate a scope for resubbing the symbol + scope[du] = /** @type {NerdamerSymbolType} */ (fn); + const U2 = /** @type {NerdamerSymbolType} */ ( + _.parse(/** @type {NerdamerSymbolType} */ (U), scope) + ); + retval = __.integrate(U2, dx, 0); + } else if ( + /** @type {FracType} */ (sym2.power).greaterThan( + /** @type {FracType} */ (x.power) + ) || + /** @type {FracType} */ (sym2.power).equals( + /** @type {FracType} */ (x.power) + ) + ) { + // Factor out coefficients + const factors = new /** @type {AlgebraClassesSubModuleType} */ ( + core.Algebra.Classes + ).Factors(); + sym1 = /** @type {FactorSubModuleType} */ ( + core.Algebra.Factor + ).coeffFactor(sym1.invert(), factors); + const div = core.Algebra.divide(sym2, sym1); + // It assumed that the result will be of group CB + if (/** @type {NerdamerSymbolType} */ (div).group === CB) { + // Try something else + retval = __.integration.by_parts(symbol, dx, depth, opt); + } else { + retval = new NerdamerSymbol(0); + /** @type {NerdamerSymbolType} */ (div).each(elem => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add(retval, __.integrate(elem, dx, depth)) + ); + }); + // Put back the factors + factors.each(factor => { + retval = _.divide(retval, factor); + }); + + retval = _.expand(retval); + } + } else { + retval = __.integration.partial_fraction(symbol, dx, depth); + } + // Handle cases such as (1-x^2)^(n/2)*x^(m) where n is odd ___ cracking knuckles... This can get a little hairy + } else if (/** @type {FracType} */ (sym1.power).den.equals(2)) { + // Assume the function is in the form (a^2-b*x^n)^(m/2) + const dc = /** @type {NerdamerSymbolType[]} */ ( + __.integration.decompose_arg(sym1.clone().toLinear(), dx) + ); + // Using the above definition + const a = dc[3]; + const x = dc[1]; + const b = dc[0]; + const _bx = dc[2]; + if (/** @type {FracType} */ (x.power).equals(2) && b.lessThan(0)) { + // If n is even && b is negative + // make a equal 1 so we can do a trig sub + if (!a.equals(1)) { + // Divide a out of everything + // move a to the coeff + coeff = /** @type {NerdamerSymbolType} */ ( + _.multiply(coeff, _.pow(a, new NerdamerSymbol(2))) + ); + } + const u = dx; + const c = /** @type {NerdamerSymbolType} */ ( + _.divide( + _.pow(b.clone().negate(), new NerdamerSymbol(1 / 2)), + _.pow(a, new NerdamerSymbol(1 / 2)) + ) + ); + const du = _.symfunction(COS, [new NerdamerSymbol(u)]); + const cosn = _.pow( + _.symfunction(COS, [new NerdamerSymbol(u)]), + new NerdamerSymbol( + Number(/** @type {FracType} */ (sym1.power).num) + ) + ); + const X = _.pow( + _.symfunction(SIN, [new NerdamerSymbol(u)]), + new NerdamerSymbol(Number(/** @type {FracType} */ (sym2.power))) + ); + const val = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.multiply(cosn, du), X) + ); + const integral = /** @type {NerdamerSymbolType} */ ( + __.integrate(val, u, depth) + ); + // But remember that u = asin(sqrt(b)*a*x) + retval = integral.sub( + u, + _.symfunction(ASIN, [_.multiply(new NerdamerSymbol(dx), c)]) + ); + } else { + retval = __.integration.partial_fraction(symbol, dx, depth, opt); + } + } else if (fIsLinear) { + retval = __.integration.partial_fraction(symbol, dx, depth); + } + } else if (sym1.isComposite() && sym2.isComposite()) { + // Sum of integrals + retval = new NerdamerSymbol(0); + if (sym1.power.greaterThan(0) && sym2.power.greaterThan(0)) { + // Combine and pull the integral of each + const sym = _.expand(symbol); + sym.each(elem => { + retval = _.add(retval, __.integrate(elem, dx, depth)); + }, true); + } else { + const p1 = Number(sym1.power); + const p2 = Number(sym2.power); + if (p1 < 0 && p2 > 0) { + // Swap + const t = sym1; + sym1 = sym2; + sym2 = t; + } + if (p1 === -1 && p2 === -1) { + retval = __.integration.partial_fraction(symbol, dx, depth); + } else { + sym1.each(elem => { + const k = _.multiply(elem, sym2.clone()); + const intg = __.integrate(k, dx, depth); + retval = /** @type {NerdamerSymbolType} */ ( + _.add(retval, intg) + ); + }); + } + } + } else if ( + g1 === CP && + /** @type {FracType} */ (symbols[0].power).greaterThan(0) + ) { + sym1 = /** @type {NerdamerSymbolType} */ (_.expand(sym1)); + retval = new NerdamerSymbol(0); + sym1.each(elem => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add( + retval, + __.integrate( + /** @type {NerdamerSymbolType} */ ( + _.multiply(elem, sym2.clone()) + ), + dx, + depth + ) + ) + ); + }, true); + } else if (g1 === FN && g2 === EX && core.Utils.inHtrig(sym1.fname)) { + sym1 = sym1.fnTransform(); + retval = __.integrate(_.expand(_.multiply(sym1, sym2)), dx, depth); + } else if ((g1 === FN && g2 === CP) || (g2 === FN && g1 === CP)) { + if (g2 === FN && g1 === CP) { + const t = sym1; + sym1 = sym2; + sym2 = t; // Swap + } + let p; + let q; + let sa; + let sb; + const du = NerdamerSymbol.unwrapSQRT( + /** @type {NerdamerSymbolType} */ (__.diff(sym1.clone(), dx)), + true + ); + const sym2Clone = NerdamerSymbol.unwrapSQRT(sym2, true); + if ( + /** @type {FracType} */ (du.power).equals( + /** @type {FracType} */ (sym2Clone.power) + ) + ) { + p = new NerdamerSymbol(Number(sym2.power)); + sa = du.clone().toLinear(); + sb = sym2.clone().toLinear(); + q = /** @type {NerdamerSymbolType} */ ( + core.Algebra.divide(sa.toLinear(), sb) + ); + if (q.isConstant()) { + const nq = _.pow(q, p.negate()); + retval = _.multiply( + nq, + __.integration.poly_integrate(sym1.clone()) + ); + } + } else { + retval = __.integration.by_parts(symbol, dx, depth, opt); + } + } else { + const syma = sym1.clone().toLinear(); + const symb = sym2.clone().toLinear(); + if ( + g1 === EX && + g2 === EX && + /** @type {NerdamerSymbolType} */ (sym1.power).contains(dx) && + /** @type {NerdamerSymbolType} */ (sym2.power).contains(dx) && + !syma.contains(dx) && + !symb.contains(dx) + ) { + retval = /** @type {NerdamerSymbolType} */ ( + _.parse( + format( + '(({0})^(({2})*({4}))*({1})^(({3})*({4})))/(log(({0})^({2}))+log(({1})^({3})))', + syma.toString(), + symb.toString(), + /** @type {NerdamerSymbolType} */ ( + sym1.power + ).multiplier.toString(), + /** @type {NerdamerSymbolType} */ ( + sym2.power + ).multiplier.toString(), + dx + ) + ) + ); + } else { + retval = __.integration.by_parts(symbol, dx, depth, opt); + } + } + } + } else if ( + l === 3 && + ((symbols[2].group === S && + /** @type {FracType} */ (symbols[2].power).lessThan(2)) || + symbols[0].group === CP) + ) { + let first = symbols[0]; + if (first.group === CP) { + // TODO {support higher powers of x in the future} + if (/** @type {FracType} */ (first.power).greaterThan(1)) { + first = /** @type {NerdamerSymbolType} */ (_.expand(first)); + } + const r = _.multiply(symbols[1], symbols[2]); + retval = new NerdamerSymbol(0); + first.each(elem => { + const prod = _.multiply(elem, r.clone()); + const intg = __.integrate(prod, dx, depth); + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, intg)); + }, true); + } else { + // Try integration by parts although technically it will never work + retval = __.integration.by_parts(symbol, dx, depth, opt); + } + } else if (allFunctions(symbols)) { + let t = new NerdamerSymbol(1); + for (let i = 0, len = symbols.length; i < len; i++) { + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, symbols[i].fnTransform())); + } + t = /** @type {NerdamerSymbolType} */ (_.expand(t)); + retval = __.integrate(t, dx, depth); + } else { + // One more go + const transformed = trigTransform(symbols); + retval = __.integrate( + /** @type {NerdamerSymbolType} */ (_.expand(transformed)), + dx, + depth + ); + } + } else { + if (cfsymbol.equals(1)) { + return __.integrate( + /** @type {NerdamerSymbolType} */ (_.expand(symbol)), + dx, + depth + ); + } + + // Only factor for multivariate which are polynomials + if ( + cfsymbol.clone().toLinear().isPoly(true) && + core.Utils.variables(cfsymbol).length > 1 + ) { + cfsymbol = /** @type {FactorSubModuleType} */ (core.Algebra.Factor).factorInner( + cfsymbol + ); + } + + retval = __.integrate(cfsymbol, dx, depth); + } + + retval = _.multiply(retval, coeff); + } + // If an integral was found then we return it + if (retval) { + return retval; + } + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + // Do nothing if it's a NoIntegralFound error otherwise let it bubble + if (!(error instanceof NoIntegralFound || error instanceof core.exceptions.DivisionByZero)) { + throw error; + } + } + + // No symbol found so we return the integral again + const dtStr = isSymbol(dt) ? dt.toString() : dt; + return /** @type {NerdamerSymbolType} */ ( + _.symfunction('integrate', [originalSymbol, new NerdamerSymbol(dtStr)]) + ); + }, + false + ); + }, + /** + * Definite integral from `from` to `to` + * + * @param {NerdamerSymbolType} symbol + * @param {NerdamerSymbolType} from + * @param {NerdamerSymbolType} to + * @param {string} [dx] + * @returns {NerdamerSymbolType} + */ + defint(symbol, from, to, dx) { + dx ||= 'x'; // Make x the default variable of integration + /** + * @param {NerdamerSymbolType} integral + * @param {Record} vars + * @param {NerdamerSymbolType} point + * @returns {NerdamerSymbolType} + */ + const getValue = function (integral, vars, point) { + try { + return /** @type {NerdamerSymbolType} */ (_.parse(integral, vars)); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // It failed for some reason so return the limit + const lim = /** @type {NerdamerSymbolType} */ (__.Limit.limit(integral, dx, point)); + return lim; + } + }; + + const vars = core.Utils.variables(symbol); + const hasTrig = symbol.hasTrig(); + let retval; + let integral; + + // Fix #593 - Only assume the first variable if dx is not defined. + if (vars.length === 1 && !dx) { + dx = vars[0]; + } + + if (!hasTrig) { + integral = /** @type {NerdamerSymbolType} */ (__.integrate(symbol, dx)); + } + + if (!hasTrig && !integral.hasIntegral()) { + /** @type {Record} */ + const upper = {}; + /** @type {Record} */ + const lower = {}; + upper[dx] = to; + lower[dx] = from; + + const a = getValue(integral, upper, to); + const b = getValue(integral, lower, from); + retval = /** @type {NerdamerSymbolType} */ (_.subtract(a, b)); + } else if (vars.length === 1 && from.isConstant() && to.isConstant()) { + const f = core.Build.build(symbol); + retval = new NerdamerSymbol( + core.Math2.num_integrate(/** @type {(x: number) => number} */ (f), Number(from), Number(to)) + ); + } else { + retval = /** @type {NerdamerSymbolType} */ ( + _.symfunction('defint', [symbol, from, to, new NerdamerSymbol(dx)]) + ); + } + return retval; + }, + + Limit: { + /** + * @param {string} start + * @param {string} end + * @returns {VectorType} + */ + interval(start, end) { + return /** @type {VectorType} */ (/** @type {unknown} */ (_.parse(format('[{0}, {1}]', start, end)))); + }, + diverges() { + return __.Limit.interval('-Infinity', 'Infinity'); + }, + /** + * Computes limit using L'Hopital's rule for 0/0 or inf/inf forms. + * + * @param {NerdamerSymbolType} f - Numerator + * @param {NerdamerSymbolType} g - Denominator + * @param {string} x - Variable + * @param {NerdamerSymbolType} lim - Limit value + * @param {number} depth - Recursion depth + * @returns {NerdamerSymbolType | VectorType | MatrixType | undefined} + */ + divide(f, g, x, lim, depth) { + if (depth++ > Settings.max_lim_depth) { + return undefined; + } + + const _fin = f.clone(); + const gin = g.clone(); + + // But first a little "cheating". x/|x| ends up in an infinite loop since the d/dx |x| -> x/|x| + // To break this loop we simply provide the answer. Keep in mind that currently limit only provides + // the two-sided limit. + // Known limit + if (g.fname === ABS) { + const sign = f.sign(); + const limSign = lim.sign(); + + if (/** @type {NerdamerSymbolType} */ (lim).isInfinity) { + return _.multiply(new NerdamerSymbol(sign), new NerdamerSymbol(limSign)); + } + if (lim.equals(0)) { + const fm = _.parse(f.multiplier); + const gm = _.parse(g.multiplier); + return _.divide(_.multiply(fm, __.Limit.interval('-1', '1')), gm); + } + // TODO: Support more limits + return __.Limit.diverges(); + } + + /** + * @param {NerdamerSymbolType | VectorType} L + * @returns {boolean} + */ + const isInfinity = function (L) { + if (core.Utils.isVector(L)) { + const vec = /** @type {VectorType} */ (L); + for (let i = 0; i < vec.elements.length; i++) { + if (!(/** @type {NerdamerSymbolType} */ (vec.elements[i]).isInfinity)) { + return false; + } + } + return true; + } + return /** @type {NerdamerSymbolType} */ (L).isInfinity; + }; + + const equals = function (L, v) { + if (core.Utils.isVector(L)) { + return false; + } + return L.equals(v); + }; + + let retval; + let count = 0; + let lim1; + let lim2; + let indeterminate; + // Let fOrig = f.clone(); + // let gOrig = g.clone(); + do { + lim1 = evaluate(/** @type {NerdamerSymbolType} */ (__.Limit.limit(f.clone(), x, lim, depth))); + lim2 = evaluate(/** @type {NerdamerSymbolType} */ (__.Limit.limit(g.clone(), x, lim, depth))); + + // If it's in indeterminate form apply L'Hopital's rule + indeterminate = (isInfinity(lim1) && isInfinity(lim2)) || (equals(lim1, 0) && equals(lim2, 0)); + // Pull the derivatives + if (indeterminate) { + const ft = __.diff(f.clone(), x); + const gt = __.diff(g.clone(), x); + + // Expanding here causes issue #12. + // there is something fishy with expand that we will + // have to find some day. + // let tSymbol = _.expand(_.divide(ft, gt)); + const tSymbol = /** @type {NerdamerSymbolType} */ (_.divide(ft, gt)); + f = tSymbol.getNum(); + g = tSymbol.getDenom(); + } + } while (indeterminate && ++count < Settings.max_lim_depth); + + if (count >= Settings.max_lim_depth) { + // Console.log("L'Hospital likely endless loop"); + // console.log(" f:"+f); + // console.log(" g:"+g); + return undefined; + } + + // REMEMBER: + // - 1/cos(x) + // n/0 is still possible since we only checked for 0/0 + const denIsZero = lim2.equals(0); + const _p = Number(gin.power); + + if (lim.isConstant(true) && denIsZero) { + // The sign of infinity depends on: + // - For even powers (x^2, x^4, etc.): denominator is always positive, so sign = sign(lim1) + // - For odd powers (x, x^3, etc.): two-sided limit doesn't exist, but we return + // the right-hand limit by convention, so sign = sign(lim1) + // In both cases, if lim1 < 0, the result is -Infinity + retval = NerdamerSymbol.infinity(lim1.lessThan(0) ? -1 : undefined); + } else if (denIsZero) { + retval = __.Limit.diverges(); + } else { + retval = _.divide(lim1, lim2); + } + + return retval; + }, + /** + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + rewriteToLog(symbol) { + const p = /** @type {NerdamerSymbolType} */ (symbol.power.clone()); + symbol.toLinear(); + return /** @type {NerdamerSymbolType} */ ( + _.pow( + new NerdamerSymbol('e'), + /** @type {NerdamerSymbolType} */ (_.multiply(p, _.symfunction(`${Settings.LOG}`, [symbol]))) + ) + ); + }, + /** + * @param {NerdamerSymbolType} f + * @param {string} x + * @param {NerdamerSymbolType} lim + * @returns {NerdamerSymbolType} + */ + getSubbed(f, x, lim) { + let retval; + // 1. rewrite EX with base e + if (f.group === EX) { + f = /** @type {NerdamerSymbolType} */ (__.Limit.rewriteToLog(f)); + } + // 2. try simple substitution + try { + retval = f.sub(x, lim); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // Nope. No go, so just return the unsubbed function so we can test the limit instead. + retval = f; + } + + return retval; + }, + isInterval(limit) { + return core.Utils.isVector(limit); + }, + /** + * @param {NerdamerSymbolType | VectorType} limit + * @returns {boolean} + */ + isConvergent(limit) { + // It's not convergent if it lies on the interval -Infinity to Infinity + if ( + // It lies on the interval -Infinity to Infinity + (__.Limit.isInterval(limit) && + /** @type {NerdamerSymbolType} */ (/** @type {VectorType} */ (limit).elements[0]).isInfinity && + /** @type {NerdamerSymbolType} */ (/** @type {VectorType} */ (limit).elements[1]).isInfinity) || + // We weren't able to calculate the limit + /** @type {NerdamerSymbolType} */ (limit).containsFunction('limit') + ) { + return false; // Then no + } + return true; // It is + }, + /** + * @param {NerdamerSymbolType} symbol + * @param {string} x + * @param {NerdamerSymbolType} lim + * @param {number} [depth] + * @returns {NerdamerSymbolType | VectorType | undefined} + */ + limit(symbol, x, lim, depth) { + // Simplify the symbol + if (symbol.isLinear() && symbol.isComposite()) { + // Apply sum of limits + let limit = new NerdamerSymbol(0); + symbol.each(s => { + limit = /** @type {NerdamerSymbolType} */ (_.add(limit, __.Limit.limit(s, x, lim, depth))); + }, true); + + return limit; + } + symbol = /** @type {NerdamerSymbolType} */ ( + /** @type {SimplifySubModuleType} */ (core.Algebra.Simplify).simplify(symbol) + ); + + depth ||= 1; + + if (depth++ > Settings.max_lim_depth) { + return undefined; + } + + // Store the multiplier + const m = _.parse(symbol.multiplier); + // Strip the multiplier + symbol.toUnitMultiplier(); + // https://en.wikipedia.org/wiki/List_of_limits + let retval; + try { + // We try the simplest option first where c is some limit + // lim a as x->c = a where c + if (symbol.isConstant(true)) { + retval = symbol; + } else { + /** @type {Record} */ + const point = {}; + point[x] = lim; + // Lim x as x->c = c where c + + try { + // Evaluate the function at the given limit + const t = _.parse(symbol.sub(x, lim), point); + + // A constant or infinity is known so we're done + if (t.isConstant(true) || t.isInfinity) { + retval = t; + } + } catch (e) { + /* Nothing. Maybe we tried to divide by zero.*/ + if (e.message === 'timeout') { + throw e; + } + } + if (!retval) { + // Split the symbol in the numerator and the denominator + const num = symbol.getNum(); + const den = symbol.getDenom(); + + if (den.isConstant(true)) { + // We still don't have a limit so we generate tests. + if (symbol.group === EX) { + // https://en.wikipedia.org/wiki/List_of_limits + // Speed boost for exponentials by detecting patterns + const f = symbol.clone().toLinear(); + const _p = symbol.power.clone(); + const _num = f.getNum(); + const _den = f.getDenom(); + const fn = /** @type {DecomposeResultType} */ ( + core.Utils.decompose_fn(_den, x, true) + ); + // Start detection of pattern (x/(x+1))^x + if ( + _num.group === S && + _num.multiplier.isOne() && + fn.ax.group === S && + fn.b.isConstant(true) && + fn.a.isOne() && + fn.b.isConstant(true) + ) { + retval = /** @type {NerdamerSymbolType} */ ( + _.parse(format('(1/e^({0}))', fn.b)) + ); + } else { + const symbol_ = __.Limit.rewriteToLog(symbol.clone()); + // Get the base + const pow = symbol_.power.clone(); + const base = symbol_.clone().toLinear(); + const limBase = __.Limit.limit(base, x, lim, depth); + // Convert Frac to NerdamerSymbol if needed + const powSymbol = isSymbol(pow) ? pow : new NerdamerSymbol(pow); + const limPow = __.Limit.limit(powSymbol, x, lim, depth); + retval = _.pow(limBase, limPow); + } + } else if (symbol.group === FN && symbol.args.length === 1) { + let evaluates; + // Squeeze theorem lim f(g(x)) = lim f(lim g)) + const arg = __.Limit.limit(symbol.args[0], x, lim, depth); + if (core.Utils.isVector(arg)) { + // Get the limit over that interval + retval = arg.map(e => { + const clone = symbol.clone(); + clone.args[0] = e; + return /** @type {NerdamerSymbolType} */ ( + __.Limit.limit( + /** @type {NerdamerSymbolType} */ ( + _.symfunction(symbol.fname, [e]) + ), + x, + lim, + depth + ) + ); + }); + + return /** @type {NerdamerSymbolType} */ (_.multiply(m, retval)); + } + // If the argument is constant then we're done + let trial; + if (arg.isConstant(true)) { + // Double check that it evaluates + trial = _.symfunction(symbol.fname, [arg]); + // Trial evaluation + try { + evaluate(trial); + evaluates = true; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + + evaluates = false; + } + } + if (evaluates) { + retval = trial; + // If the limit converges. We'll deal with non-convergent ones later + } else if (__.Limit.isConvergent(arg)) { + if (symbol.fname === LOG) { + switch (arg.toString()) { + // Lim -> 0 + case '0': + retval = NerdamerSymbol.infinity().negate(); + break; + case 'Infinity': + retval = NerdamerSymbol.infinity(); + break; + case '-Infinity': + retval = NerdamerSymbol.infinity(); + break; + } + } else if ((symbol.fname === COS || symbol.fname === SIN) && lim.isInfinity) { + retval = __.Limit.interval(-1, 1); + } else if (symbol.fname === TAN) { + const sArg = symbol.args[0]; + const n = sArg.getNum(); + const d = sArg.getDenom(); + const pi = n.toUnitMultiplier(); + if (lim.isInfinity || (pi.equals('pi') && d.equals(2))) { + retval = __.Limit.diverges(); + } + } else if (symbol.fname === Settings.FACTORIAL) { + if (arg.isInfinity) { + return NerdamerSymbol.infinity(); + } + } + } + } else if (symbol.group === S) { + if (Number(symbol.power) > 0) // These functions always converge to the limit + { + return /** @type {NerdamerSymbolType} */ (_.parse(symbol, point)); + } + // We're dealing with 1/x^n but remember that infinity has already been dealt + // with by substitution + if (core.Utils.even(/** @type {FracType} */ (symbol.power))) { + // Even powers converge to infinity + retval = NerdamerSymbol.infinity(); + } else { + // Odd ones don't + retval = __.Limit.diverges(); + } + } else if (symbol.group === CB) { + let lim1; + let lim2; + // Loop through all the symbols + // thus => lim f*g*h = lim (f*g)*h = (lim f*g)*(lim h) + // symbols of lower groups are generally easier to differentiatee so get them to the right by first sorting + const symbols = /** @type {NerdamerSymbolType[]} */ (symbol.collectSymbols()).sort( + (a, b) => a.group - b.group + ); + + let f = symbols.pop(); + // Calculate the first limit so we can keep going down the list + lim1 = /** @type {NerdamerSymbolType} */ ( + evaluate(/** @type {NerdamerSymbolType} */ (__.Limit.limit(f, x, lim, depth))) + ); + + // Reduces all the limits one at a time + while (symbols.length) { + // Get the second limit + let g = symbols.pop(); + // Get the limit of g + lim2 = /** @type {NerdamerSymbolType} */ ( + evaluate( + /** @type {NerdamerSymbolType} */ (__.Limit.limit(g, x, lim, depth)) + ) + ); + + // If the limit is in indeterminate form aplly L'Hospital by inverting g and then f/(1/g) + if ( + lim1.isInfinity || + (!__.Limit.isConvergent(lim1) && lim2.equals(0)) || + (lim1.equals(0) && __.Limit.isConvergent(lim2)) + ) { + if (g.containsFunction(LOG)) { + // Swap them + g = [f, (f = g)][0]; + } + // Invert the symbol + g.invert(); + + // Product of infinities + if (lim1.isInfinity && lim2.isInfinity) { + lim1 = NerdamerSymbol.infinity(); + } else { + lim1 = /** @type {NerdamerSymbolType | undefined} */ ( + __.Limit.divide(f, g, x, lim, depth) + ); + } + } else { + // Lim f*g = (lim f)*(lim g) + lim1 = /** @type {NerdamerSymbolType} */ (_.multiply(lim1, lim2)); + // Let f*g equal f and h equal g + f = /** @type {NerdamerSymbolType} */ (_.multiply(f, g)); + } + } + + // Done, lim1 is the limit we're looking for + retval = lim1; + } else if (symbol.isComposite()) { + let _lim; + if (!symbol.isLinear()) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + // Apply lim f+g = (lim f)+(lim g) + retval = new NerdamerSymbol(0); + + let symbols = /** @type {NerdamerSymbolType[]} */ (symbol.collectSymbols()).sort( + (a, b) => b.group - a.group + ); + + const _symbols = []; + // Analyze the functions first + let fns = new NerdamerSymbol(0); + for (let i = 0, l = symbols.length; i < l; i++) { + const sym = symbols[i].clone(); + if (sym.group === FN || (sym.group === CB && sym.hasFunc(''))) { + fns = /** @type {NerdamerSymbolType} */ (_.add(fns, sym)); + } else { + _symbols.push(sym); + } + } + _symbols.unshift(/** @type {NerdamerSymbolType} */ (fns)); + + // Make sure that we didn't just repackage the exact same symbol + if (_symbols.length !== 1) { + symbols = _symbols; + } + + for (let i = 0, l = symbols.length; i < l; i++) { + const sym = symbols[i]; + // If the addition of the limits is undefined then the limit diverges so return -infinity to infinity + try { + _lim = __.Limit.limit(sym, x, lim, depth); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + _lim = __.Limit.diverges(); + } + + try { + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, _lim)); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + if (depth++ > Settings.max_lim_depth) { + return undefined; + } + retval = __.Limit.limit(__.diff(symbol, x), x, lim, depth); + } + } + } + } else { + retval = __.Limit.divide(num, den, x, lim, depth); + } + } + } + + // If we still don't have a solution, return it symbolically + retval ||= /** @type {NerdamerSymbolType} */ ( + _.symfunction('limit', [symbol, new NerdamerSymbol(x), lim]) + ); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // If all else fails return the symbolic function + retval = /** @type {NerdamerSymbolType} */ ( + _.symfunction('limit', [symbol, new NerdamerSymbol(x), lim]) + ); + } + + return /** @type {NerdamerSymbolType | VectorType} */ (_.multiply(m, retval)); + }, + }, + Fresnel: { + S(x) { + if (x.isConstant(true)) { + return __.defint(_.parse('sin(pi*x^2/2)'), new NerdamerSymbol(0), x, 'x'); + } + return _.symfunction('S', [x]); + }, + C(x) { + if (x.isConstant(true)) { + return __.defint(_.parse('cos(pi*x^2/2)'), new NerdamerSymbol(0), x, 'x'); + } + return _.symfunction('C', [x]); + }, + }, + }); + + nerdamer.register([ + { + name: 'diff', + visible: true, + numargs: [1, 3], + build() { + return __.diff; + }, + }, + { + name: 'sum', + visible: true, + numargs: 4, + build() { + return __.sum; + }, + }, + { + name: 'product', + visible: true, + numargs: 4, + build() { + return __.product; + }, + }, + { + name: 'integrate', + visible: true, + numargs: [1, 2], + build() { + return __.integrate; + }, + }, + { + name: 'defint', + visible: true, + numargs: [3, 4], + build() { + return __.defint; + }, + }, + { + name: 'S', + visible: true, + numargs: 1, + build() { + return __.Fresnel.S; + }, + }, + { + name: 'C', + visible: true, + numargs: 1, + build() { + return __.Fresnel.C; + }, + }, + { + name: 'limit', + visible: true, + numargs: [3, 4], + build() { + return __.Limit.limit; + }, + }, + ]); + // Link registered functions externally + nerdamer.updateAPI(); +})(); diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/Extra.js b/tools/ui/src/lib/vendors/nerdamer-prime/Extra.js new file mode 100644 index 000000000..8984cdd46 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/Extra.js @@ -0,0 +1,926 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * License : MIT + * Source : https://github.com/jiggzson/nerdamer + */ + +// Type imports for JSDoc ====================================================== +// These typedefs provide type aliases for the interfaces defined in index.d.ts. +// They enable proper type checking when working with the classes defined in this file. +// +// Usage patterns: +// - For return types: @returns {NerdamerSymbolType} +// - For parameters: @param {NerdamerSymbolType} symbol +// - For variable declarations: /** @type {NerdamerSymbolType} */ +// +// Note: When casting local class instances to interface types, use the pattern: +// /** @type {InterfaceType} */ (/** @type {unknown} */ (localInstance)) +// This is needed because TypeScript sees local classes and interfaces as separate types. + +/** + * Core type aliases from index.d.ts + * + * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType + * + * @typedef {import('./index').NerdamerCore.Frac} FracType + * + * @typedef {import('./index').NerdamerCore.Vector} VectorType + * + * @typedef {import('./index').NerdamerCore.Matrix} MatrixType + * + * @typedef {import('./index').NerdamerCore.Parser} ParserType + * + * @typedef {import('./index').NerdamerCore.Settings} SettingsType + * + * @typedef {import('./index').NerdamerExpression} ExpressionType + * + * @typedef {typeof import('./index')} NerdamerType + * + * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface + * + * @typedef {import('./index').NerdamerCore.Math2} Math2Interface + * + * @typedef {import('./index').NerdamerCore.Core} CoreType + * + * @typedef {import('./index').ExpressionParam} ExpressionParam + * + * @typedef {import('./index').ArithmeticOperand} ArithmeticOperand + * + * @typedef {import('./index').NerdamerCore.AlgebraModule} AlgebraModuleType + * + * @typedef {import('./index').NerdamerCore.PartFracSubModule} PartFracSubModuleType + * + * @typedef {import('./index').NerdamerCore.CalculusModule} CalculusModuleType + * + * @typedef {import('./index').NerdamerCore.ExtraModule} ExtraModuleType + * + * @typedef {import('./index').NerdamerCore.LaPlaceSubModule} LaPlaceSubModuleType + * + * @typedef {import('./index').NerdamerCore.StatisticsSubModule} StatisticsSubModuleType + * + * @typedef {import('./index').NerdamerCore.UnitsSubModule} UnitsSubModuleType + * + * @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType + */ + +// Check if nerdamer exists globally (browser) or needs to be required (Node.js) +let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined; +if (typeof module !== 'undefined' && nerdamer === undefined) { + nerdamer = require('./nerdamer.core.js'); + require('./Calculus'); + require('./Algebra'); +} + +/** @returns {ExtraModuleType} */ +(function initExtraModule() { + /** @type {CoreType} */ + const core = nerdamer.getCore(); + /** @type {ParserType} */ + const _ = core.PARSER; + const { + NerdamerSymbol, + Vector: _Vector, + /** @type {AlgebraModuleType} */ + Algebra, + /** @type {CalculusModuleType} */ + Calculus, + } = core; + const { format, isVector, isArray, isSymbol } = core.Utils; + const { S, EX: _EX, CP, PL, CB, FN } = core.groups; + core.Settings.Laplace_integration_depth = 40; + + /** + * Check if a symbol's power is itself a symbol with group S or CB + * + * @param {NerdamerSymbolType} sym + * @returns {boolean} + */ + function hasPowerGroupSOrCB(sym) { + return isSymbol(sym.power) && (sym.power.group === S || sym.power.group === CB); + } + + /** + * Finds a function by name within this symbol's tree. + * + * @this {NerdamerSymbolType} + * @param {string} fname The function name to search for + * @returns {NerdamerSymbolType | undefined} The found function symbol clone, or undefined if not found + */ + NerdamerSymbol.prototype.findFunction = function findFunction(fname) { + // This is what we're looking for + if (this.group === FN && this.fname === fname) { + return this.clone(); + } + let found; + if (this.symbols) { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + found = this.symbols[x].findFunction(fname); + if (found) { + break; + } + } + } + + return found; + }; + + /** @type {ExtraModuleType} */ + const __ = (core.Extra = { + version: '1.4.2', + // http://integral-table.com/downloads/LaplaceTable.pdf + // Laplace assumes all coefficients to be positive + LaPlace: { + // Using: integral_0^oo f(t)*e^(-s*t) dt + /** + * @param {NerdamerSymbolType} symbol + * @param {NerdamerSymbolType | string} t + * @param {NerdamerSymbolType | string} s + * @returns {NerdamerSymbolType} + */ + transform(symbol, t, s) { + /** @type {NerdamerSymbolType} */ + symbol = symbol.clone(); + + t = t.toString(); + // First try a lookup for a speed boost + symbol = NerdamerSymbol.unwrapSQRT(symbol, true); + /** @type {NerdamerSymbolType} */ + let retval; + const coeff = symbol.stripVar(t); + const g = symbol.group; + + symbol = /** @type {NerdamerSymbolType} */ (_.divide(symbol, coeff.clone())); + + if (symbol.isConstant() || !symbol.contains(t, true)) { + retval = _.parse(format('({0})/({1})', symbol, s)); + } else if (g === S && core.Utils.isInt(symbol.power)) { + const n = String(symbol.power); + retval = _.parse(format('factorial({0})/({1})^({0}+1)', n, s)); + } else if (symbol.group === S && symbol.power.equals(1 / 2)) { + retval = _.parse(format('sqrt(pi)/(2*({0})^(3/2))', s)); + } else if (symbol.isComposite()) { + retval = new NerdamerSymbol(0); + symbol.each(x => { + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, __.LaPlace.transform(x, t, s))); + }, true); + } else if (symbol.isE() && hasPowerGroupSOrCB(symbol)) { + const a = /** @type {NerdamerSymbolType} */ (symbol.power).stripVar(t); + retval = _.parse(format('1/(({1})-({0}))', a, s)); + } else { + const fns = ['sin', 'cos', 'sinh', 'cosh']; + // Support for symbols in fns with arguments in the form a*t or n*t where a = symbolic and n = Number + if ( + symbol.group === FN && + fns.indexOf(symbol.fname) !== -1 && + (symbol.args[0].group === S || symbol.args[0].group === CB) + ) { + const a = symbol.args[0].stripVar(t); + + switch (symbol.fname) { + case 'sin': + retval = _.parse(format('({0})/(({1})^2+({0})^2)', a, s)); + break; + case 'cos': + retval = _.parse(format('({1})/(({1})^2+({0})^2)', a, s)); + break; + case 'sinh': + retval = _.parse(format('({0})/(({1})^2-({0})^2)', a, s)); + break; + case 'cosh': + retval = _.parse(format('({1})/(({1})^2-({0})^2)', a, s)); + break; + } + } else { + // Try to integrate for a solution + // we need at least the Laplace integration depth + const depthIsLower = core.Settings.integration_depth < core.Settings.Laplace_integration_depth; + + let savedIntegrationDepth; + if (depthIsLower) { + savedIntegrationDepth = core.Settings.integration_depth; // Save the depth + core.Settings.integration_depth = core.Settings.Laplace_integration_depth; // Transforms need a little more room + } + + core.Utils.block( + 'PARSE2NUMBER', + () => { + const u = t; + const sym = symbol.sub(t, u); + const integrationExpr = _.parse(`e^(-${s}*${u})*${sym}`); + retval = Calculus.integrate(integrationExpr, u); + if (retval.hasIntegral?.()) { + retval = _.symfunction('laplace', [symbol, _.parse(String(t)), _.parse(String(s))]); + return; + } + // _.error('Unable to compute transform'); + retval = retval.sub(t, 0); + retval = /** @type {NerdamerSymbolType} */ ( + _.expand(_.multiply(retval, new NerdamerSymbol(-1))) + ); + retval = retval.sub(u, t); + }, + false + ); + + retval = /** @type {NerdamerSymbolType} */ ( + core.Utils.block('PARSE2NUMBER', () => _.parse(retval), true) + ); + + if (depthIsLower) // Put the integration depth as it was + { + core.Settings.integration_depth = savedIntegrationDepth; + } + } + } + + return /** @type {NerdamerSymbolType} */ (_.multiply(retval, coeff)); + }, + /** + * @param {NerdamerSymbolType} symbol + * @param {NerdamerSymbolType | string} s_ + * @param {NerdamerSymbolType | string} t + * @returns {NerdamerSymbolType} + */ + inverse(symbol, s_, t) { + const inputSymbol = symbol.clone(); + return core.Utils.block( + 'POSITIVE_MULTIPLIERS', + () => { + /** @type {NerdamerSymbolType | undefined} */ + let retval; + // Expand and get partial fractions + if (symbol.group === CB) { + symbol = /** @type {NerdamerSymbolType} */ ( + /** @type {PartFracSubModuleType} */ (Algebra.PartFrac).partfrac( + /** @type {NerdamerSymbolType} */ (_.expand(symbol)), + s_ + ) + ); + } + + if (symbol.group === S || symbol.group === CB || symbol.isComposite()) { + /** @type {number | FracType} */ + let p; + /** @type {FracType} */ + let denP; + /** @type {NerdamerSymbolType} */ + let a; + /** @type {NerdamerSymbolType | string} */ + let b; + /** @type {NerdamerSymbolType} */ + let d; + /** @type {string} */ + let exp; + /** @type {DecomposeResultType} */ + let f2; + /** @type {string | number} */ + let fact; + // Remove the multiplier + const m = symbol.multiplier.clone(); + symbol.toUnitMultiplier(); + // Get the numerator and denominator + let num = symbol.getNum(); + const den = symbol.getDenom().toUnitMultiplier(); + + // TODO: Make it so factor doesn't destroy pi + // num = core.Algebra.Factor.factor(symbol.getNum()); + // den = core.Algebra.Factor.factor(symbol.getDenom().invert(null, true)); + + if (den.group === CP || den.group === PL) { + denP = /** @type {FracType} */ (den.power.clone()); + den.toLinear(); + } else { + denP = new core.Frac(1); + } + + // Convert s to a string + const s = s_.toString(); + // Split up the denominator if in the form ax+b + /** @type {DecomposeResultType} */ + const f = core.Utils.decompose_fn(den, s, true); + // Move the multiplier to the numerator + /** @type {DecomposeResultType} */ + const _fe = core.Utils.decompose_fn( + /** @type {NerdamerSymbolType} */ (_.expand(num.clone())), + s, + true + ); + num.multiplier = num.multiplier.multiply(m); + + const finalize = function () { + // Put back the numerator + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, num)); + retval.multiplier = retval.multiplier.multiply(symbol.multiplier); + // Put back a + retval = /** @type {NerdamerSymbolType} */ (_.divide(retval, f.a)); + }; + + // Store the parts in variables for easy recognition + // check if in the form t^n where n = integer + if ( + (den.group === S || den.group === CB) && + f.x.value === s && + f.b.equals(0) && + core.Utils.isInt(f.x.power) + ) { + p = /** @type {number} */ (/** @type {unknown} */ (f.x.power)) - 1; + fact = core.Math2.factorial(p); + // N!/s^(n-1) + retval = /** @type {NerdamerSymbolType} */ ( + _.divide(_.pow(_.parse(String(t)), new NerdamerSymbol(p)), new NerdamerSymbol(fact)) + ); + // Wrap it up + finalize(); + } else if (den.group === CP && denP.equals(1)) { + if (f.x.group === core.groups.PL && Algebra.degree(den).equals(2)) { + // Possibly in the form 1/(s^2+2*s+1) + // Try factoring to get it in a more familiar form{ + // Apply inverse of F(s-a) + /** + * @type {{ + * f: NerdamerSymbolType; + * a: NerdamerSymbolType; + * h: NerdamerSymbolType; + * c?: NerdamerSymbolType; + * }} + */ + const completed = Algebra.sqComplete(den, s); + const u = core.Utils.getU(den); + // Get a for the function above + a = core.Utils.decompose_fn(completed.a, s, true).b; + const tf = __.LaPlace.inverse( + _.parse(`1/((${u})^2+(${completed.c}))`), + u, + String(t) + ); + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(tf, _.parse(`(${m})*e^(-(${a})*(${t}))`)) + ); + // A/(b*s-c) -> ae^(-bt) + } else if (f.x.isLinear() && !num.contains(s)) { + t = /** @type {NerdamerSymbolType | string} */ ( + _.divide(_.parse(String(t)), f.a.clone()) + ); + + // Don't add factorial of one or zero + p = /** @type {number} */ (/** @type {unknown} */ (denP)) - 1; + fact = p === 0 || p === 1 ? '1' : `(${denP}-1)!`; + retval = _.parse( + format( + '(({0})^({3}-1)*e^(-(({2})*({0}))/({1})))/(({4})*({1})^({3}))', + t, + f.a, + f.b, + denP, + fact + ) + ); + // Wrap it up + finalize(); + } else if (f.x.group === S && f.x.power.equals(2)) { + if (num.contains(s)) { + // A*s/(b*s^2+c^2) + a = new NerdamerSymbol(1); + if (num.group === CB) { + /** @type {NerdamerSymbolType} */ + let newNum = new NerdamerSymbol(1); + num.each(x => { + if (x.contains(s)) { + newNum = /** @type {NerdamerSymbolType} */ (_.multiply(newNum, x)); + } else { + a = /** @type {NerdamerSymbolType} */ (_.multiply(a, x)); + } + }); + num = newNum; + } + + // We need more information about the denominator to decide + f2 = core.Utils.decompose_fn(num, s, true); + const fn1 = f2.a; + const fn2 = f2.b; + const aHasSin = fn1.containsFunction('sin'); + const aHasCos = fn1.containsFunction('cos'); + const bHasCos = fn2.containsFunction('cos'); + const bHasSin = fn2.containsFunction('sin'); + if ( + f2.x.value === s && + f2.x.isLinear() && + !((aHasSin && bHasCos) || aHasCos || bHasSin) + ) { + retval = _.parse( + format( + '(({1})*cos((sqrt(({2})*({3}))*({0}))/({2})))/({2})', + t, + f2.a, + f.a, + f.b + ) + ); + } else if (aHasSin && bHasCos) { + const sin = /** @type {NerdamerSymbolType} */ (fn1.findFunction?.('sin')); + const cos = /** @type {NerdamerSymbolType} */ (fn2.findFunction?.('cos')); + // Who has the s? + if (sin?.args?.[0].equals(cos?.args?.[0]) && !sin?.args?.[0].contains(s)) { + b = /** @type {NerdamerSymbolType} */ ( + _.divide(fn2, cos.toUnitMultiplier()) + ).toString(); + const c = sin.args[0].toString(); + d = f.b; + const e = _.divide(fn1, sin.toUnitMultiplier()); + exp = + '(({1})*({2})*cos({3})*sin(sqrt({4})*({0})))/sqrt({4})+({1})*sin({3})*({5})*cos(sqrt({4})*({0}))'; + retval = _.parse(format(exp, t, a, b, c, d, e)); + } + } + } else { + retval = _.parse( + format( + '(({1})*sin((sqrt(({2})*({3}))*({0}))/({2})))/sqrt(({2})*({3}))', + t, + num, + f.a, + f.b + ) + ); + } + } + } else if ( + /** @type {FracType} */ (f.x.power).num && + /** @type {FracType} */ (f.x.power).num.equals(3) && + /** @type {FracType} */ (f.x.power).den.equals(2) && + num.contains('sqrt(pi)') && + !num.contains(s) && + num.isLinear() + ) { + b = /** @type {NerdamerSymbolType} */ (_.divide(num.clone(), _.parse('sqrt(pi)'))); + retval = _.parse(format('(2*({2})*sqrt({0}))/({1})', t, f.a, b, num)); + } else if (denP.equals(2) && f.x.power.equals(2)) { + if (num.contains(s)) { + // Decompose the numerator to check value of s + f2 = core.Utils.decompose_fn( + /** @type {NerdamerSymbolType} */ (_.expand(num.clone())), + s, + true + ); + if (f2.x.isComposite()) { + /** @type {DecomposeResultType[]} */ + const sTerms = []; + // First collect the factors e.g. (a)(bx)(cx^2+d) + /** @type {DecomposeResultType[]} */ + const symbols = /** @type {DecomposeResultType[]} */ ( + num + .collectSymbols(x => { + x = NerdamerSymbol.unwrapPARENS(x); + /** @type {DecomposeResultType} */ + const decomp = core.Utils.decompose_fn(x, s, true); + decomp.symbol = x; + return decomp; + }) + // Then sort them by power hightest to lowest + .sort((x1, x2) => { + const p1 = + /** @type {DecomposeResultType} */ (x1).x.value === s + ? /** @type {number} */ ( + /** @type {unknown} */ ( + /** @type {DecomposeResultType} */ (x1).x.power + ) + ) + : 0; + const p2 = + /** @type {DecomposeResultType} */ (x2).x.value === s + ? /** @type {number} */ ( + /** @type {unknown} */ ( + /** @type {DecomposeResultType} */ (x2).x.power + ) + ) + : 0; + return p2 - p1; + }) + ); + a = new NerdamerSymbol(-1); + // Grab only the ones which have s + for (let i = 0; i < symbols.length; i++) { + const fc = symbols[i]; + if (fc.x.value === s) { + sTerms.push(fc); + } else { + a = /** @type {NerdamerSymbolType} */ (_.multiply(a, fc.symbol)); + } + } + // The following 2 assumptions are made + // 1. since the numerator was factored above then each s_term has a unique power + // 2. because the terms are sorted by descending powers then the first item + // has the highest power + // We can now check for the next type s(s^2-a^2)/(s^2+a^2)^2 + if ( + sTerms[0].x.power.equals(2) && + sTerms[1].x.power.equals(1) && + sTerms[1].b.equals(0) && + !sTerms[0].b.equals(0) + ) { + b = sTerms[0].a.negate(); + exp = + '-(({1})*({2})*({5})*({0})*sin((sqrt(({4})*({5}))*({0}))/({4})))/' + + '(2*({4})^2*sqrt(({4})*({5})))-(({1})*({3})*({0})*sin((sqrt(({4})*({5}))*({0}))/({4})))' + + '/(2*({4})*sqrt(({4})*({5})))+(({1})*({2})*cos((sqrt(({4})*({5}))*({0}))/({4})))/({4})^2'; + retval = _.parse(format(exp, t, a, b, sTerms[0].b, f.a, f.b)); + } + } else if (f2.x.isLinear()) { + a = /** @type {NerdamerSymbolType} */ (_.divide(f2.a, new NerdamerSymbol(2))); + exp = + '(({1})*({0})*sin((sqrt(({2})*({3}))*({0}))/({2})))/(({2})*sqrt(({2})*({3})))'; + retval = _.parse(format(exp, t, a, f.a, f.b)); + } else if (f2.x.power.equals(2)) { + if (f2.b.equals(0)) { + a = /** @type {NerdamerSymbolType} */ ( + _.divide(f2.a, new NerdamerSymbol(2)) + ); + exp = + '(({1})*sin((sqrt(({2})*({3}))*({0}))/({2})))/(({2})*sqrt(({2})*({3})))+(({1})*({0})*cos((sqrt(({2})*({3}))*({0}))/({2})))/({2})^2'; + retval = _.parse(format(exp, t, a, f.a, f.b)); + } else { + a = /** @type {NerdamerSymbolType} */ ( + _.divide(f2.a, new NerdamerSymbol(2)) + ); + d = f2.b.negate(); + exp = + '-((({2})*({4})-2*({1})*({3}))*sin((sqrt(({2})*({3}))*({0}))/({2})))/(2*({2})*({3})*sqrt(({2})*({3})))+' + + '(({4})*({0})*cos((sqrt(({2})*({3}))*({0}))/({2})))/(2*({2})*({3}))+(({1})*({0})*cos((sqrt(({2})*({3}))*({0}))/({2})))/({2})^2'; + retval = _.parse(format(exp, t, a, f.a, f.b, d)); + } + } + } else { + a = /** @type {NerdamerSymbolType} */ (_.divide(num, new NerdamerSymbol(2))); + exp = + '(({1})*sin((sqrt(({2})*({3}))*({0}))/({2})))/(({3})*sqrt(({2})*({3})))-(({1})*({0})*cos((sqrt(({2})*({3}))*({0}))/({2})))/(({2})*({3}))'; + retval = _.parse(format(exp, t, a, f.a, f.b)); + } + } else if (symbol.isComposite()) { + // 1/(s+1)^2 + if (denP.equals(2) && f.x.group === S) { + retval = _.parse(`(${m})*(${t})*e^(-(${f.b})*(${t}))`); + } else { + retval = new NerdamerSymbol(0); + + symbol = /** @type {NerdamerSymbolType} */ ( + /** @type {PartFracSubModuleType} */ (Algebra.PartFrac).partfrac( + /** @type {NerdamerSymbolType} */ (_.expand(symbol)), + s_ + ) + ); + + symbol.each(x => { + retval = /** @type {NerdamerSymbolType} */ ( + _.add(retval, __.LaPlace.inverse(x, s_, t)) + ); + }, true); + } + } + } + + retval ||= _.symfunction('ilt', [inputSymbol, _.parse(String(s_)), _.parse(String(t))]); + + return /** @type {NerdamerSymbolType} */ (retval); + }, + true + ); + }, + }, + Statistics: { + /** + * @param {NerdamerSymbolType[]} arr + * @returns {Record} + */ + frequencyMap(arr) { + /** @type {Record} */ + const map = {}; + // Get the frequency map + for (let i = 0, l = arr.length; i < l; i++) { + const e = arr[i]; + const key = e.toString(); + map[key] ||= 0; // Default it to zero + map[key]++; // Increment + } + return map; + }, + /** + * @param {NerdamerSymbolType[]} arr + * @returns {NerdamerSymbolType[]} + */ + sort(arr) { + return arr.sort((a, b) => { + if (!a.isConstant() || !b.isConstant()) { + _.error('Unable to sort! All values must be numeric'); + } + return /** @type {number} */ (/** @type {unknown} */ (a.multiplier.subtract(b.multiplier))); + }); + }, + /** + * @param {NerdamerSymbolType[]} arr + * @returns {NerdamerSymbolType} + */ + count(arr) { + return new NerdamerSymbol(arr.length); + }, + /** + * @param {NerdamerSymbolType[]} arr + * @param {NerdamerSymbolType} [x_] + * @returns {NerdamerSymbolType} + */ + sum(arr, x_) { + /** @type {NerdamerSymbolType} */ + let sum = new NerdamerSymbol(0); + for (let i = 0, l = arr.length; i < l; i++) { + const xi = arr[i].clone(); + if (x_) { + sum = /** @type {NerdamerSymbolType} */ ( + _.add(_.pow(_.subtract(xi, x_.clone()), new NerdamerSymbol(2)), sum) + ); + } else { + sum = /** @type {NerdamerSymbolType} */ (_.add(xi, sum)); + } + } + + return sum; + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + mean(...args) { + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.mean(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + return /** @type {NerdamerSymbolType} */ (_.divide(__.Statistics.sum(args), __.Statistics.count(args))); + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + median(...args) { + /** @type {NerdamerSymbolType} */ + let retval; + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.median(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + try { + const sorted = __.Statistics.sort(args); + const l = args.length; + if (core.Utils.even(l)) { + const mid = l / 2; + retval = __.Statistics.mean(sorted[mid - 1], sorted[mid]); + } else { + retval = sorted[Math.floor(l / 2)]; + } + } catch (e) { + if (/** @type {Error} */ (e).message === 'timeout') { + throw e; + } + retval = _.symfunction('median', args); + } + return retval; + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + mode(...args) { + /** @type {NerdamerSymbolType} */ + let retval; + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.mode(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + + const map = __.Statistics.frequencyMap(args); + + // The mode of 1 item is that item as per issue #310 (verified by Happypig375). + if (core.Utils.keys(map).length === 1) { + retval = args[0]; + } else { + // Invert by arraning them according to their frequency + /** @type {Record} */ + const inverse = {}; + for (const x in map) { + if (!Object.hasOwn(map, x)) { + continue; + } + const freq = map[x]; + // Check if it's in the inverse already + if (freq in inverse) { + const e = inverse[freq]; + // If it's already an array then just add it + if (isArray(e)) { + e.push(x); + } + // Convert it to and array + else { + inverse[freq] = [x, /** @type {string} */ (inverse[freq])]; + } + } else { + inverse[freq] = x; + } + } + // The keys now represent the maxes. We want the max of those keys + const keyNums = core.Utils.keys(inverse).map(k => Number(k)); + const maxKey = Math.max.apply(null, keyNums); + const max = inverse[maxKey]; + // Check it's an array. If it is then map over the results and convert + // them to NerdamerSymbol + if (isArray(max)) { + retval = _.symfunction( + 'mode', + max.sort().map(v => _.parse(v)) + ); + } else { + retval = _.parse(/** @type {string} */ (max)); + } + } + + return retval; + }, + /** + * @param {NerdamerSymbolType} k + * @param {NerdamerSymbolType[]} args + * @returns {NerdamerSymbolType} + */ + gVariance(k, args) { + const x_ = __.Statistics.mean(...args); + const sum = __.Statistics.sum(args, x_); + return /** @type {NerdamerSymbolType} */ (_.multiply(k, sum)); + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + variance(...args) { + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.variance(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + const k = /** @type {NerdamerSymbolType} */ ( + _.divide(new NerdamerSymbol(1), __.Statistics.count(args)) + ); + return __.Statistics.gVariance(k, args); + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + sampleVariance(...args) { + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.sampleVariance(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + + const k = /** @type {NerdamerSymbolType} */ ( + _.divide(new NerdamerSymbol(1), _.subtract(__.Statistics.count(args), new NerdamerSymbol(1))) + ); + return __.Statistics.gVariance(k, args); + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + standardDeviation(...args) { + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.standardDeviation(.../** @type {NerdamerSymbolType[]} */ (args[0].elements)); + } + return /** @type {NerdamerSymbolType} */ ( + _.pow(__.Statistics.variance(...args), new NerdamerSymbol(1 / 2)) + ); + }, + /** + * @param {...NerdamerSymbolType} args + * @returns {NerdamerSymbolType} + */ + sampleStandardDeviation(...args) { + // Handle arrays + if (isVector(args[0])) { + return __.Statistics.sampleStandardDeviation( + .../** @type {NerdamerSymbolType[]} */ (args[0].elements) + ); + } + return /** @type {NerdamerSymbolType} */ ( + _.pow(__.Statistics.sampleVariance(...args), new NerdamerSymbol(1 / 2)) + ); + }, + /** + * @param {NerdamerSymbolType} x + * @param {NerdamerSymbolType} mean + * @param {NerdamerSymbolType} stdev + * @returns {NerdamerSymbolType} + */ + zScore(x, mean, stdev) { + return /** @type {NerdamerSymbolType} */ (_.divide(_.subtract(x, mean), stdev)); + }, + }, + Units: { + table: { + foot: '12 inch', + meter: '100 cm', + decimeter: '10 cm', + }, + }, + }); + + nerdamer.register([ + { + name: 'laplace', + visible: true, + numargs: 3, + build() { + return __.LaPlace.transform; + }, + }, + { + name: 'ilt', + visible: true, + numargs: 3, + build() { + return __.LaPlace.inverse; + }, + }, + // Statistical + { + name: 'mean', + visible: true, + numargs: -1, + build() { + return __.Statistics.mean; + }, + }, + { + name: 'median', + visible: true, + numargs: -1, + build() { + return __.Statistics.median; + }, + }, + { + name: 'mode', + visible: true, + numargs: -1, + build() { + return __.Statistics.mode; + }, + }, + { + name: 'smpvar', + visible: true, + numargs: -1, + build() { + return __.Statistics.sampleVariance; + }, + }, + { + name: 'variance', + visible: true, + numargs: -1, + build() { + return __.Statistics.variance; + }, + }, + { + name: 'smpstdev', + visible: true, + numargs: -1, + build() { + return __.Statistics.sampleStandardDeviation; + }, + }, + { + name: 'stdev', + visible: true, + numargs: -1, + build() { + return __.Statistics.standardDeviation; + }, + }, + { + name: 'zscore', + visible: true, + numargs: 3, + build() { + return __.Statistics.zScore; + }, + }, + ]); + + // Link registered functions externally + nerdamer.updateAPI(); +})(); + +// Added for all.min.js +if (typeof module !== 'undefined') { + module.exports = nerdamer; +} diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/LICENSE b/tools/ui/src/lib/vendors/nerdamer-prime/LICENSE new file mode 100644 index 000000000..1a62b421b --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 together-science + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/Solve.js b/tools/ui/src/lib/vendors/nerdamer-prime/Solve.js new file mode 100644 index 000000000..7f7e424fa --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/Solve.js @@ -0,0 +1,2370 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * Source : https://github.com/jiggzson/nerdamer + */ + +// Type imports for JSDoc ====================================================== +// These typedefs provide type aliases for the interfaces defined in index.d.ts. +// They enable proper type checking when working with the classes defined in this file. +// +// Usage patterns: +// - For return types: @returns {NerdamerSymbolType} +// - For parameters: @param {NerdamerSymbolType} symbol +// - For variable declarations: /** @type {NerdamerSymbolType} */ + +/** + * Core type aliases from index.d.ts + * + * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType + * + * @typedef {import('./index').NerdamerCore.Frac} FracType + * + * @typedef {import('./index').NerdamerCore.Vector} VectorType + * + * @typedef {import('./index').NerdamerCore.Matrix} MatrixType + * + * @typedef {import('./index').NerdamerCore.Parser} ParserType + * + * @typedef {import('./index').NerdamerCore.Settings} SettingsType + * + * @typedef {import('./index').NerdamerExpression} ExpressionType + * + * @typedef {typeof import('./index')} NerdamerType + * + * Constructor types (for factory functions) + * + * @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor + * + * @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor + * + * Module types + * + * @typedef {import('./index').NerdamerCore.AlgebraModule} AlgebraModuleType + * + * @typedef {import('./index').NerdamerCore.CalculusModule} CalculusModuleType + * + * @typedef {import('./index').NerdamerCore.FactorSubModule} FactorSubModuleType + * + * @typedef {import('./index').NerdamerCore.SimplifySubModule} SimplifySubModuleType + * + * @typedef {import('./index').NerdamerCore.IntegrationSubModule} IntegrationSubModuleType + * + * @typedef {import('./index').NerdamerCore.AlgebraClassesSubModule} AlgebraClassesSubModuleType + * + * @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType + * + * @typedef {import('./index').NerdamerCore.SolveModule} SolveModuleType + * + * Utility types + * + * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface + * + * @typedef {import('./index').NerdamerCore.Build} BuildInterface + * + * @typedef {import('big-integer').BigInteger} BigIntegerType + * + * Equation instance type + * + * @typedef {import('./index').NerdamerCore.EquationInstance} EquationInstanceType + * + * Solution result types + * + * @typedef {import('./index').NerdamerCore.SystemSolutionResult} SystemSolutionResultType + * + * @typedef {import('./index').NerdamerCore.SystemSolutionValue} SystemSolutionValueType + * + * @typedef {import('./index').NerdamerCore.CircleSolutionResult} CircleSolutionResultType + * + * @typedef {(NerdamerSymbolType | EquationInstanceType | string)[]} SolveEquationArray + */ + +// Check if nerdamer exists globally (browser) or needs to be required (Node.js) +let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined; +if (typeof module !== 'undefined' && nerdamer === undefined) { + nerdamer = require('./nerdamer.core.js'); + require('./Calculus.js'); + require('./Algebra.js'); +} + +/** @returns {SolveModuleType} */ +(function initSolveModule() { + // Handle imports + const core = nerdamer.getCore(); + const _ = core.PARSER; + /** @type {AlgebraModuleType} */ + const _A = /** @type {AlgebraModuleType} */ (core.Algebra); + /** @type {CalculusModuleType} */ + const _C = /** @type {CalculusModuleType} */ (core.Calculus); + const { integration } = /** @type {{ integration: IntegrationSubModuleType }} */ (_C); + const { decompose_arg: explode } = integration; + const { + Factor, + Simplify, + Classes: AlgebraClasses, + } = /** @type {{ Factor: FactorSubModuleType; Simplify: SimplifySubModuleType; Classes: AlgebraClassesSubModuleType }} */ ( + _A + ); + const { evaluate, remove, format, knownVariable, isSymbol, variables, range } = core.Utils; + const { build } = core.Build; + const { NerdamerSymbol } = core; + const { S, PL, CB, CP, FN } = core.groups; + const { Settings } = core; + const { isArray } = core.Utils; + + // The search radius for the roots + core.Settings.SOLVE_RADIUS = 1000; + // The maximum number to fish for on each side of the zero + core.Settings.ROOTS_PER_SIDE = 10; + // Covert the number to multiples of pi if possible + core.Settings.make_pi_conversions = false; + // The step size + core.Settings.STEP_SIZE = 0.1; + + // The epsilon size + core.Settings.EPSILON = 2e-13; + // The maximum iterations for Newton's method + core.Settings.MAX_NEWTON_ITERATIONS = 200; + // The epsilon used in Newton's iteration + // core.Settings.NEWTON_EPSILON = Number.EPSILON * 2; + core.Settings.NEWTON_EPSILON = 2e-15; + + // The maximum number of time non-linear solve tries another jump point + core.Settings.MAX_NON_LINEAR_TRIES = 12; + // The amount of iterations the function will start to jump at + core.Settings.NON_LINEAR_JUMP_AT = 50; + // The size of the jump + core.Settings.NON_LINEAR_JUMP_SIZE = 100; + // The original starting point for nonlinear solving + core.Settings.NON_LINEAR_START = 0.01; + // When points are generated as starting points for Newton's method, they are sliced into small + // slices to make sure that we have convergence on the right point. This defines the + // size of the slice + core.Settings.NEWTON_SLICES = 200; + // The distance in which two solutions are deemed the same + core.Settings.SOLUTION_PROXIMITY = 1e-14; + // Indicate wheter to filter the solutions are not + core.Settings.FILTER_SOLUTIONS = true; + // The maximum number of recursive calls + core.Settings.MAX_SOLVE_DEPTH = 10; + // The tolerance that's considered close enough to zero + core.Settings.ZERO_EPSILON = 1e-9; + // The maximum iteration for the bisection method incase of some JS strangeness + core.Settings.MAX_BISECTION_ITER = 2000; + // The tolerance for the bisection method + core.Settings.BI_SECTION_EPSILON = 1e-12; + + core.NerdamerSymbol.prototype.hasTrig = function hasTrig() { + return this.containsFunction(['cos', 'sin', 'tan', 'cot', 'csc', 'sec']); + }; + + core.NerdamerSymbol.prototype.hasNegativeTerms = function hasNegativeTerms() { + if (this.isComposite()) { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const sym = this.symbols[x]; + if ((sym.group === PL && sym.hasNegativeTerms()) || this.symbols[x].power.lessThan(0)) { + return true; + } + } + } + return false; + }; + + /* Nerdamer version 0.7.x and up allows us to make better use of operator overloading + * As such we can have this data type be supported completely outside of the core. + * This is an equation that has a left hand side and a right hand side + */ + /** + * Equation class representing LHS = RHS. + * + * @class + * @param {NerdamerSymbolType} lhs - The left hand side symbol + * @param {NerdamerSymbolType} rhs - The right hand side symbol + */ + function Equation(lhs, rhs) { + if ( + (rhs.isConstant() && lhs.isConstant() && !lhs.equals(rhs)) || + (lhs.equals(core.Settings.IMAGINARY) && rhs.isConstant(true)) || + (rhs.equals(core.Settings.IMAGINARY) && lhs.isConstant(true)) + ) { + throw new core.exceptions.NerdamerValueError(`${lhs.toString()} does not equal ${rhs.toString()}`); + } + /** @type {NerdamerSymbolType} */ + this.LHS = lhs; // Left hand side + /** @type {NerdamerSymbolType} */ + this.RHS = rhs; // Right and side + } + // UTILS ##!! + + Equation.prototype = { + toString() { + return `${this.LHS.toString()}=${this.RHS.toString()}`; + }, + text(option) { + return `${this.LHS.text(option)}=${this.RHS.text(option)}`; + }, + /** + * Brings the equation to LHS (sets RHS to zero). + * + * @param {boolean} [expand] - Whether to expand the result + * @returns {NerdamerSymbolType} The LHS with RHS subtracted + */ + toLHS(expand) { + expand = !!expand; + const eqn = this.removeDenom(); + let a = eqn.LHS; + let b = eqn.RHS; + + if (a.isConstant(true) && !b.isConstant(true)) { + // Swap them to avoid confusing parser and cause an infinite loop + [a, b] = [b, a]; + } + const _t = /** @type {NerdamerSymbolType} */ (_.subtract(a, b)); + /** @type {NerdamerSymbolType} */ + let retval = expand ? /** @type {NerdamerSymbolType} */ (_.expand(_t)) : _t; + + // Quick workaround for issue #636 + // This basically borrows the removeDenom method from the Equation class. + // TODO: Make this function a stand-alone function + retval = new Equation(retval, new NerdamerSymbol(0)).removeDenom().LHS; + + return retval; + }, + /** + * Removes denominators from both sides. + * + * @returns {Equation} Equation with denominators removed + */ + removeDenom() { + let a = this.LHS.clone(); + let b = this.RHS.clone(); + // Remove the denominator on both sides + const den = /** @type {NerdamerSymbolType} */ (_.multiply(a.getDenom(), b.getDenom())); + a = /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(a, den.clone()))); + b = /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(b, den))); + // Swap the groups + if (b.group === CP && b.group !== CP) { + const t = a; + a = b; + b = t; // Swap + } + + // Scan to eliminate denominators + if (a.group === CB) { + let t = new NerdamerSymbol(a.multiplier); + /** @type {NerdamerSymbolType} */ + let newRHS = b.clone(); + a.each(y => { + if (y.power.lessThan(0)) { + newRHS = /** @type {NerdamerSymbolType} */ (_.divide(newRHS, y)); + } else { + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, y)); + } + }); + a = t; + b = newRHS; + } else if (a.group === CP) { + // The logic: loop through each and if it has a denominator then multiply it out on both ends + // and then start over + for (const x in a.symbols) { + if (!Object.hasOwn(a.symbols, x)) { + continue; + } + const sym = a.symbols[x]; + if (sym.group === CB) { + for (const y in sym.symbols) { + if (!Object.hasOwn(sym.symbols, y)) { + continue; + } + const sym2 = sym.symbols[y]; + if (sym2.power.lessThan(0)) { + const result = new Equation( + /** @type {NerdamerSymbolType} */ ( + _.expand(_.multiply(sym2.clone().toLinear(), a)) + ), + /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(sym2.clone().toLinear(), b))) + ); + return result; + } + } + } + } + } + + return new Equation(a, b); + }, + /** + * Creates a copy of this equation. + * + * @returns {Equation} + */ + clone() { + return new Equation(this.LHS.clone(), this.RHS.clone()); + }, + /** + * Substitutes a value for a variable on both sides. + * + * @param {NerdamerSymbolType} x - Variable to replace + * @param {NerdamerSymbolType} y - Value to substitute + * @returns {Equation} + */ + sub(x, y) { + const clone = this.clone(); + clone.LHS = clone.LHS.sub(x.clone(), y.clone()); + clone.RHS = clone.RHS.sub(x.clone(), y.clone()); + return clone; + }, + /** + * Checks if the equation evaluates to zero. + * + * @returns {boolean} + */ + isZero() { + return core.Utils.evaluate(this.toLHS()).equals(0); + }, + /** + * Returns LaTeX representation. + * + * @param {string} [option] + * @returns {string} + */ + latex(option) { + return [this.LHS.latex(option), this.RHS.latex(option)].join('='); + }, + }; + // Overwrite the equals function + /** + * Creates an Equation from two symbols. This extends the parser's equals function to return Equation objects. + * + * @param {NerdamerSymbolType} a + * @param {NerdamerSymbolType} b + * @returns {Equation} + */ + // @ts-ignore - Overriding parser.equals to return Equation instead of Symbol + _.equals = function equals(a, b) { + return new Equation(a, b); + }; + + // Extend simplify + (function extendSimplifyForEquations() { + const simplify = _.functions.simplify[0]; + _.functions.simplify[0] = function simplifyWithEquationSupport(symbol) { + if (symbol instanceof Equation) { + symbol.LHS = simplify(symbol.LHS); + symbol.RHS = simplify(symbol.RHS); + return symbol; + } + // Just call the original simplify + return simplify(symbol); + }; + })(); + + /** + * Sets two expressions equal + * + * @param {NerdamerSymbolType} symbol + * @returns {Equation} + */ + core.Expression.prototype.equals = function equals(symbol) { + if (symbol instanceof core.Expression) { + symbol = symbol.symbol; + } // Grab the symbol if it's an expression + const eq = new Equation(this.symbol, symbol); + return eq; + }; + + core.Expression.prototype.solveFor = function solveFor(x) { + core.Utils.armTimeout(); + try { + const { symbol } = this; + if (this.symbol instanceof Equation) { + // Exit right away if we already have the answer + // check the LHS + if (this.symbol.LHS.isConstant() && this.symbol.RHS.equals(x)) { + return [new core.Expression(this.symbol.LHS)]; + } + + // Check the RHS + if (this.symbol.RHS.isConstant() && this.symbol.LHS.equals(x)) { + return [new core.Expression(this.symbol.RHS)]; + } + } + + const terms = solve(symbol, x); + const result = terms.map(term => { + term = /** @type {NerdamerSymbolType} */ ( + Simplify.simplify(/** @type {NerdamerSymbolType} */ (_.parse(term))) + ); + const expr = new core.Expression(term); + return expr; + }); + return result; + } finally { + core.Utils.disarmTimeout(); + } + }; + + core.Expression.prototype.expand = function expand() { + if (this.symbol instanceof Equation) { + const clone = this.symbol.clone(); + clone.RHS = /** @type {NerdamerSymbolType} */ (_.expand(clone.RHS)); + clone.LHS = /** @type {NerdamerSymbolType} */ (_.expand(clone.LHS)); + return new core.Expression(clone); + } + return new core.Expression(_.expand(/** @type {NerdamerSymbolType} */ (this.symbol))); + }; + + // eslint-disable-next-line func-names -- naming this 'variables' would shadow the imported variables utility + core.Expression.prototype.variables = function () { + if (this.symbol instanceof Equation) { + return core.Utils.arrayUnique( + core.Utils.variables(this.symbol.LHS).concat(core.Utils.variables(this.symbol.RHS)) + ); + } + return core.Utils.variables(this.symbol); + }; + + const setEq = function setEq(a, b) { + return _.equals(a, b); + }; + + // Link the Equation class back to the core + core.Equation = Equation; + + // Loops through an array and attempts to fails a test. Stops if manages to fail. + const checkAll = (core.Utils.checkAll = function checkAll(args, test) { + for (let i = 0; i < args.length; i++) { + if (test(args[i])) { + return false; + } + } + return true; + }); + + // Version solve + /** @type {SolveModuleType} */ + const __ = (core.Solve = { + version: '2.0.3', + /** @type {NerdamerSymbolType[]} */ + solutions: [], + solve(eq, variable) { + const save = Settings.PARSE2NUMBER; + Settings.PARSE2NUMBER = false; + const solution = solve(eq, String(variable)); + Settings.PARSE2NUMBER = save; + return new core.Vector(solution); + // Return new core.Vector(solve(eq.toString(), variable ? variable.toString() : variable)); + }, + /** + * Brings the equation to LHS. A string can be supplied which will be converted to an Equation + * + * @param {Equation | string | NerdamerSymbolType} eqn + * @param {boolean} [expand] + * @returns {NerdamerSymbolType} + */ + toLHS(eqn, expand) { + if (isSymbol(eqn)) { + return eqn; + } + // If it's an equation then call its toLHS function instead + if (!(eqn instanceof Equation)) { + const eqnStr = /** @type {string} */ (eqn); + const es = eqnStr.split('='); + // Convert falsey values to zero + es[1] ||= '0'; + eqn = new Equation( + /** @type {NerdamerSymbolType} */ (_.parse(es[0])), + /** @type {NerdamerSymbolType} */ (_.parse(es[1])) + ); + } + return eqn.toLHS(expand); + }, + // GetSystemVariables: function(eqns) { + // vars = variables(eqns[0], null, null, true); + // + // //get all variables + // for (let i = 1, l=eqns.length; i < l; i++) + // vars = vars.concat(variables(eqns[i])); + // //remove duplicates + // vars = core.Utils.arrayUnique(vars).sort(); + // + // //done + // return vars; + // }, + /** + * Solve a set of circle equations. + * + * @param {NerdamerSymbolType[]} eqns + * @param {string[]} vars + * @returns {Array | object} + */ + solveCircle(eqns, vars) { + // Convert the variables to symbols + const svars = vars.map(x => /** @type {NerdamerSymbolType} */ (_.parse(x))); + + /** @type {number[][]} */ + const deg = []; + + /** @type {CircleSolutionResultType} */ + let solutions = []; + + // Get the degree for the equations + for (let i = 0; i < eqns.length; i++) { + /** @type {number[]} */ + const d = []; + for (let j = 0; j < svars.length; j++) { + d.push(Number(_A.degree(eqns[i], svars[j]))); + } + // Store the total degree + d.push(/** @type {number} */ (core.Utils.arraySum(d, true))); + deg.push(d); + } + + let a = eqns[0]; + let b = eqns[1]; + + if (deg[0][2] > deg[1][2]) { + [b, a] = [a, b]; + [deg[1], deg[0]] = [deg[0], deg[1]]; + } + + // Only solve it's truly a circle + if (deg[0][0] === 1 && deg[0][2] === 2 && deg[1][0] === 2 && deg[1][2] === 4) { + // For clarity we'll refer to the variables as x and y + const x = vars[0]; + const y = vars[1]; + + // We can now get the two points for y + const yPoints = solve( + /** @type {NerdamerSymbolType} */ ( + _.parse(b, knownVariable(x, solve(/** @type {NerdamerSymbolType} */ (_.parse(a)), x)[0])) + ), + y + ).map(pt => pt.toString()); + + // Since we now know y we can get the two x points from the first equation + const xPoints = [ + solve(/** @type {NerdamerSymbolType} */ (_.parse(a, knownVariable(y, yPoints[0]))))[0].toString(), + ]; + + if (yPoints[1]) { + xPoints.push( + solve( + /** @type {NerdamerSymbolType} */ (_.parse(a, knownVariable(y, yPoints[1]))) + )[0].toString() + ); + } + + if (Settings.SOLUTIONS_AS_OBJECT) { + /** @type {Record} */ + const solObj = {}; + solObj[x] = xPoints; + solObj[y] = yPoints; + solutions = solObj; + } else { + yPoints.unshift(y); + xPoints.unshift(x); + solutions = [xPoints, yPoints]; + } + } + + return solutions; + }, + /** + * Solve a system of nonlinear equations + * + * @param {NerdamerSymbolType[]} eqns The array of equations + * @param {number} [tries] The maximum number of tries + * @param {number} [start] The starting point where to start looking for solutions + * @returns {SystemSolutionResultType | []} + */ + solveNonLinearSystem(eqns, tries, start) { + if (tries < 0) { + return []; // Can't find a solution + } + + start = typeof start === 'undefined' ? core.Settings.NON_LINEAR_START : start; + + // The maximum number of times to jump + const maxTries = core.Settings.MAX_NON_LINEAR_TRIES; + + // Halfway through the tries + const halfway = Math.floor(maxTries / 2); + + // Initialize the number of tries to 10 if not specified + tries = typeof tries === 'undefined' ? maxTries : tries; + + // A point at which we check to see if we're converging. By inspection it seems that we can + // use around 20 iterations to see if we're converging. If not then we retry a jump of x + const jumpAt = core.Settings.NON_LINEAR_JUMP_AT; + + // We jump by this many points at each pivot point + const jump = core.Settings.NON_LINEAR_JUMP_SIZE; + + // Used to check if we actually found a solution or if we gave up. Assume we will find a solution. + let found = true; + + const createSubs = function (vars, matrix) { + return vars.map((x, i) => Number(matrix.get(i, 0))); + }; + + const vars = core.Utils.arrayGetVariables(eqns); + const jacobian = core.Matrix.jacobian(eqns, vars, x => build(x, vars), true); + + const maxIter = core.Settings.MAX_NEWTON_ITERATIONS; + let o; + let y; + let iters; + let xn1; + let norm; + let lnorm; + let xn; + let d; + + const fEqns = eqns.map(eq => build(eq, vars)); + + // Note: J stores compiled functions, not symbols. We use Matrix for its iteration + // capabilities, but elements are actually compiled functions `(...args: number[]) => number` + // The type system expects NerdamerSymbol but we're deliberately storing functions. + const J = jacobian.map( + (/** @type {NerdamerSymbolType} */ e) => + /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (build(e, vars))), + true + ); + // Initial values + xn1 = core.Matrix.cMatrix(0, vars); + + // Initialize the c matrix with something close to 0. + let c = core.Matrix.cMatrix(start, vars); + + iters = 0; + + // Start of algorithm + do { + // If we've reached the max iterations then exit + if (iters > maxIter) { + found = false; + break; + } + + // Set the substitution object + o = createSubs(vars, c); + + // Set xn + xn = c.clone(); + + // Capture current values for use in callbacks + const currentO = o; + const currentC = c; + + // Make all the substitutions for each of the equations + fEqns.forEach((f, i) => { + currentC.set(i, 0, f(...currentO)); + }); + + let m = new core.Matrix(); + // J actually contains compiled functions, cast to access them + /** @type {{ each: (fn: (element: unknown, row: number, col: number) => void) => void }} */ ( + /** @type {unknown} */ (J) + ).each((fn, i, j) => { + const ans = /** @type {(...args: number[]) => number} */ (fn)(...currentO); + m.set(i, j, ans); + }); + + m = m.invert(); + + // Preform the elimination + y = /** @type {MatrixType} */ (_.multiply(m, c)).negate(); + + // The callback is to avoid overflow in the coeffient denonimator + // it converts it to a decimal and then back to a fraction. Some precision + // is lost be it's better than overflow. + d = y.subtract(xn1, x => _.parse(Number(x))); + + xn1 = xn.add(y, x => _.parse(Number(x))); + + // Move c is now xn1 + c = xn1; + + // Get the norm + + // the expectation is that we're converging to some answer as this point regardless of where we start + // this may have to be adjusted at some point because of erroneous assumptions + if (iters >= jumpAt) { + // Check the norm. If the norm is greater than one then it's time to try another point + if (Number(norm) > 1) { + // Reset the start point at halway + if (tries === halfway) { + start = 0; + } + const sign = tries > halfway ? 1 : -1; // Which side are we incrementing + // we increment +n at one side and -n at the other. + const n = (tries % Math.floor(halfway)) + 1; + // Adjust the start point + start += sign * n * jump; + // Call restart + return __.solveNonLinearSystem(eqns, --tries, start); + } + } + lnorm = norm; + iters++; + norm = d.max(); + + // Exit early. Revisit if we get bugs + if (Number(norm) === Number(lnorm)) { + break; + } + } while (Number(norm) >= Number.EPSILON); + + // Return a blank set if nothing was found; + if (!found) { + return []; + } + + // Return c since that's the answer + return /** @type {SystemSolutionResultType | []} */ ( + __.systemSolutions(c, vars, true, x => core.Utils.round(Number(x), 14)) + ); + }, + /** + * Converts solution results to the appropriate format based on Settings.SOLUTIONS_AS_OBJECT. + * + * @param {MatrixType} result The result matrix + * @param {string[]} vars The variable names + * @param {boolean} [expandResult] Whether to expand the result + * @param {Function} [callback] Optional callback to transform each solution value + * @returns {SystemSolutionResultType} + */ + systemSolutions(result, vars, expandResult, callback) { + if (core.Settings.SOLUTIONS_AS_OBJECT) { + /** @type {Record} */ + const solutions = {}; + result.each((e, idx) => { + /** @type {SystemSolutionValueType} */ + let solution = /** @type {string | number} */ ((expandResult ? _.expand(e) : e).valueOf()); + if (callback) { + solution = callback.call(e, solution); + } + solutions[vars[idx]] = solution; + }); + return solutions; + } + /** @type {[string, SystemSolutionValueType][]} */ + const solutions = []; + result.each((e, idx) => { + /** @type {SystemSolutionValueType} */ + let solution = /** @type {string | number} */ ((expandResult ? _.expand(e) : e).valueOf()); + if (callback) { + solution = callback.call(e, solution); + } + solutions.push([vars[idx], solution]); + }); + return solutions; + }, + /** + * Solves a system of equations by substitution. This is useful when no distinct solution exists. e.g. a line, + * plane, etc. + * + * @param {Array} eqns + * @returns {CircleSolutionResultType | []} + */ + solveSystemBySubstitution(eqns) { + // Assume at least 2 equations. The function variables will just return an empty array if undefined is provided + const varsA = variables(eqns[0]); + const varsB = variables(eqns[1]); + // Check if it's a circle equation + if (eqns.length === 2 && varsA.length === 2 && core.Utils.arrayEqual(varsA, varsB)) { + return /** @type {CircleSolutionResultType | []} */ (__.solveCircle(eqns, varsA)); + } + + return []; // Return an empty set + }, + + // https://www.lakeheadu.ca/sites/default/files/uploads/77/docs/RemaniFinal.pdf + /** + * Solves a systems of equations + * + * @param {Array} eqns An array of equations + * @param {Array} varArray An array of variables + * @returns {Array | object} + */ + solveSystem(eqns, varArray) { + // Check if a varArray was specified + // nerdamer.clearVars();// this deleted ALL variables: not what we want + // parse all the equations to LHS. Remember that they come in as strings + for (let i = 0; i < eqns.length; i++) { + eqns[i] = __.toLHS(eqns[i]); + } + + const l = eqns.length; + let m = new core.Matrix(); + const c = new core.Matrix(); + let expandResult = false; + let vars; + + if (typeof varArray === 'undefined') { + // Check to make sure that all the equations are linear + if (!_A.allLinear(eqns)) { + try { + return __.solveNonLinearSystem(eqns); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + if (e instanceof core.exceptions.DivisionByZero) { + return __.solveSystemBySubstitution(eqns); + } + } + } + + vars = core.Utils.arrayGetVariables(eqns); + + // If the system only has one variable then we solve for the first one and + // then test the remaining equations with that solution. If any of the remaining + // equation fails then the system has no solution + if (vars.length === 1) { + let n = 0; + let sol; + let e; + do { + e = eqns[n].clone(); + + if (n > 0) { + e = e.sub(vars[0], sol[0]); + } + + sol = solve(e, vars[0]); + // Skip the first one + if (n === 0) { + continue; + } + } while (++n < eqns.length); + + // Format the output + let solutions; + if (Settings.SOLUTIONS_AS_OBJECT) { + solutions = {}; + solutions[vars[0]] = sol; + } else if (sol.length === 0) { + solutions = sol; // No solutions + } else { + solutions = [vars[0], sol]; + } + + return solutions; + } + + // Deal with redundant equations as expressed in #562 + // The fix is to remove all but the number of equations equal to the number + // of variables. We then solve those and then evaluate the remaining equations + // with those solutions. If the all equal true then those are just redundant + // equations and we can return the solution set. + if (vars.length < eqns.length) { + const reduced = []; + const n = eqns.length; + for (let i = 0; i < n - 1; i++) { + reduced.push(_.parse(eqns[i])); + } + + /** @type {Record} */ + const knowns = {}; + const solutions = __.solveSystem(reduced, vars); + // The solutions may have come back as an array + if (Array.isArray(solutions)) { + solutions.forEach(sol => { + // For substitution, we only use single-value solutions (not arrays) + if (!Array.isArray(sol[1])) { + knowns[sol[0]] = sol[1]; + } + }); + } else { + // Filter out array solutions for substitution + for (const key of Object.keys(solutions)) { + const val = solutions[key]; + if (!Array.isArray(val)) { + knowns[key] = val; + } + } + } + + // Start by assuming they will all evaluate to zero. If even one fails + // then all zero will be false + let allZero = true; + // Check if the last solution evalutes to zero given these solutions + for (let i = n - 1; i < n; i++) { + if (!(/** @type {NerdamerSymbolType} */ (_.parse(eqns[i], knowns)).equals(0))) { + allZero = false; + } + } + + if (allZero) { + return solutions; + } + } + + // Deletes only the variables of the linear equations in the nerdamer namespace + for (let i = 0; i < vars.length; i++) { + nerdamer.setVar(vars[i], 'delete'); + } + // TODO: move this to cMatrix or something similar + // populate the matrix + for (let i = 0; i < l; i++) { + const e = eqns[i]; // Store the expression + // Iterate over the columns + for (let j = 0; j < vars.length; j++) { + const v = vars[j]; + let coeffs = []; + e.each(x => { + if (x.contains(v)) { + coeffs = coeffs.concat(x.coeffs()); + } + }); + + const cf = core.Utils.arraySum(coeffs); + m.set(i, j, cf); + } + + // Strip the variables from the symbol so we're left with only the zeroth coefficient + // start with the symbol and remove each variable and its coefficient + let num = e.clone(); + vars.forEach(varName => { + num = num.stripVar(varName, true); + }); + c.set(i, 0, num.negate()); + } + } else { + /** + * The idea is that we loop through each equation and then expand it. Afterwards we loop through each + * term and see if and check to see if it matches one of the variables. When a match is found we mark + * it. No other match should be found for that term. If it is we stop since it's not linear. + */ + vars = varArray; + expandResult = true; + for (let i = 0; i < l; i++) { + // Prefill + c.set(i, 0, new NerdamerSymbol(0)); + const e = /** @type {NerdamerSymbolType[]} */ ( + /** @type {NerdamerSymbolType} */ (_.expand(eqns[i])).collectSummandSymbols() + ); // Expand and store + // go trough each of the variables + for (let j = 0; j < varArray.length; j++) { + m.set(i, j, new NerdamerSymbol(0)); + const v = varArray[j]; + // Go through the terms and sort the variables + for (let k = 0; k < e.length; k++) { + const term = e[k]; + let check = false; + for (let z = 0; z < varArray.length; z++) { + // Check to see if terms contain multiple variables + if (term.contains(varArray[z])) { + if (check) { + core.Utils.err(`Multiple variables found for term ${term}`); + } + check = true; + } + } + // We made sure that every term contains one variable so it's safe to assume that if the + // variable is found then the remainder is the coefficient. + if (term.contains(v)) { + const tparts = /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ ( + explode(remove(e, k), v) + ); + k--; // Issue #52: decrement k to hit this spot in the array e again next loop + m.set(i, j, _.add(m.get(i, j), /** @type {NerdamerSymbolType} */ (tparts[0]))); + } + } + } + // All the remaining terms go to the c matrix + for (let k = 0; k < e.length; k++) { + c.set(i, 0, _.add(c.get(i, 0), e[k])); + } + } + // Consider case (a+b)*I+u + } + + // Check if the system has a distinct solution + if (vars.length !== eqns.length || m.determinant().equals(0)) { + // Solve the system by hand + // return __.solveSystemBySubstitution(eqns, vars, m, c); + throw new core.exceptions.SolveError('System does not have a distinct solution'); + } + + // Use M^-1*c to solve system + m = m.invert(); + const result = m.multiply(c); + // Correct the sign as per issue #410 + if (core.Utils.isArray(varArray)) { + result.each(x => x.negate()); + } + + return __.systemSolutions(result, vars, expandResult); + }, + /** + * The quadratic function but only one side. + * + * @param {NerdamerSymbolType} c + * @param {NerdamerSymbolType} b + * @param {NerdamerSymbolType} a + * @returns {(NerdamerSymbolType | VectorType | MatrixType)[]} + */ + quad(c, b, a) { + let discriminant = _.subtract( + _.pow(b.clone(), new NerdamerSymbol(2)), + _.multiply(_.multiply(a.clone(), c.clone()), new NerdamerSymbol(4)) + ); /* B^2 - 4ac*/ + // Fix for #608 + discriminant = /** @type {NerdamerSymbolType} */ (_.expand(discriminant)); + const det = /** @type {NerdamerSymbolType} */ (_.pow(discriminant, new NerdamerSymbol(0.5))); + const den = /** @type {NerdamerSymbolType} */ ( + _.parse(/** @type {NerdamerSymbolType} */ (_.multiply(new NerdamerSymbol(2), a.clone()))) + ); + const retval = [ + _.parse(format('(-({0})+({1}))/({2})', b, det, den)), + _.parse(format('(-({0})-({1}))/({2})', b, det, den)), + ]; + + return retval; + }, + /** + * The cubic equation + * http://math.stackexchange.com/questions/61725/is-there-a-systematic-way-of-solving-cubic-equations + * + * @param {NerdamerSymbolType} dO + * @param {NerdamerSymbolType} cO + * @param {NerdamerSymbolType} bO + * @param {NerdamerSymbolType} aO + * @returns {Array} + */ + cubic(dO, cO, bO, aO) { + // Convert everything to text + const a = aO.text(); + const b = bO.text(); + const c = cO.text(); + const d = dO.text(); + + const t = `(-(${b})^3/(27*(${a})^3)+(${b})*(${c})/(6*(${a})^2)-(${d})/(2*(${a})))`; + const u = `((${c})/(3*(${a}))-(${b})^2/(9*(${a})^2))`; + const v = `(${b})/(3*(${a}))`; + const x = `((${t})+sqrt((${t})^2+(${u})^3))^(1/3)+((${t})-sqrt((${t})^2+(${u})^3))^(1/3)-(${v})`; + + // Convert a to one + const w = '1/2+sqrt(3)/2*i'; // Cube root of unity + + return [_.parse(x), _.parse(`(${x})(${w})`), _.parse(`(${x})(${w})^2`)]; + + // https://www.wikihow.com/Solve-a-Cubic-Equation method 3 + // const delta0 = `(${b})^2-(3*(${a})(${c}))`; + // _.parse(delta0); + // const delta1 = `2(${b})^3-(9*(${a})(${b})(${c}))+27((${a})^2)(${d})`; + // _.parse(delta1); + // // const delta = `(${delta1})^2-(4*(${delta0})^3)/(-27(${a})^2)`; + + // const C = `((sqrt((${delta1})^2-(4*(${delta0})^3))+(${delta1}))/2)^(1/3)`; + // _.parse(C); + // const u = `(-1+sqrt(-3))/2` + // _.parse(u); + + // const result = [] + // for (let n = 1; n <=3; n++) { + // let x = `-((${b})+ (${u})^${n}*(${C})+(${delta0})/((${u})^${n}*(${C})))/(3(${a}))`; + // console.log(x); + // console.log(x.substring(168)); + // result.push(_.parse(x)); + // } + + // return result.map((x)=>_.parse(x)) + }, + /** + * The quartic equation + * + * @param {NerdamerSymbolType} e + * @param {NerdamerSymbolType} d + * @param {NerdamerSymbolType} c + * @param {NerdamerSymbolType} b + * @param {NerdamerSymbolType} a + * @returns {Array} + */ + quartic(e, d, c, b, a) { + /** @type {Record} */ + const scope = {}; + core.Utils.arrayUnique( + variables(a).concat(variables(b)).concat(variables(c)).concat(variables(d)).concat(variables(e)) + ).forEach(x => { + scope[x] = 1; + }); + const aStr = a.toString(); + const bStr = b.toString(); + const cStr = c.toString(); + const dStr = d.toString(); + const eStr = e.toString(); + let _D; + /* Var D = core.Utils.block('PARSE2NUMBER', function() { + return _.parse(format("256*({0})^3*({4})^3-192*({0})^2*({1})*({3})*({4})^2-128*({0})^2*({2})^2*({4})^2+144*({0})^2*({2})*({3})^2*({4})"+ + "-27*({0})^2*({3})^4+144*({0})*({1})^2*({2})*({4})^2-6*({0})*({1})^2*({3})^2*({4})-80*({0})*({1})*({2})^2*({3})*({4})+18*({0})*({1})*({2})*({3})^3"+ + "+16*({0})*({2})^4*({4})-4*({0})*({2})^3*({3})^2-27*({1})^4*({4})^2+18*({1})^3*({2})*({3})*({4})-4*({1})^3*({3})^3-4*({1})^2*({2})^3*({4})+({1})^2*({2})^2*({3})^2", + aStr, bStr, cStr, dStr, eStr), scope); + });*/ + + const p = _.parse(format('(8*({0})*({2})-3*({1})^2)/(8*({0})^2)', aStr, bStr, cStr)).toString(); // A, b, c + const q = _.parse( + format('(({1})^3-4*({0})*({1})*({2})+8*({0})^2*({3}))/(8*({0})^3)', aStr, bStr, cStr, dStr) + ).toString(); // A, b, c, d, e + const D0 = _.parse(format('12*({0})*({4})-3*({1})*({3})+({2})^2', aStr, bStr, cStr, dStr, eStr)).toString(); // A, b, c, d, e + const D1 = _.parse( + format( + '2*({2})^3-9*({1})*({2})*({3})+27*({1})^2*({4})+27*({0})*({3})^2-72*({0})*({2})*({4})', + aStr, + bStr, + cStr, + dStr, + eStr + ) + ).toString(); // A, b, c, d, e + const Q = _.parse(format('((({1})+(({1})^2-4*({0})^3)^(1/2))/2)^(1/3)', D0, D1)).toString(); // D0, D1 + const quarticS = _.parse( + format('(1/2)*(-(2/3)*({1})+(1/(3*({0}))*(({2})+(({3})/({2})))))^(1/2)', aStr, p, Q, D0) + ).toString(); // A, p, Q, D0 + const x1 = _.parse( + format( + '-(({1})/(4*({0})))-({4})+(1/2)*sqrt(-4*({4})^2-2*({2})+(({3})/({4})))', + aStr, + bStr, + p, + q, + quarticS + ) + ); // A, b, p, q, S + const x2 = _.parse( + format( + '-(({1})/(4*({0})))-({4})-(1/2)*sqrt(-4*({4})^2-2*({2})+(({3})/({4})))', + aStr, + bStr, + p, + q, + quarticS + ) + ); // A, b, p, q, S + const x3 = _.parse( + format( + '-(({1})/(4*({0})))+({4})+(1/2)*sqrt(-4*({4})^2-2*({2})-(({3})/({4})))', + aStr, + bStr, + p, + q, + quarticS + ) + ); // A, b, p, q, S + const x4 = _.parse( + format( + '-(({1})/(4*({0})))+({4})-(1/2)*sqrt(-4*({4})^2-2*({2})-(({3})/({4})))', + aStr, + bStr, + p, + q, + quarticS + ) + ); // A, b, p, q, S + return [x1, x2, x3, x4]; + }, + /** + * Breaks the equation up in its factors and tries to solve the smaller parts + * + * @param {NerdamerSymbolType} symbol + * @param {string} solveFor + * @returns {Array} + */ + divideAndConquer(symbol, solveFor) { + let sols = []; + // See if we can solve the factors + const factors = Factor.factorInner(symbol); + if (factors.group === CB) { + factors.each(x => { + x = NerdamerSymbol.unwrapPARENS(x); + sols = sols.concat(solve(x, solveFor)); + }); + } + return sols; + }, + /** + * Attempts to solve the equation assuming it's a polynomial with numeric coefficients + * + * @param {NerdamerSymbolType} eq + * @param {string} solveFor + * @returns {Array} + */ + csolve(eq, solveFor) { + return core.Utils.block( + 'IGNORE_E', + () => { + let p; + let pn; + let n; + let pf; + let r; + let _theta; + let sr; + let _sp; + const roots = []; + const f = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(eq, solveFor, true)); + if (f.x.group === S) { + p = _.parse(f.x.power); + pn = Number(p); + n = _.pow(_.divide(f.b.negate(), f.a), /** @type {NerdamerSymbolType} */ (p).invert()); + pf = NerdamerSymbol.toPolarFormArray(/** @type {NerdamerSymbolType} */ (n)); + r = pf[0]; + _theta = pf[1]; + sr = r.toString(); + _sp = p.toString(); + let k; + let root; + let str; + for (let i = 0; i < pn; i++) { + k = i; + str = format('({0})*e^(2*{1}*pi*{2}*{3})', sr, k, p, core.Settings.IMAGINARY); + root = _.parse(str); + roots.push(root); + } + } + return roots; + }, + true + ); + }, + /** + * Generates starting points for the Newton solver given an expression at zero. It begins by checking if zero is + * a good point and starts expanding by a provided step size. Builds on the fact that if the sign changes over + * an interval then a zero must exist on that interval + * + * @param {NerdamerSymbolType} symbol + * @param {number} step + * @param {boolean} extended + * @returns {Array} + */ + getPoints(symbol, step, extended) { + step ||= 0.01; + let points = []; + const f = build(symbol); + const x0 = 0; + + const start = Math.round(x0); + const _last = f(start); + const rside = core.Settings.ROOTS_PER_SIDE; // The max number of roots on right side + const lside = rside; // The max number of roots on left side + // check around the starting point + points.push(Math.floor(start / 2)); // Half way from zero might be a good start + points.push(Math.abs(start)); // |f(0)| could be a good start + points.push(start); // |f(0)| could be a good start + // adjust for log. A good starting point to include for log is 0.1 + symbol.each(x => { + if (x.containsFunction(core.Settings.LOG)) { + points.push(0.1); + } + }); + + const left = range(-core.Settings.SOLVE_RADIUS, start, step); + const right = range(start, core.Settings.SOLVE_RADIUS, step); + + const testSide = function (side, numRoots) { + // Console.log("test side "+side[0]+":"+side.at(-1)); + let xi; + let val; + let sign; + const hits = []; + const lastPoint = side[0]; + let lastSign = Math.sign(f(lastPoint)); + for (let i = 0, l = side.length; i < l && hits.length < numRoots; i++) { + xi = side[i]; // The point being evaluated + val = f(xi); + sign = Math.sign(val); + // Don't add non-numeric values + if (isNaN(sign)) { + continue; + } + + // Compare the signs. The have to be different if they cross a zero + if (sign !== lastSign) { + hits.push(xi); // Take note of the possible zero location + hits.push(side[i - 1]); // Also the other side + // console.log(" hit at "+xi); + // if (hits.length >= numRoots){ + // break; + // } + } + lastSign = sign; + } + + points = points.concat(hits); + }; + + testSide(left, lside); + testSide(right, rside); + + if (extended) { + // Check for sign changes way outside the range + // in a limited way + const max = core.Settings.SOLVE_RADIUS; + testSide([max, max * max], 1); + testSide([-max * max, -max], 1); + } + + // Console.log("points: "+points); + return points; + }, + /** + * Implements the bisection method. Returns undefined in no solution is found + * + * @param {number} point + * @param {Function} f + * @returns {undefined | number} + */ + bisection(point, f) { + let left = point - 1; + let right = point + 1; + // First test if this point is even worth evaluating. It should + // be crossing the x axis so the signs should be different + if (Math.sign(f(left)) !== Math.sign(f(right))) { + let safety = 0; + + let epsilon; + let middle; + + do { + epsilon = Math.abs(right - left); + // Safety against an infinite loop + if (safety++ > core.Settings.MAX_BISECTION_ITER || isNaN(epsilon)) { + return undefined; + } + // Calculate the middle point + middle = (left + right) / 2; + + if (f(left) * f(middle) > 0) { + left = middle; + } else { + right = middle; + } + } while (epsilon >= Settings.EPSILON); + + const solution = (left + right) / 2; + + // Test the solution to make sure that it's within tolerance + const xPoint = f(solution); + + if (!isNaN(xPoint) && Math.abs(xPoint) <= core.Settings.BI_SECTION_EPSILON) { + // Returns too many junk solutions if not rounded at 13th place. + return /** @type {number} */ (core.Utils.round(solution, 13)); + } + return undefined; + } + return undefined; + }, + // Helper function for when Newton gets into the weeds + // look from left and right of a sign-change interval + // narrows it down + // result: A real point with tractable numbers to continue from + // or undefined + bSearch(left, right, f) { + let fLeft = f(left); + let fRight = f(right); + // Reject imposters + if (Math.sign(fLeft) === Math.sign(fRight) || isNaN(fLeft) || isNaN(fRight)) { + return undefined; + } + + const maxIter = 80; // Guess the amount of iterations to outrun precision? + let iterations = 0; + do { + const x = (left + right) / 2; + const sLeft = Math.sign(fLeft); + const sX = Math.sign(f(x)); + if (sLeft === sX) { + if (x === left) { + break; // Precision exceeded + } + left = x; + fLeft = f(left); + } else { + if (x === right) { + break; // Precision exceeded + } + right = x; + fRight = f(right); + } + iterations++; + } while (left !== right && iterations < maxIter); + // If one of them is infinite or NaN, there is probably a singularity here + if (!isFinite(f(left)) || !isFinite(f(right))) { + return undefined; + } + // Return the point where the absolute value is smaller + // return (Math.abs(f(left)) < Math.abs(f(right)))? left:right; + return left; + }, + /** + * Implements Newton's iterations. Returns undefined if no solutions if found + * + * @param {number} point + * @param {Function} f + * @param {Function} fp + * @returns {undefined | number} + */ + Newton(point, f, fp, point2) { + // Console.log("Newton point "+point); + const maxiter = core.Settings.MAX_NEWTON_ITERATIONS; + let iter = 0; + // First try the point itself. If it's zero voila. We're done + let x0 = point; + let x; + let e; + let delta; + do { + const fx0 = f(x0); // Store the result of the function + // if the value is zero then we're done because 0 - (0/d f(x0)) = 0 + if (x0 === 0 && fx0 === 0) { + x = 0; + // Console.log(" exact zero"); + break; + } + + iter++; + if (iter > maxiter) { + // Console.log(" iter:"+iter+", last e:"+e); + return undefined; + } + + const fpx0 = fp(x0); + // Infinite or NaN or 0 derivative at x0? + if (isNaN(fpx0) || isNaN(fx0)) { + // Nothing we can do + // console.log(" non-finite derivative"); + return undefined; + } + if (fpx0 === 0) { + // Max/min or saddle point. what can we do? repeat last delta. + x += delta; + } else if (!isFinite(fx0) || !isFinite(fpx0) || Math.abs(fx0) > 1e25) { + // Hail Mary: binary search through the + // sign-switch interval + return __.bSearch(point2, x0, /** @type {(x: number) => number} */ (f)); + // // numbers got too big + // // at least follow the slope down + // const direction = Math.sign(fpx0)/Math.sign(fx0); + // // direction is 1 or -1 + // // big and growing: shrink x + // // -big and -growing: shrink x + // // big and -growing: grow x + // // -big and growing: grow x + // if (x0 === 0) { + // // just move it a bit, so in the next loop + // // we can make progress + // x = x0 + direction; + // } else { + // // can shrink/grow by dividing or multiplying + // x = x0 / (direction===1?2:0.5); + // } + } else { + // Regular case, follow tangent + x = x0 - fx0 / fpx0; + // Console.log("new x: "+x); + } + delta = x - x0; + if (delta === 0 && !isFinite(fpx0)) { + // No movement + return undefined; + } + e = Math.abs(delta); + x0 = x; + } while (e > Settings.NEWTON_EPSILON); + + // Console.log(" found "+x); + return x; + }, + rewrite(rhs, lhs, forVariable) { + lhs ||= new NerdamerSymbol(0); + if (rhs.isComposite() && rhs.isLinear()) { + // Try to isolate the square root + // container for the square roots + const sqrts = []; + // All else + const rem = []; + rhs.each(x => { + x = x.clone(); + if (x.fname === 'sqrt' && x.contains(forVariable)) { + sqrts.push(x); + } else { + rem.push(x); + } + }, true); + + if (sqrts.length === 1) { + // Move the remainder to the RHS + lhs = /** @type {NerdamerSymbolType} */ ( + _.expand( + _.pow( + _.subtract(lhs, /** @type {NerdamerSymbolType} */ (core.Utils.arraySum(rem))), + new NerdamerSymbol(2) + ) + ) + ); + // Square both sides + rhs = /** @type {NerdamerSymbolType} */ ( + _.expand(_.pow(NerdamerSymbol.unwrapSQRT(sqrts[0]), new NerdamerSymbol(2))) + ); + } + } else { + rhs = NerdamerSymbol.unwrapSQRT(/** @type {NerdamerSymbolType} */ (_.expand(rhs))); // Expand the term expression go get rid of quotients when possible + } + + let c = 0; // A counter to see if we have all terms with the variable + const l = rhs.length; + // Try to rewrite the whole thing + if (rhs.group === CP && rhs.contains(forVariable) && rhs.isLinear()) { + rhs.distributeMultiplier(); + let t = new NerdamerSymbol(0); + // First bring all the terms containing the variable to the lhs + rhs.each(x => { + if (x.contains(forVariable)) { + c++; + t = /** @type {NerdamerSymbolType} */ (_.add(t, x.clone())); + } else { + lhs = /** @type {NerdamerSymbolType} */ (_.subtract(lhs, x.clone())); + } + }); + rhs = t; + + // If not all the terms contain the variable so it's in the form + // a*x^2+x + if (c !== l) { + return __.rewrite(rhs, lhs, forVariable); + } + return [rhs, lhs]; + } + if (rhs.group === CB && rhs.contains(forVariable) && rhs.isLinear()) { + if (rhs.multiplier.lessThan(0)) { + rhs.multiplier = rhs.multiplier.multiply(new core.Frac(-1)); + lhs.multiplier = lhs.multiplier.multiply(new core.Frac(-1)); + } + if (lhs.equals(0)) { + return new NerdamerSymbol(0); + } + let t = new NerdamerSymbol(1); + rhs.each(x => { + if (x.contains(forVariable)) { + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, x.clone())); + } else { + lhs = /** @type {NerdamerSymbolType} */ (_.divide(lhs, x.clone())); + } + }); + rhs = t; + return __.rewrite(rhs, lhs, forVariable); + } + if (!rhs.isLinear() && rhs.contains(forVariable)) { + const p = /** @type {NerdamerSymbolType} */ (_.parse(rhs.power.clone().invert())); + rhs = /** @type {NerdamerSymbolType} */ (_.pow(rhs, p.clone())); + lhs = /** @type {NerdamerSymbolType} */ ( + _.pow(/** @type {NerdamerSymbolType} */ (_.expand(lhs)), p.clone()) + ); + return __.rewrite(rhs, lhs, forVariable); + } + if (rhs.group === FN || rhs.group === S || rhs.group === PL) { + return [rhs, lhs]; + } + return [rhs, lhs]; + }, + sqrtSolve(symbol, v) { + let sqrts = new NerdamerSymbol(0); + let rem = new NerdamerSymbol(0); + if (symbol.isComposite()) { + symbol.each(x => { + if (x.fname === 'sqrt' && x.contains(v)) { + sqrts = /** @type {NerdamerSymbolType} */ (_.add(sqrts, x.clone())); + } else { + rem = /** @type {NerdamerSymbolType} */ (_.add(rem, x.clone())); + } + }); + // Quick and dirty ATM + if (!sqrts.equals(0)) { + const t = _.expand( + _.multiply( + _.parse(symbol.multiplier), + _.subtract(_.pow(rem, new NerdamerSymbol(2)), _.pow(sqrts, new NerdamerSymbol(2))) + ) + ); + // Square both sides + let solutions = solve(t, v); + // Test the points. The dumb way of getting the answers + solutions = solutions.filter(e => { + if (e.isImaginary()) { + return true; + } + /** @type {Record} */ + const subs = {}; + subs[v] = e; + const point = evaluate(symbol, subs); + if (point.equals(0)) { + return true; + } + return false; + }); + return solutions; + } + } + return undefined; + }, + }); + + // Special case to handle solving equations with exactly one abs() correctly + const absSolve = function (eqns, solveFor, depth, fn) { + const eq = eqns.toString(); + const match = eq.match(/(? 2) { + return null; + } + // Can handle only abs at beginning + if ((eqns.LHS.group !== FN || false) && eqns.RHS.group !== FN) { + return null; + } + // We have exactly one abs. kill it and make two cases + const eqplus = eqns.constructor(eq.replace(/(?"; + // original = eqns.toString(); + // try { + // solutions = _solve(eqns, solveFor, solutions, depth, fn); + // } catch (error) { + // console.error(error); + // } + // console.log("solve: "+original+" for "+solveFor+" = "+solutions); + // return solutions; + // } + function solve(eqns, solveFor, solutions, depth, fn) { + depth ||= 0; + + if (depth++ > Settings.MAX_SOLVE_DEPTH) { + return solutions; + } + + // Parse out functions. Fix for issue #300 + // eqns = core.Utils.evaluate(eqns); + solutions ||= []; + // Mark existing solutions as not to have duplicates + const existing = {}; + + // Easy fail. If it's a rational function and the denominator is zero + // then we're done. Issue #555 + /** @type {Record} */ + const known = {}; + known[solveFor] = 0; + + // Is used to add solutions to set. + // TODO: Set is now implemented and should be utilized + const addToResult = function (r, hasTrig) { + const rIsSymbol = isSymbol(r); + if (r === undefined || (typeof r === 'number' && isNaN(r))) { + return; + } + if (isArray(r)) { + r.forEach(sol => { + addToResult(sol); + }); + } else if (r.valueOf() !== 'null') { + // Call the pre-add function if defined. This could be useful for rounding + if (typeof core.Settings.PRE_ADD_SOLUTION === 'function') { + r = core.Settings.PRE_ADD_SOLUTION(r); + } + + if (!rIsSymbol) { + r = _.parse(r); + } + // Try to convert the number to multiples of pi + if (core.Settings.make_pi_conversions && hasTrig) { + const temp = _.divide(r.clone(), new NerdamerSymbol(Math.PI)); + const m = temp.multiplier; + const a = Math.abs(Number(m.num)); + const b = Math.abs(Number(m.den)); + if (a < 10 && b < 10) { + r = _.multiply(temp, new NerdamerSymbol('pi')); + } + } + + // And check if we get a number otherwise we might be throwing out symbolic solutions. + const rStr = r.toString(); + + if (!existing[rStr]) { + solutions.push(r); + } + // Mark the answer as seen + existing[rStr] = true; + } + }; + + // Make preparations if it's an Equation + if (eqns instanceof Equation) { + // See absSolve above + // the rest of solve does a crappy job at solving abs, + // so we wrap it here if necessary + const absResult = absSolve(eqns, solveFor, depth, fn); + if (absResult) { + addToResult(absResult); + return solutions; + } + + // If it's zero then we're done + if (eqns.isZero()) { + return [new NerdamerSymbol(0)]; + } + // If the lhs = x then we're done + if (eqns.LHS.equals(solveFor) && !eqns.RHS.contains(solveFor, true)) { + return [eqns.RHS]; + } + // If the rhs = x then we're done + if (eqns.RHS.equals(solveFor) && !eqns.LHS.contains(solveFor, true)) { + return [eqns.LHS]; + } + } + + // Unwrap the vector since what we want are the elements + if (eqns instanceof core.Vector) { + eqns = /** @type {SolveEquationArray} */ (eqns.elements); + } + // If it's an array then solve it as a system of equations + // Must check BEFORE the default assignment to preserve original solveFor value + if (isArray(eqns)) { + return __.solveSystem(/** @type {SolveEquationArray} */ (eqns), solveFor); + } + solveFor ||= 'x'; // Assumes x by default + + if (isSymbol(eqns) && evaluate(eqns.getDenom(), known).equals(0) === true) { + return solutions; + } + + // Maybe we get lucky. Try the point at the function. If it works we have a point + // If not it failed + if (eqns.group === S && eqns.contains(solveFor)) { + try { + /** @type {Record} */ + const o = {}; + o[solveFor] = 0; + evaluate(fn, o); + addToResult(new NerdamerSymbol(0)); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // Do nothing; + } + + return solutions; + } + if (eqns.group === CB) { + // It suffices to solve for the numerator + const num = eqns.getNum(); + + if (num.group === CB) { + const sf = String(solveFor); // Everything else belongs to the coeff + // get the denominator and make sure it doesn't have x since we don't know how to solve for those + num.each(x => { + if (x.contains(sf)) { + solve(x, solveFor, solutions, depth, eqns); + } + }); + + return solutions; + } + + return solve(num, solveFor, solutions, depth, fn); + } + + if (eqns.group === FN && eqns.fname === 'sqrt') { + eqns = _.pow(NerdamerSymbol.unwrapSQRT(eqns), new NerdamerSymbol(2)); + } + // Pass in false to not expand equations such as (x+y)^5. + // It suffices to solve for the numerator since there's no value in the denominator which yields a zero for the function + let eq = (core.Utils.isSymbol(eqns) ? eqns : __.toLHS(eqns, false)).getNum(); + const vars = core.Utils.variables(eq); // Get a list of all the variables + const numvars = vars.length; // How many variables are we dealing with + + // it sufficient to solve (x+y) if eq is (x+y)^n since 0^n + if (core.Utils.isInt(eq.power) && Number(eq.power) > 1) { + eq = _.parse(eq).toLinear(); + } + + // If we're dealing with a single variable then we first check if it's a + // polynomial (including rationals).If it is then we use the Jenkins-Traubb algorithm. + // Don't waste time + if ((eq.group === S || eq.group === CB) && eq.contains(solveFor)) { + return [new NerdamerSymbol(0)]; + } + // Force to polynomial. We go through each and then we look at what it would + // take for its power to be an integer + // if the power is a fractional we divide by the fractional power + let fractionals = {}; + let cfact; + + const correctDenom = function (symbol) { + symbol = _.expand(symbol, { + expand_denominator: true, + expand_functions: true, + }); + const original = symbol.clone(); // Preserve the original + + if (symbol.symbols) { + for (const x in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, x)) { + continue; + } + const sym = symbol.symbols[x]; + + // Get the denominator of the sub-symbol + const den = sym.getDenom(); + + if (!den.isConstant(true) && symbol.isComposite()) { + /** @type {NerdamerSymbolType} */ + let t = new NerdamerSymbol(0); + symbol.each(e => { + t = /** @type {NerdamerSymbolType} */ (_.add(t, _.multiply(e, den.clone()))); + }); + + return correctDenom(_.multiply(_.parse(symbol.multiplier), t)); + } + + const parts = explode(sym, solveFor); + const isSqrt = /** @type {NerdamerSymbolType} */ (parts[1]).fname === core.Settings.SQRT; + const v = /** @type {NerdamerSymbolType} */ ( + NerdamerSymbol.unwrapSQRT(/** @type {NerdamerSymbolType} */ (parts[1])) + ); + /** @type {FracType} */ + const p = /** @type {FracType} */ (v.power.clone()); + // Circular logic with sqrt. Since sqrt(x) becomes x^(1/2) which then becomes sqrt(x), this continues forever + // this needs to be terminated if p = 1/2 + if (!isSymbol(p) && !p.equals(1 / 2)) { + if (Number(p.den) > 1) { + if (isSqrt) { + symbol = _.subtract(symbol, sym.clone()); + symbol = _.add(symbol, _.multiply(parts[0].clone(), v)); + return correctDenom(symbol); + } + let c = fractionals[Number(p.den)]; + fractionals[Number(p.den)] = c ? c++ : 1; + } else if (p.sign() === -1) { + const factor = _.parse(`${solveFor}^${Math.abs(Number(p))}`); // This + // unwrap the symbol's denoniator + const currentSymbol = symbol; + currentSymbol.each((y, index) => { + if (y.contains(solveFor)) { + currentSymbol.symbols[index] = _.multiply(y, factor.clone()); + } + }); + fractionals = {}; + return correctDenom(_.parse(currentSymbol)); + } else if (sym.group === PL) { + const minP = core.Utils.arrayMin(core.Utils.keys(sym.symbols).map(Number)); + if (minP < 0) { + const factor = /** @type {NerdamerSymbolType} */ ( + _.parse(`${solveFor}^${Math.abs(minP)}`) + ); + /** @type {NerdamerSymbolType} */ + let corrected = new NerdamerSymbol(0); + original.each(origSym => { + corrected = /** @type {NerdamerSymbolType} */ ( + _.add(corrected, _.multiply(origSym.clone(), factor.clone())) + ); + }, true); + return corrected; + } + } + } + } + } + + return symbol; + }; + + // Separate the equation + const separate = function (equation) { + /** @type {NerdamerSymbolType} */ + let lhs = new NerdamerSymbol(0); + /** @type {NerdamerSymbolType} */ + let rhs = new NerdamerSymbol(0); + equation.each(x => { + if (x.contains(solveFor, true)) { + lhs = /** @type {NerdamerSymbolType} */ (_.add(lhs, x.clone())); + } else { + rhs = /** @type {NerdamerSymbolType} */ (_.subtract(rhs, x.clone())); + } + }); + return [lhs, rhs]; + }; + + /** + * @type {( + * name: string, + * lhs: NerdamerSymbolType, + * rhs: NerdamerSymbolType + * ) => NerdamerSymbolType | undefined} + */ + __.inverseFunctionSolve = function inverseFunctionSolve(name, lhs, rhs) { + // Ax+b comes back as [a, x, ax, b]; + const parts = explode(lhs.args[0], solveFor); + // Check if x is by itself + const x = /** @type {NerdamerSymbolType} */ (parts[1]); + if (x.group === S) { + return /** @type {NerdamerSymbolType} */ ( + _.divide(_.symfunction(name, [_.divide(rhs, _.parse(lhs.multiplier))]), parts[0]) + ); + } + return undefined; + }; + + // First remove any denominators + eq = correctDenom(eq); + + if (eq.equals(0)) { + return [eq]; + } + // Correct fractionals. I can only handle one type right now + const fkeys = core.Utils.keys(fractionals); + if (fkeys.length === 1) { + // Make a note of the factor + cfact = fkeys[0]; + eq.each((x, index) => { + if (x.contains(solveFor)) { + const parts = explode(x, solveFor); + const v = /** @type {NerdamerSymbolType} */ (parts[1]); + const p = /** @type {FracType} */ (v.power); + if (p.den.gt(1)) { + v.power = p.multiply(new core.Frac(cfact)); + eq.symbols[index] = /** @type {NerdamerSymbolType} */ (_.multiply(v, parts[0])); + } + } + }); + eq = _.parse(eq); + } + + // Try for nested sqrts as per issue #486 + addToResult(__.sqrtSolve(eq, solveFor)); + + // Polynomial single variable + if (numvars === 1) { + if (eq.isPoly(true)) { + // Try to factor and solve + const factors = new AlgebraClasses.Factors(); + + Factor.factorInner(eq, factors); + // If the equation has more than one symbolic factor then solve those individually + if (factors.getNumberSymbolics() > 1) { + for (const factorKey in factors.factors) { + if (!Object.hasOwn(factors.factors, factorKey)) { + continue; + } + addToResult(solve(factors.factors[factorKey], solveFor)); + } + } else { + const coeffs = core.Utils.getCoeffs(eq, solveFor); + const deg = coeffs.length - 1; + let wasCalculated = false; + if (vars[0] === solveFor) { + // Check to see if all the coefficients are constant + if ( + checkAll(coeffs, coeff => /** @type {NerdamerSymbolType} */ (coeff).group !== core.groups.N) + ) { + const roots = core.Algebra.proots(eq); + // If all the roots are integers then return those + if (checkAll(roots, root => !core.Utils.isInt(root))) { + // Roots have been calculates + wasCalculated = true; + roots.forEach(root => { + addToResult(new NerdamerSymbol(root)); + }); + } + } + + if (!wasCalculated) { + eqns = _.parse(eqns); + if (eqns instanceof core.Equation) { + eqns = eqns.toLHS(); + } + + // We can solve algebraically for degrees 1, 2, 3. The remainder we switch to Jenkins- + if (deg === 1) { + addToResult( + _.divide( + /** @type {NerdamerSymbolType} */ (coeffs[0]), + /** @type {NerdamerSymbolType} */ (coeffs[1]).negate() + ) + ); + } else if (deg === 2) { + addToResult(_.expand(__.quad.apply(undefined, coeffs))); + } else if (deg === 3) { + let cubicSolutions = []; // Set to blank + // first try to factor and solve + const _factored = Factor.factorInner(/** @type {NerdamerSymbolType} */ (eqns)); + + // If it was successfully factored + cubicSolutions = []; + if (cubicSolutions.length > 0) { + addToResult(cubicSolutions); + } else { + addToResult(__.cubic.apply(undefined, coeffs)); + } + } else { + /* + Var sym_roots = csolve(eq, solveFor); + if(sym_roots.length === 0) + sym_roots = divnconsolve(eq, solveFor); + if(sym_roots.length > 0) + addToResult(sym_roots); + else + */ + _A.proots(eq).map(addToResult); + } + } + } + } + } else { + // Attempt Newton + // Since it's not a polynomial then we'll try to look for a solution using Newton's method + const hasTrig = eq.hasTrig(); + // We get all the points where a possible zero might exist. + const points1 = __.getPoints(eq, 0.1); + const points2 = __.getPoints(eq, 0.05); + const points3 = __.getPoints(eq, 0.01, true); + let points = core.Utils.arrayUnique(points1.concat(points2).concat(points3)).sort((a, b) => a - b); + // Console.log("all points: "+points); + let i; + let point; + let solution; + + // Compile the function + const f = build(eq.clone()); + + // First try to eliminate some points using bisection + const tPoints = []; + for (i = 0; i < points.length; i++) { + point = points[i]; + + // See if there's a solution at this point + solution = __.bisection(point, /** @type {(x: number) => number} */ (f)); + + // If there's no solution then add it to the array for further investigation + if (typeof solution === 'undefined') { + tPoints.push(point); + continue; + } + + // Add the solution to the solution set + // console.log("added without Newton: "+solution); + // console.log("for: "+eq.text()); + addToResult(solution, hasTrig); + } + + // Reset the points to the remaining points + points = tPoints; + // Console.log("Newton points: "+points); + + // Build the derivative and compile a function + const d = _C.diff(eq.clone()); + const fp = build(/** @type {NerdamerSymbolType} */ (d)); + let lastPoint = points[0]; + for (i = 0; i < points.length; i++) { + point = points[i]; + + addToResult( + __.Newton( + point, + /** @type {(x: number) => number} */ (f), + /** @type {(x: number) => number} */ (fp), + lastPoint + ), + hasTrig + ); + lastPoint = point; + } + + // Sort by numerical value to be ready for uniquefy filter + solutions.sort((a, b) => { + const sa = a.text('decimals'); + const sb = b.text('decimals'); + const xa = Number(sa); + const xb = Number(sb); + if (isNaN(xa) && isNaN(xb)) { + return sa.localeCompare(sb); + } + if (isNaN(xa) && !isNaN(xb)) { + return -1; + } + if (!isNaN(xa) && isNaN(xb)) { + return 1; + } + return xa - xb; + }); + + // Round to 15 digits + solutions = solutions.map(a => + a.isConstant() ? new NerdamerSymbol(Number(Number(a).toPrecision(15))) : a + ); + + // Uniquefy to epsilon + // console.log("solutions: "+solutions); + solutions = solutions.filter((sol, idx, arr) => { + const val = Number(Number(sol).toPrecision(15)); + const prevVal = Number(arr[idx - 1]); + // Console.log(" x: "+val) + if (idx === 0 || isNaN(val) || isNaN(prevVal)) { + return true; + } + // If ((Math.abs(val-prevVal) < Settings.EPSILON)) { + // console.log("diff too small: "+val+", "+prevVal); + // } + return Math.abs(val - prevVal) >= Settings.EPSILON; + }); + // Console.log("solutions after filter: "+solutions); + } + // The idea here is to go through the equation and collect the coefficients + // place them in an array and call the quad or cubic function to get the results + } else if (!eq.hasFunc(solveFor) && eq.isComposite()) { + try { + // This is where solving certain quads goes wrong + + const factored = Factor.factorInner(eq.clone()); + const test = _.expand(/** @type {NerdamerSymbolType} */ (_.parse(factored))); + const test2 = _.expand(eq.clone()); + const diff = /** @type {NerdamerSymbolType} */ (_.subtract(test, test2)); + let validFactorization = true; + if (!diff.equals(0)) { + // Console.log("factored: "+test); + // console.log("original: "+test2); + validFactorization = false; + } + + if (validFactorization && factored.group === CB) { + factored.each(factor => { + addToResult(solve(factor, solveFor)); + }); + } else { + const coeffs = core.Utils.getCoeffs(eq, solveFor); + + const l = coeffs.length; + const deg = l - 1; // The degree of the polynomial + // get the denominator and make sure it doesn't have x + + // handle the problem based on the degree + switch (deg) { + case 0: { + const separated = separate(eq); + const lhs = separated[0]; + const rhs = separated[1]; + + if (lhs.group === core.groups.EX) { + // We have a*b^(mx) = rhs + // => log(b^(mx)) = log(rhs/a) + // => mx*log(b) = log(rhs/a) + // => x = log(rhs/a)/(m*log(b)) + + const log = core.Settings.LOG; + const exprStr = `${log}((${rhs})/(${lhs.multiplier}))/(${log}(${lhs.value})*${/** @type {NerdamerSymbolType} */ (lhs.power).multiplier})`; + const parsed = _.parse(exprStr); + addToResult(parsed); + } + break; + } + case 1: + // Nothing to do but to return the quotient of the constant and the LT + // e.g. 2*x-1 + addToResult( + _.divide( + /** @type {NerdamerSymbolType} */ (coeffs[0]), + /** @type {NerdamerSymbolType} */ (coeffs[1]).negate() + ) + ); + break; + case 2: + addToResult(__.quad.apply(undefined, coeffs)); + break; + case 3: + addToResult(__.cubic.apply(undefined, coeffs)); + break; + case 4: + addToResult(__.quartic.apply(undefined, coeffs)); + break; + default: + addToResult(__.csolve(eq, solveFor)); + if (solutions.length === 0) { + addToResult(__.divideAndConquer(eq, solveFor)); + } + } + + if (solutions.length === 0) { + // Try factoring + addToResult(solve(factored, solveFor, solutions, depth)); + } + } + } catch (e) { + /* Something went wrong. EXITING*/ + if (e.message === 'timeout') { + throw e; + } + } + } else { + try { + const rw = __.rewrite(eq, null, solveFor); + const lhs = rw[0]; + let rhs = rw[1]; + if (lhs.group === FN) { + if (lhs.fname === 'abs') { + // Solve only if solveFor was the only arg + if (lhs.args[0].toString() === solveFor) { + addToResult([rhs.clone(), rhs.negate()]); + } + } else if (lhs.fname === 'sin') { + // Asin + addToResult(__.inverseFunctionSolve('asin', lhs, rhs)); + } else if (lhs.fname === 'cos') { + // Asin + addToResult(__.inverseFunctionSolve('acos', lhs, rhs)); + } else if (lhs.fname === 'tan') { + // Asin + addToResult(__.inverseFunctionSolve('atan', lhs, rhs)); + } else if (lhs.fname === core.Settings.LOG) { + // Ax+b comes back as [a, x, ax, b]; + const parts = explode(lhs.args[0], solveFor); + // Check if x is by itself + const x = /** @type {NerdamerSymbolType} */ (parts[1]); + if (x.group === S) { + rhs = _.divide( + _.subtract( + _.pow( + lhs.args.length > 1 ? lhs.args[1] : new NerdamerSymbol('e'), + _.divide(rhs, _.parse(lhs.multiplier)) + ), + parts[3] + ), + parts[0] + ); + const newEq = new Equation(x, rhs).toLHS(); + addToResult(solve(newEq, solveFor)); + } + } else { + addToResult(_.subtract(lhs, rhs)); + } + } else { + const neq = new Equation(lhs, rhs).toLHS(); // Create a new equation + + if (neq.equals(eq)) { + throw new Error('Stopping. No stop condition exists'); + } + addToResult(solve(neq, solveFor)); + } + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + // Let's try this another way + // 1. if the symbol is in the form a*b*c*... then the solution is zero if + // either a or b or c is zero. + if (eq.group === CB) { + addToResult(0); + } else if (eq.group === CP) { + const separated = separate(eq); + const lhs = separated[0]; + const rhs = separated[1]; + + // Reduce the equation + if (lhs.group === core.groups.EX && lhs.value === solveFor) { + // Change the base of both sides + const p = /** @type {NerdamerSymbolType} */ (lhs.power.clone().invert()); + addToResult(_.pow(rhs, p)); + } + } + } + } + + if (cfact) { + solutions = solutions.map(sol => _.pow(sol, new NerdamerSymbol(cfact))); + } + + // Perform some cleanup but don't do it agains arrays, etc + // Check it actually evaluates to zero + if (isSymbol(eqns)) { + /** @type {Record} */ + const knowns = {}; + solutions = solutions.filter(sol => { + try { + knowns[solveFor] = sol; + const zero = Number(evaluate(eqns, knowns)); + + // Allow symbolic answers + if (isNaN(zero)) { + return true; + } + return true; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return false; + } + }); + } + + return solutions; + } + + // Register the functions for external use + nerdamer.register([ + { + name: 'solveEquations', + parent: 'nerdamer', + numargs: -1, + visible: true, + build() { + return solve; // Comment out to return a vector + /* + return function() { + return core.Utils.convertToVector(solve.apply(null, arguments)); + }; + */ + }, + }, + { + name: 'solve', + parent: 'Solve', + numargs: 2, + visible: true, + /** @returns {(...args: unknown[]) => unknown} */ + build() { + return /** @type {(...args: unknown[]) => unknown} */ (core.Solve.solve); + }, + }, + { + name: 'setEquation', + parent: 'Solve', + numargs: 2, + visible: true, + build() { + return setEq; + }, + }, + ]); + nerdamer.updateAPI(); +})(); diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/all.js b/tools/ui/src/lib/vendors/nerdamer-prime/all.js new file mode 100644 index 000000000..9f62f0be7 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/all.js @@ -0,0 +1,16 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * Source : https://github.com/jiggzson/nerdamer + * Can be used to load all add-ons with one require + */ + +const nerdamer = require('./nerdamer.core.js'); +require('./Algebra.js'); +require('./Calculus.js'); +require('./Solve.js'); +require('./Extra.js'); + +// Export nerdamer +module.exports = nerdamer; diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/constants.js b/tools/ui/src/lib/vendors/nerdamer-prime/constants.js new file mode 100644 index 000000000..18f26ee10 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/constants.js @@ -0,0 +1,261 @@ +/* + * Mathematical constants for nerdamer + * This file contains precomputed values and mathematical constants + * used throughout the library. + */ + +/** + * Container of pregenerated prime numbers up to 2083 This array is used as a cache and can be extended at runtime by + * functions like generatePrimes() + */ +const PRIMES = [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, + 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, + 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, + 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, + 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, + 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, + 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, + 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, + 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, + 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, + 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, + 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, + 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, + 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, + 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, +]; + +/** Set representation of PRIMES for O(1) lookup This object is used as a cache and can be extended at runtime */ +/** @type {Record} */ +const PRIMES_SET = {}; +for (const p of PRIMES) { + PRIMES_SET[p] = true; +} + +/** High precision value of Pi (200 decimal places) Used for high-precision calculations */ +const LONG_PI = + '3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214' + + '808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196'; + +/** High precision value of Euler's number e (200 decimal places) Used for high-precision calculations */ +const LONG_E = + '2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178525166427427466' + + '39193200305992181741359662904357290033429526059563073813232862794349076323382988075319525101901'; + +/** + * Precomputed high-precision fraction values for the bigLog function. These are used for arbitrary-precision logarithm + * calculations. Each entry is a string representation of a high-precision rational number. + */ +const BIG_LOG_CACHE = [ + '-253631954333118718762629409109262279926288908775918712466601196032/39970093576053625963957478139049824030906352922262642968060706375', + '0', + '24553090145869607172412918483124184864289170814122579923404694986469653261608528681589949629750677407356463601998534945057511664951799678336/35422621391945757431676178435630229283255250779216421054188228659061954317501699707236864189383591478024245495110561124597124995986978302375', + '369017335340917140706044240090243368728616279239227943871048759140274862131699550043150713059889196223917527172547/335894053932612728969975338549993764554481173661218585876475837409922537622385232776657791604345125227005476864000', + '24606853025626737903121303930100462245506322607985779603220820323211395607931699126390918477501325805513849611930008427268176602460462988972957593458726734897129954728102144/17750092415977639787139561330326170936321452137635322313122938207611787444311735251389066106937796085669460151963285086542745859461943369606018450213014148175716400146484375', + '399073568781976806715759409052286641738926636328983929439450824555613704676637191564699164303012247386095942144825603522401740680808466858044/247958349743620302021733249049411604982786755454514947379317600613433680222511897950658049325685140346169718465773927872179874971908848116625', + '1468102989495846944084741146947295378041808701256909016224309866143294556551407470861354311593351276612463858816796714569499021375899793849136855085849133702029337910502448189055357182595424959360/819363879309286303497217527375463120404739098260200279520788950777458900438307356738082930586032462601215802636320993648007907724899611296693997216938989854861043298494990214825163523387600982777', + '5896704855274661767824574093605344871722790278354431422729640950821239030785642943033153793245906863203822369276271050164634206965056233097479117980782641839669/3030306850569309344013726745100070601277982132543905537366562638553198167007159067544789592089960911065181606283478843359856123992707598685058297067179343872000', + '76631772943534985713873427262830314617912556928476573358548256872141516989538374761909611879922349479420014771499018155447198112155515453671128814488139633810493264352294560043912066253026059140653027326566801398784/36852092933388988649396042883218509607503204211148493545892849595498822817623842579026942621098851631842754395231561679671400197056377380063233740202370686144673585955581403046886083948450136247134308381940165804875', + '3159076083816399509754948610929467278257473888282947311280653574634802580912280940686954763313882823327077171624015737719617373932318151594325834524000275847475866299387913048/1437757485694188822758304467756419845842037623148461107362957994816554782989250555362514354661961482939226272309026092009962414616417412938087494467254146002233028411865234375', + '22266067259907364984531611601870291368272674573653403965630628996687370994139884833897773468149149664829922302484782423514167405397665098388400450149078982462318781750661005833037235183394221496186539779712428265837926417581952/9670030144664428565128962309657100138096047028794689249320859276197340398920725569428532293373676415359965773460364494998334259893079003125373872108770534788283842907318071170285038777091588292539102269617376180390982915567375', + '14604654564989239958569331443385369522850975185358647132770022716433280072271007767111036877803328768910274400515590151934676819262085211828028638417329558229123989556376108454497813055/6090614019162516693013973409650613208227889078878781039105047015752493519149314227721984436973374032279421344818329285207124280297611253861173835238379831004010748379874393292231671808', + '1901241885407696031217292877862925220917660047127261026827869027159993239567933534052663335498281439239753018507182016153657409777749792228538380379703411298411623469292891476969894084838876001545818141543890273256985768690847587711270930688/765116019778838839812655402103512685695769161212360553099732689795578904762091216998790589926057819838537805856579109910198553330075924857419395160755642371550113347465300208422126945265887065434116781678702741657275181694851670325469434625', + '139459806786604751793737926146840623607010208216289543036026206208962059593900745886202214788747453279179283344350478734275973878932538430194363355795823581315329311220701640235653288975569812161436/54371368534412517053056101353618694718215711767266376573138772968257303578467926450212293233332401067673270853953399269852376592855992724934941173346260129257754416412476202526978443681584633116375', + '1045669091124493070709683241190022970908640501171378776604126771144008324358233819560649021940145166254659028524319517244711645162132513416238958170819347361185944945680269442845829390112062101255500836072082817820950448463314034677353723256969344/396228259004446234921310936915931611736815598535963504660076315228798989932959459406702091180060429080345146735173591749448509810270759531977278642135591672189002006272326131885315743181289970885337574780897529347356567086535505950450897216796875', + '9912919238915437302006264477931031611447467070103973106567538528951878797932559935860738745374437522819124347510590800370471910492338584284092534264608801221235029062881964101996762011296996851893455828946521/3660537472668264151218961634689665210933936249986285290553357254224360417386515311493310199319523687171757653216994741150377508234317025158302057758196429623723072084157928224798322861732880034847243894784000', + '9263710175433181746575186369318246002919895649622127410824041370079225200282403368319370743363303164313395723904510539050157032684710468364067204876434546848634842333436957245275217583248805993142227630297924119330553308466662488683624783307023014909360640/3341177182697517248552428837661919299725031035849865632511882688786226888137634168024976033652753689210700218163621739078534353578510364301481093730054725078138658805025014615651043313990684347632166030359086885561104034510990826655289288319840595753002771', + '5116082230713622171832327542439052727465114322479570603905499496221224653983960598946033081212909066917137546065542953865612718836914393275681318667667521726785633638189373998191090501201427906618075889744489190209584/1805752553736060443820406101277706970767657006346276183748749630179442318063568286372320188433843729960294965366346522303898609655762491623098453269916163621089005711823488749297418113474056676109581110715068124438875', + '246569125619713282434448566970352231845414317018379160824176638351574938993535464763890962336882760882398479702237564384291290459961036068916857265499633061660562532011248501476114401629839742058389195725393702000011860799793778295606988057303225493814005789533570432/85307063020836305797178273029353623060860009152114361453434032434699636078115114412588719432277441055049132559782203988387794711585368296817222565434951256788867244687081233632650953850383220864394261763844194948389861147622944651546912394593164406926489862036343375', + '133672026303452911046163998480860917119290576658330909785707604886881155606725822685088929236266583416708668502760907677019598002175122453170574729028452721476464728566191464897928696630979863154661704374206171469014225143/45398130975270785045482567762871405072140548998125471025451666500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '6041693953360002800224091673336562508913199995987479264605216252220579740134601435770085920869376641180763419907442721705887169884230643795126568815123647603047739799302562095542459344811429882053086550900803768964612193941424128649976704727183797495759082741166938351872/2016766992122395667828553277997478570503475626107286343497917705446132017125079612756035254750822860815515899557855166824523851779156336235294914777307802256439645525835223691751931866188957324792276149549076500784191791380803500156776088683900346065830066370370083309875', + '705868391597244582764749229356331441978820024796066870551110486625729826111158236686696326058778874201639006234449557592353247542995871491078308187261304930042019640830629526023972693107193897009168955674240659026247094657679060/231848642748474339277532000336338632910990823562381469441716922006107433404523316252618490265927265734670539384485699132080062215196462178933963957679882342083893417545858074378754089719547920901917516016346211301054206383643383', + '101832160604157943093944673541651013907278188571533075311673249923948856034633446617630054761681006062910980371900782781226979391765818325065031889334563981235894369036439929651260587335544056975715076598739977065390678221999918899003881778449092038750712969437519295878491018112/32944277910571666002449086492515464541550138004002141571670657643770713783329063548790202120805341989608877739811787937782240802963962520261844114327432160788193314874913687387269408387417806176202979244637915812905426565263196954203487934225589622864145960079736633434831996625', + '10655703119271468913597640479490594180964700448340778168715956712130636958373270202484276402718566314881119559090842449610957974112230306343486091910217340665146602598568991520563987490686996746558858366002301982443029430290679385551/3398412687366638541233365137084722368200311117891192348532156645374786104142009695796409107380345795998400850838706661851176885183144928701608654514812261697598380070746520197171576610572921007069104300695592751543563472456384512000', + '1903039332876763837419920240543738799531131775028971323439870868730321221615515008394327723508670975623498588291298064320786970626232668956372004004897872810230178526101184611242511193415796638694370503100219710864543168952682617801833318493436174387568067811938490953495819438108686336/598806534367503338307287246320963280558134937382149405305466709787179429317914803617527827862441615350396864359976273212272586892074799651088317544101755361439294687323233086696182687664637422796995789967075271448560870681210580691574924544896656175563265378514188341796398162841796875', + '525573915563826130963525826191411949262846916750432019596028344808298471293378917508549164993368392834023782480702893643486699787870059946429810070222126260200026332874480239090370088123833491499400991181659445914352500247596757005142623368/163278727324937389095822405034435687776345799835442022795533783889356344755225815267819508608559076191292900367982490827396869405536484846115955581043091229202052407483776587687967125885665493681707461345895999542381476164157058393971431375', + '9263815657177858787273494705338516861045771674838057329170239610953039987023429736752079544014780707408666628475997291124805562998227296677616204140605356257712022384368492575381355563976330347792504605666631512343447560301417325154003481040250148561839861837778597346623630046623751094400/2843321709948499955095590862256744532227698001408929142548057792217790532624003190447363578048562448168721539177458065482170148482375585867230123873178100117094533143052886527452665480614620123764036974180917207421482431983407742154634391264619615289225747664532332469783301704643254076601', + '407959339726114455622180187758753007349209016396248763075759257357925636039752474207685682218422721827857994768023399625060206708378433960993946156803948655098667156937949174400873748557248801874735834957795040139401560494087476967548060208243867/123780218751812156744401121690996305978134694678934447237402511116731459214498784497436358160964198336874043702652746834763131444030185151143987331404604087778514863973633941401826334750268416015224906056576641018962863645043976537664227639296000', + '2547676391598917379516698439971914695230548782904479778605691338364453606537643088857116141939170899135026552016969320061900926954008522781162186995856580955090548471448276736878300717869625651893741316530109438876067419826217901657017506157997588944233677467357220316084583383623602865379325184/764562034757392298786420374672266498815021229519853724850874576419885380830752931701831256959159800764672605004880389358601658343203513177084389490286723240185146570925957286083025676875197029662038213216541352875570101363668917766225709569356861275434470568767077844675593176178611021135573625', + '186545352286463730559933346565311535598243666022232037054735807289501173444103692309735768703898330430135399033529355360391658728987379385732098960609744313878477967971557204207043802935782878745271859468248704012618254203101767841517569443555143252/55399179641621656233589820996143825959365789093262978988289445625153099592463372579496245442338653053662134699646413817866770218574795378644415019944304868289119443774932782235638737888469746745621382139263856603239588594078668393194675445556640625', + '664884440164786473344854955309049113269357314957985265728106924238588705533437169796551912202931185746193155801905841712503407258166135075966280435780812714252670362202091663287095423712596462690753468682634261029392794173636943978404002804413009590005984736612421172979101972556772005594499779860608/195485517776407145286424460448995460754674039560651791192647586550615878988380153730602665795647187884543361218962125172808792176382956599256188706636727418572541254480798303566840010217729386905041217793614214518363859058348249961790104618910877813067510758225302884815410347238200133693756493703875', + '2614957283934314904315471338485451166053664494383241929385424599389309215073267052860464009981063483440201193771607520572077231889699858482582363845275452280606276949653970992719332472370351170732899676316967244504534154616036371979031399425846100527685/761493664432749089312665480773496290658029971027686543404885407644062485746072719559288231362060149626237939029641098328278650939665665969011529293869562636656650999759724704272743235210867676873525147820749560155294022488994426729939894753293900972032', + '124843380518493746761140367283007507854364503961156704095198010255465940085534099747297600085903814014415830785663764373057896014399822131175202342399536439284123918855893825207202244831315575594886675813256448846863723093240955901916229136393454605455444105444987028391748121054399538064686074523506176/36022228212051654395480210378626648518430280334458144892889271272122662467638331091863215146548048144675657239846337165813938424387499358852301016926312083940212100001220180762189978024821166744964908871443681332664798940660421469519997746775275873085770018269706847741064037876137315001228315806659875', + '827992369063043155578730871896750570951766628472810506926098505028264552046829097082095665194000002802661600196840639204300804225352337632259980703832713031790922485730615305441309917696044954289187837653933158950774246017223571461858939407386087081525130831392/236805932823686534991153393869288530368011574665859226704279685567723830696754821658770176385138917722808377962346690757191122309876922069867472518117628639913077442806147910884267694879089753138429767401700283014143248445966474839193628309668702223994071394625', + '17347276886878323736540051321582548724378497839789943634071026331001588645519865992773157565595886250230140452154269197770615097377486013097979087647774513500701793885978192218455687078883766086309728287172567466406449372659680040183273634701092561727514713494914793425407149186041796935055187281744386432/4919325621804683623339606849970832094714371903709195539440424738973575902329797546592497378000858196173718145883783709223158260700365224756081275272021856393735663399552166737690038832550853145831185979094979556715294990257315369124065787473707136464772247917156232366320267601622617803514003753662109375', + '137984231830526866236186357461458917020538108058615632801298091031540729111527734872044790487396302545910108285921421417358113055522725197998483383380192391312304647004240060970929072498293210057120617332323445379424867965764749534125081131327565507524502163460761/38810445792642817561168950890315210470940006613819790543653745327778579787694809782601777514116858514049585074667085399925278459138508514838268321349069481334967221455722811414399738756151414906092225265355449011152267068726417045644222323488445626292574879744000', + '746567120547823334914136339633766098626636643449144032626270358619125402826113269699709721071135471625588981126637674402048519990010499180844665151971356149292818375448504122545400227696621572263621729512461528550588108384619064912224884465737417596190735966915167530332762203074440688676123756162572829692160/208334337057923929636884170505570363171441147899816815785150954417598643614152856767186132467069365605496210036171429712485182162940460120834349006784956522600679357307849981862006710239311750261522832996877712350330290831638640913932265004107623954913155144975252743257846945609734368518424172846119306643431', + '64649371728330695076928013661001819989330953381731372450140483779536126948957993261299287753791770622512248630224724990234903928056275080682537641377393210728546364176267034339221558641084730052304770498929958838997239635790469536857863963589118888238069738647239076/17903951498200212327802847425913723358452100686246224008745414214690047078122925247086521362329833307849817944645647750649290248110509395628305970523384831671737569872597295947593410067364379687588919135621621162007748635920864926867870502568935739725312687094047375', + '2454918942158003099688922026016393688092399295166304634317616773083386087532869193458590448918958337530406410803840837646465522656670050113548208618655070231274778592766244282964463702354872753657766121825196898916725498553882689210280080206627916046484942827487726300822318764058084323314109595329304407466188383616/674880185931325925966586583820010578979699141814417326552629206140252348822939845006845669570885271576698771404162512001549922909048916000017837898649100825976232784446638776021483802989797501705685620612986771521390439936066527738682396560462899753657942715306792783283782238662155922082005591512296007820682995125', + '74018558041066162916454010680594042518462756234254788158141115244349044958441521749277686851928706433556285971088455226217644009628399441967508838553345152310730562224910795446341601049647392069373970101491741830623078126344928804029524181578945586663110848142571149861/20204153620006780689923328634586091101021423979622170579036140596085566172775051595588438592742563923428900864000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '2127032036758045513335690185608563023954009095206088224487365541995326714285119384743928987635752931664240752323937321097955456543854943206092931247498833001499955456190701695430459583885125382086777607021670447795321669948733328973350279846928613949120929250312666393359442423066212311060931469017737106028339882830848/576612418511902928757340062840968526862381326698309578771238715462180282212422302261044980131594522407066369222998903808960617461164985318633518680304995784614308979881735537678182134128319596636920719106506829571072447362052319438091347699720147003209417806230149598345068078717948025207635448205253184540936478445125', + '35289653975561083576641954928762116897061274899517309102784750384002335187117263273488751066569234386120759866204372398611196356888479036949053282301027789530999737306501029700128744408015642211359442183943916106790666114870974212159410284751571905275610921784716184508440/9503006066880728386808143045924119024212377150217533250562188228062174064693375135306438120385877320162710918716613546077156389583384656340709638430674364232343609717735574035535102953482366914421205216675248471695111720986346092738728929878538430662191272737183832556131', + '102633551023964794485575491065909467125458972250222581133681080524371507544152979467328048718122409841060527545925136196267751819689935599599321090571687632103850847605493223603751038996548520557330016046032671961857623066292962260173840972332108111505971231021442896036760967107060309991355545554631003681544611731245475968/27459658121882266328752886605529964804078316737648012166874496015808620265471203512606463219297059547428855195782384236337998738233668399173746663289852416697917397644234441300570212555870401420579737973722145663287124151049692290432756231390864184491891697469874600345958989433125942336757049639797225309327019275689074625', + '10034393558388390065766795008210457368713365491566387292163814915435906649268119060550511145023450790393353937124495488860451123302412204483570913557762460385297770427946219119911920640306914453207097103853766023934602534502476962159682750262143380527529536498215384467975023/2667919902603322771586358077760621955455470781865624844984169443739075976572061827709528710108877015489050369589117491611045518221354793418884447632063538994046714401229510497599783726376490260140723032102883617341970952663947646017489439179953454964374887388652792446976000', + '248528145263843375390386172800048509380966183384567983242213959113927668429802237067505890436957693495616107089384741585283620097982859345081736730899912519273262934785992235852866637878831878448348444611412764161078458068549719800733237024285525816723480868704742804077255242682077291713092790250511567621735004237450946304/65676865669148624809340872151906045781446981664561196686217551358486802274698228825404698950974939545099727242259547145392352658637333562345477931951890984276718673618736565926663528625796412420753961231404680876558659735251469326707567479071881966875336951133475135427640218972722939427821842173216282390058040618896484375', + '7805448718805635696495809414501206964843262114470109146341305656318015059743127114324245035489577134938579856003956861881125856595981500593426840968087618241785931128978516340812066502964561231235073012672356530509663384739132686548934288703179479011016719045530855033205271548/2050525178024039744126592505352202216905491833360272553169520915020715464206141942151086176509423406413311520838568324134077402841030113427309725873344806030836314500267104070131451720947531994814710189000076651895520222646974590481497382830325485174899169093049299764813276375', + '3355325071293197839434119105039673324264765809771192815982246040415580387729382404624613875653005261578877047405365032178619450963731719777167015959920645055600439987161800547901539269321100559393048973255388860193948274255340335876890491746900991668165565729269698196233805991206691196045182214641935483083662356666996922240/876402579119117579582569839757462461050855174353108858954282915644790659429341853404829661899850841645529640454766173209897510988090318303454542547519850473808789222552969933222203420847859171250332350076509996295844203965564448154484566493395403967626596213792922784509892086361572955175655987334882030766001799867659814117', + '218871061991045868372866381545267589365410350294028138778572466235486397478028823720846191998825628156716190463263492304639890659254282445466806224943413446008645087186307985343574807361972238230520975439736199291019544576443791916302825193643774360055545186783819367378492631806297/56849560726416896431557940314760680962653658127458002233782028041537121216487790008085876994020812492987733987414743604239935223783349870516284048368761617736127892160849065895223288023531930411718807065209903593668117085505482007061969339237404945180379460053180570404846043136000', + '5008685108365226931582937964451700746853986170633433728409171904803795018146152804690759530990140552460596075588463394200510044617816085275660078502126507209302951286606953039953843685800941558212440519542602092919776366067720586295390886070120828199562643208637974347390938772070049344991272621102622931576339988103674070876518912/1293888539680354282541277646947380627241979967611883341823378331667976045287311988103163380651334828012840330710760757271860219584371109472132211215957402251594055009937397184768184517621978947384029376766290498101728971145633139541827544539988344772578184316843734267915665730981857376872622787627370859411909330227080697966353375', + '15388340113525711660227566446101909585796746979396093776960989868457211684028149502578116456785221720682202816140911944661051001675127262774824593420825587319436537346311831003212424497488485098543512314062112948777572038731823948224734505930748371522309451168088057190162878224801232/3954220582960831691377435160890656173654063611768428458807273708040518769541211737927975894584024448193835165167801976423275767590502552964407494549049777006346189436817215329891530811451811864579644894987864267389290848598289794977382504890216219362031324635609053075313568115234375', + '5099039333987561374222193551155323470675617979816941646196895589439391685938046865391119484510329634015275893520725135141878751153360264368353595348921951280561029028912953500944814771064409611917475818956659775131751121312316084465321917769679881052144364834485866477379437705913911371481828140817759401117780199246301705600020671104/1303503600297679371136943454060319958680553228879031326679449263682048703103464872914972900105569835004878963701599765030590097739639045890060548760692125546754294514068052902543220382104483822438283040090444827980927544440984823535260277595466339403795403200720622852069244768910603820007632395190204569927612348189089161551951106625', + '4902837141334073026145827027361937996261324349722726869116185158777439337041263482852376194988371853413467559557923410949898048139830183335197992754748294810838187068126867611615800383834975563313220497573778480109264178673389149671194149749735833378557143135481387904961537942569904075/1247045310545991266291285730016853118981099516935251861146038369985109288084420528171217942065832292739130145780833406014673689119563698528225048800794718789218267628507713621235056538202070171596177775095071513194885568843375526804796016261173388452184505503341132236719484809714335744', + '38114743522716832107917466438257616720476488812538316101658139632867788464381862291240727309611460187159930652186486096300862388591521625093237019662273764387591494074792574929490381910446287947994150655077877204446864004067956087975012773988833339521775463977233068498404144221045837190392670308437391686081418318624745039402145439223552/9647001083383999453668111809775451078976046488746916070976218645431946648087171586252172936600115032316383427265217993193444199863138429602138841976586190525451324093772097241349417938578878934577091671046050326087898259692917931230974174799815198493279413438192301437068820185757869608523761456160341754512329264442115351926967120404125', + '573695055225225727008803730767518906490704995929177617646275646884555707960986625481944101622708415415988844740028718027554452662358957933526173824325955904005404113684003841990198157072540659184995738719040024647370869010473254071681533880576462368600901824622431045529064651675640055917092/144509482511118816399089096021290587489594541280398871255876563615464628718527634679330291741479135415168539765887291789615790513527330600394937614433502341116068305347468133950204152174094704092402978083370792135432486240914953928188835819767755172666693219213868545854371103120604946200875', + '23876960329653589647925126180903391687666378233201794403339630995420215267415575142266707357255726330536094448314199602616026935251126469221925945960901748679919435908556550271504767784553484434363646489174587463466333864577705745452492395785557425904735048180164697040313528831173448025400634629163795223739061661461986923675833880378496/5986312408594306954013526197465608559068621248896320652512228238115589875514604632230098997609482248000888567135685167138762172475788060284232459813998201719590208742091697294562538265829954186149162974972471533202880368317237508987477069872431064075005305838801862900501819963793062041081601844759452202282545840716920793056488037109375', + '58168289917567723171226992383559866214094157894992327555495441698028867727845766488121900626912848698952863438654895252811583144479300382761129433911280049009362667380001406579175563745824368613319103673817094498117944856004415812877213722455299491145649879676787079744410765053845551958756701/14517067289347903655500020160671113450349743650636953726251191692074385521975132268313263723831804150872238173602847065423463131917373356798750100313145228608894881457107689499956903046984443545789053438946050974567665049237414588435796381674590098629779384355275820782532479708807512981504000', + '728621890568281859295409481422447012528302594365693410763821707074444799793690738137592101239862736313347273167450056625929591960610208335290882047413011571781161008296084630072829079783328937418641417642857196346026366370059522990813537731394823630207433267854616768658990289454635793326766697884798538576055949457122067828153655416688640/181030730759516991863708593747964787874073354051675597050399087612142539517308720603687322924426591889179726492403913356461908748733972707460063017057809060190437917851790767968877215795679844983288935075688219234885360839984681619084834228226744165610073685719017596630302462070188937998558312507638434329299017584329479516410907786681093', + '86855946923438322218622470067224691860808273886184997065663554841573982963995340977083049132518812923329423480393306918856650577072525633920456721265953575424233701929892019410099166322511413146891121248381648145391642571638857576890568882512129960291171866772665863159474602604647289052079991768/21485753507365901947528588896402264670781310878547726104482740647554738151100954835784115119035980523529677083504495839730499664052882400915208251594384038810917282207449860876251558307288700200910747338758723324686939379138206117634546981163355060740270734146780942696291669461182599512320099625', + '2158989152301022938148680102142188531448821359505188055264665167313418619665693092337665573150374231484840948447637297247277576415460889296724813940128955070240137590073233263168835678714131062764247434144994737610229909964847568491446606012581370840699582055341626266533733744293929658949697805855362114229666626620766245630122333733703618176/531794915405164005613733454597931482878479882704956110685223892325074211694837836221759995948610212818642789132749082430059593652854659130217225506942675608692701447738732031302987802196501895840510235161825501235133794449421919927396142470196961877376701957829921152848178076410141813926924749057304222282687697297216661687583257901415465125', + '139432548574396829074586704387656697097760057897628994548358619815052936481650396157428747411173567801047221928593253479330480454469358220685854351236980383914223693722868233819483137401339800304943891968050399345430243790898955416907228948287367356990263740207046902209563417267686591994743547621/34201151688775214071963206765436083445901621442002061707492082843232231754829227303539041286301398668437202547003300396162741375435703188500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '11008517174872833286150985180322584448162884832099344969609291070844193524816852920942383850580217443209402836100467940651581092350600329145627967515818684442171571156446321228596914355704205623857871497315955269266498229823278800717909321269179839084452384509142712677235552103459737790674103994445173074670347080506698168482564009465276165824768/2689223396936080856855299215659204161946704205931885125148201643087176556822542895325191478283706585400237901215485150928036895428721912118467760766508162631903585126377676412573187912443878232521444786090510891599171741773242011017926658231638022943018461086517502584854390836347781674626615709751386455292026775663545470794167629144456268750125', + '16108638074211260588800537540680707641986073914251424878121255234668558067988171568946079848860335948991834525552515669040163026131919804987340113244760738846884911038097907756220945883750502673899084880578229601870882631165510396775126850307838505063922101682333806284668762825609556049426829531780/3919363961344261777100658318137884299575193089462944554282218278496298610828757650104922583359642384253066896538203596057302203635134833545580869871333892935330950583664400555463557735723364497947986885146043017010159347046389604172186788902608216894094289769850517098027486468084407618748895626853', + '34420755849180279597302103726180110022640946692592540634353734157479505420320000324260530767186132260970572450489530034440214259559325114511265075416512316229177952140217732655405289808326341696986755141965043719344169685611217958619102774617224847284122901023774956887687026904767714958090256282893003000752947427857703259704682455375442735857024/8342030311716679826889917494957593165464748884572298173556257652389845294530325764837124998293398445804458613956489096007564811101361266196542129764287084823604897187311540561857741285793447174119667215803837719660675298308873496219385226998078648428368061868944322478384684509466965129972030932418920415308276430355882329457342937549162000252625', + '345888075261020004071220843714060353763382280664960929903544964118831237876694384053904571498830068831026644303797377762345709976595360421502594656308937649239978525713471393570536680412814805076323426256584504251728507416368609420882442293831684681071553766603478479006495757222912500012444787804577811/83505703731469734628961395063481893801938371516752417759131774530720075262459158384433785006689548434701904106312038822969658455364219435022841597243178757423598248565463985786213156556523685666430799283870548238467817226915680747412191245046634279766450629886904716776719219698922088211154187845632000', + '1061717830619177527082296723099890392273896386613997004874669053445943252046748251883532634529759169500795452576392700472771365240996842610207274128102329096619028487369622001737128463631016494371635687841733644339636164570819431573829173533941056258744442930643735587780907310433371453992062647737259587563398111688659657406089003293576961475848704/255359631537215747979895955806995352799574790340218399351168178555478073997876110889483456972687438702262017800167048243754141722496276537685853311434069991222324039005160057724073156957530106623908696241268268096879569794431919729620178375212905203484165745866913773304319069321426245521467122472046370356725530914587807274074293673038482666015625', + '274122944106300296738399632684955400761495830361663966466225652918683099779465438024846903286816813856490888796372134557295699980528187779624865098445756013563535339056233912394908544185885547842235097677765325396255649207317018754967666450708249125316192200151505568416495274671679500594656671785202496/65687592621976546250581560102201535533608158256953087745856906437400149205693427285162333502528793675585022025602144243543064185647792948495372442630333800126269123531636800213405254045262127593759539706750242430153456891792533267948231185296091297979933562727112487057234422009426868531651634706262125', + '7842680480716516803148821198697967237136721860017131244266974996267074742248599085253569637183007740566941125452215834642683053334607896723447140851344501084122965014242091312411884985569341166545074688756440728922408743841592658677792796881188604773469108807869960161395759837407978596679911066586626885830991556090978327508459276025943279064965688960/1872528612245648675720382138045071131304652050696842872529163720558126655075937845539792108048310219395746259570506175902206215101518698490144716531697689534559827422735649881381597761684154409796315455445459537515308174919488497154409643876490472215352056502193150125644288086294418253309947229151074464928874881827227706992859640236086417889990541889', + '5300824422251242070074569186825929119848111723012841627275830216301188228660779008353049603527567784119877706984722171178137272986345560485784907345500893648715341273841147320288851034078863843374665850852481747000237834238703248634174397792745914847774297223176674917912406659831206869442510948965571661/1261140476013707338477604677428573831791396352814802149994640617701773078174882455512668089072441176857892331468691160991310474734143842336092636848492066592397892638052212250229129355009939118431643425836944282456647571558383755315238500832868535816144280088644939696339160092963629012001958205063168000', + '2220223718762215584659309059880106334425515875615107369399767892051551634000614327272260081056973863669004224981561870246078120862256383581012183852291444462730018546753183156982897386563561418424093883164027305254176874653780425452987066512563140531367766900610414277825262239199580925879453806414860409441845631158680721091621460775043562065815179617536/526383206607841251253861841374779803798480623722760367843070466043030228662340154304405180907941079883976168609082254331465595267209149963786388600028701073430773581228212441424400748220833542964971495005714483235359479470452593264280645360131482713147116366500300066771223383007216182988263355451923333319170174755334598973202740108032097242475554128875', + '521891797109626296684891455959263713257353500867652268541535940159815152120871142196535233326890353914761242025931373491906127275561002910157909306979093246574207104081108188995072105948138299097848175016082947174156278439986705241571619793059501724269644447572323501261424770743329858038040685313621446524/123310256826873923765604825413207481739886340225713108649758575106598510022338480189649787216845041382860899099250547657534972156328080736149239332330143771138115695598493059325064119176038137294863053148618656356436332991079150723235214278848602671333076219529535123842212129829931654967367649078369140625', + '66086044538329677372986118727999622900471937619891337714357792768200341519193500393739322894033303245376225584865369486696276607060432449792893028061817203932068085863800494054274423512956136695211796751845295921015953538329385253280866669403169919614982155350899648626481405781514434761541281229159396787287553493046927448595964103589100429722948913403008/15562137339474350565671240515273666798063901504051979980452491653975250630723677279081058884163396938548780856293034775459223871281049026140999055923743471466471830572672766633086347312178711643724485955576579988182546105048041649947277672869613992334541438784737993706482731696809943027528882927942967419447250586964258807454003775693567366165507144866375', + '335377615394100148751647837967017467711612297170079949298328061159559939969228226474615711044891085626519877634842694983669611974807129333052471799687426665556738316626171408219730853872410792831871526174987402129691897433888027072807302411474690613948951673562473758814664346259109886876538510453475290967835/78715592752271462306588358880337347638000605031000575876214116610339827495261512281635361568951675037834544811575026718101166562072917855004822606752296233435017284127594847656529606648345533195437635894948829857913798336356647286032372695130461573940500785137424365840081503133157308796505622439791698116608', + '20090879701618729602554170716780970848925039917987945471322994867171660307998603515745066411687983450400412739285577269751603921163835619296822801840348319742203974023505186187060251544248644338412667631232247108675504629538319425769464277309915502144443973397371136256151336255138506001292355330875114245901820438821732843540725116728866301271466614762497024/4700223519410528857298732096729483544820841497820611795617923063440946097326817340637303431283005509904481323205480729806879570430868897342398783028649633951362398196137429076844504529051072393709154483678349272930361110568616112723747726853614661953537957117231900032044221535502745676310313569997665352252492568100075191900969170979460298189170486601502625', + '2904778979985524171206573028445379872240558084236464200857594814631031581387804621371822074061289363372523364167184697785570324832815972970658633551879143187709707164796663015180877412717910872234647704536817108676736661804878068078543241390828229923424191204586313620612539678930999769543756218765870513049986792/677408099044823641581658869221044375312077929976719183424865834811543737800956896926637625166844372424044003929341361734886232742770909683021563822987505236295727478159938135467975522336774471915167606673489722102077041330652185811196423400701795791669780695158730756241178262962515917389382302757366325768069625', + '31270155809329751863885224732454397292230969002004953832354065319735530624996254695453061851449600345977646455072512400760539747054003851289540339425848681804190284451253462663731135337775088379954403740058084949675460445909826322297817535400604180338201322667139062500269285493417563095365899631360901732684124930296643108551710704785906431324876072470231424/7269578038000504017073007978844992319987411732848567116655821196644382777088703228960020894756722675887473977480537577509061256138261063926845643360849217556370868752909531088361229374467207196928745673402380473721018157327193509586295879051411183657185176812738231456253321187419224704301236205478184115996135940848503487199394612616742961108684539794921875', + '432538822079707760382094121020421735679118830363764570640789368235407853152380328891350816400541189148550353337874309885334920995713154225799660601389784410911658967499100610376065640785585342035058364676314084595283850213942576431310823836792440218271879354669291052589804956435743500204185107215929849054782893113/100246574739326291035824954677502591279343311051719151327066341370995390423713403739043396503785261917771859220535505691760472395306543276314938287868734009582906895763073519374099272340577921671298878837400921045252035507925021904954445172372479744465666760762909731237634082051855588025732494461939980856983552000', + '1914333673689206389116942789116917579088664511118582610293383428712902211612554212779880638065888518488492298586641997844141510832940409501694726851666478650414191249534733087933879981733561565249818572204932715347752949087800778646065986244657260832234479202201129845117955957242616947361016603702640821256099895469088229339240402478576285854783063612307200/442333801076281757298117784528962837115323835962460661601905131618341609371649347131724700192551574625400701487125984359494804828935577124602622582550626336986871791407398609915208114339012374456785705161994343348351473385887949809051796407340988735853810174589261300681030826392672282630613354461927208579993042968520650313621522933214063366558703422757071', + '5704691626402072213006354545292364761246893919997531024861408248746241619817955824682639582830486790618668221530365426203424888737658778881721063941495350237419723855000515747725926735319471480027293210991869255971365522170749568996651406002311020883635577590045650037569906001924971041810547543163363976464780729932/1314240362076792592671773873754757443276256223533339004339330559325754574023619698171225777585408160438834449576526997055649849875516310105297894855264038450585295422343454458568152668980131977005808840141079502436391909349182185596381509091427752151860204235071122788499996991078935216686010888734471173124487937875', + '451219364084386208718456142329444023337343409261545444643031014769484085278440612677813682892926852469540118625738238137458321112005189595703619178533263706811689687213128887082197330137502064260105387500552856851972124172206996205919556553246133218441769325133725631665593372188755090094541462474970525820334385058333756591222492801647110594428922046641945259392/103648250172203340865458115839764297558925693061798169434516001775068769911768281084188883278842546791612199025413573394167639925287068809631958006622842716869211374513136766899877504136929177267362862319748507372147243911522667591375015611312165464514308451222180272935398828092646393830572838772085366567154646425598388620105539619174837489536378605144891769625', + '1591076564577634575701791393842535460875733974464805197283632670013516183281542903377750304419996681222758401497321278555686661981435637461350320471258386388843198706277657208526372100698700615835733712519332548607115875288787602084336341594576426630670911478276101702119972195558314357975365863803265163991961173/364433108410193393847203348728981296285742202617988970384277162225847256295865554341611171460436362780497179090329831885853324392923449579538286804729856000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '5307507148709435807261229345132535134848030343901300324623409721698217134932866488937772986386501538026693956121121771089740095760486362654754229904633476234090792827930433598453200536551779789049352131005222659995845427680686677324963223653949277037520711609343166926627713758990384011274777087968799793410506391884872456420041494598273703914689586637995139580794368/1212244290381524115082005575105703496583315188540177702780216570265159923654197746388568151706509243638302707511238539845531608313334248788980188054651601536067740709977678222790481348003085034888244447626347312094586389347535167903408519024105748523384932942441843118813819781347089702286481924493361058339731949844620763272778960860509570622673048786968660129650125', + '3046577098843580578619955603029073328361298436129791931304665777036152915858575282362742008136721284817899542475666054101957899858138590963789072530710775790433466847100422875906866496318804986202089528198912098377828580031852152997907433335176267300286466072997014023120087988748396598176622765923059385876855303940720/693911859077752895978833241338902695755528613729508555938416419864772385336777924892434701804078893822446727762077537955240605927550548046309823051841326355655848406646248595628715185413852040295631448250459076043816328082561248420925930431777502622148019371383254316636979070731427737764160145097293260938978413488819', + '155310926743873343426312607182060072939030765297630534544899230213054714482456829198485999782086944271490229412707980997432528653509945301574794242118433868747172279224118534460563400440670015323324602117517342201640016853927923976796453577048995243600967202462466681112804476672348206491490513419336308575442086511015101999847896197546900512764233674747415844045184/35278162986589659300679088538176070649273991865663041616088851923111175518405117391134863644540911005782372831496121355954470744169969774540892096320978686548284501139861783292226140413769665461494668479430833892857992401391262903582109993838728281915806394747833588629467613308837196269424421131934859079260185052081536487462257809987002198437182539441349474426375', + '46729591025621874782758519074451728476386657576036360734358719976400940301493939192083339293779149127132651616972817165172116269307276487158069293114860391502484125554406945249728802484128756924044633825692779783425628292959170597009935305429239702926898931975023598456207165067568919757902764815108751735753431153581849/10585868084079030838651390738371141142245086465033459640458366146849314274285871375459898014414833295804139979016362796357043372316321872357817727821559232353993714062535883074661734509440994664726425399880995133711038483607773782532430879196405793694658185175583691180757783597895616920432527125993118171361116684288000', + '3736372348124144720852190769710129461145889011598636925228657393934132828633132357232883470466940330848177425542748100211498184494252714616379450272611850068867816250209867530921278645286769418080018709947826876461419654782341309127709703626401211996255743831998918894661053669189312375159058718767082163156988766821194002596331826150321864927832618126580509732359424/844176851007504003627016945212023239308348428094023437269532743221937069345682900884618378849283002998220513307273333096775669556093005637615012750733216460458689282791632437851364674879152695438589692227159038555722539345972833161146108367329370564838671791241431125003401861887477969954740544092178721327724620450947646209914621007186497081420384347438812255859375', + '1258774755828991281578968023382624723772927642002016270484090409043454336040857926581316994594109169123354553321469500848146015719851609220423736153365139804086413284787598253618361769125996755159571523632747129480387254164008968993734442164892486441152227433281625391753702577143985047832519062595123255569172968685060844/283659859661671181526547833415653453506477950678651675193210969173130116121017723360258249430884213011988678011357458727603413521688184521573094783291496368005697481333739504490647194454695504722542014845706216224432373442438242355188461951883454664693262684873988061018976711201351382163527093784753398257094429403691625', + '84932063355292829988908961192574710493098897148701473172754949846455626381329456661808566365329266898990829247446356970454502007127269708487563279536825277374133681167235811080298134899629580318813382668399644553111080625918213250223197440426147821225593304993621451053135332451997633132772608233430131400186571793929377129211228689703376067763625568623535588709576320/19089978133324852910950469658566458037096027722326716800113107848115231563787455584278193954518442601810776347091253561956877155673550458955562102935555510392954425196165785410319126098393353878286400877305164869548380670204577544630353859009177051698096136470072137579698473017257397949994722015089768745013713383769765609613514021200888647472804720456494757423299627', + '802638881530832431828249604040579750916118423833791608589560402449036920165704012070349537114920882938466635598602387718300074733476150548724726460209016834416094317724261857969955414000155807312852092720310159572547644569797512233899495300028159721348599816083166712365215075728968005941610056018023633235372936903015771583/179951197386119079732438617407921535065140503043429174394605652913879982486051627760652197484142547447000508189455126493868229565647284332735552462525598465192073558793335913005459266977086104359621022691931002488052727597513413492393525660272900161375677499228252863529934576881596384036401784035248649026076581302370304000', + '25208742399375362881099811032135575360109715964024747212026245529087599633280142314962581193303683759605084995818253124445773115574470717199218828756449187055537877478033129862600982068782249943150019637186466260707552416433010545437321814115233841687700051830170191107127799355485920046505591193770164750886037885397478191534797655616745528343172318318678405576430544896/5637713398995569614196397857525646325234056219513202928587580534159596897880731043336790273040813044077153051260989730372846713618900145444802234629922717464041261370803598799826604841654608724727320798324006129524610666235998113655193642594744544226880944882342204407750193512869672849910003246504052298022468012594459974209940607450480609190841893267203392880360823875', + '732332637178584560220688900268566130246820235956768724845747830959547501950765063982943061181526237061809052444110437930100210105274824607344902764184151030827266142225894655531497849161692760385938786736436977268616029862577293984376170905024712952813234949508186306774257035535805330366742322777611063402455261391021097128/163377988152179636922409938163005948596822656868040662831003991395905185823836089887990201522673196719628247106777881741843786365701135555917263285268753605514812568258179391272113405920369587922702002732667842511108732068683407168090725712401954314293193572654347237716691784386690948494003094992876867763698101043701171875', + '22391374854299462107923583267570593886002658786775211597896252879708753450794332301142909715845151746786016535157797023153041007263258732991465037648536702217273897876864351559736449481285518249846264600935543582562018874574655740660800634883403597181876364712521253650744197321080655028374602772696770227741610874399454362583980465225235674816785988164157351243193265401728/4983275997188967758382167867656806681565521964135131710678447245984239733544941881418957694612909599261843693554043504855207170782744518943778096768859337980470219783210191765678908537645270392500777411134164918803949155037581108678968701961544979451081836872425574123683574475790997414488641534074873248802450108535183413970057319247033379016606809226547805269752123746375', + '21252344995592269775107236774689012760750850598567799560343535731251766780016530978663079386453430272698006977668063208502408607227544322945446079708000304179073184745623740466334127867464883762843346619853325415963062181018736410570113171387891518398846617197097407612852037905687899800932343156979591740310928257766081697645/4718515378484509142377558412184183991357747235892194234377932213613746008373918923611236346330189287336493794499188640380975364991548794341177060325509698404571318037298112293926833877103554546466055612498927474225619680186119313129604319096374924037870803554153458104225741255753314955115645304948564151765516183663634546688', + '76703085666560609319365659209445044957052359500745196718269665234646121134787807938680787341023203786904308047981099228357850016602203539979246579454229078497537148799349353250167621935384193502383187510928609818011142642954550330069991570983902841318203794113898871515702445720207144863877026526025447819537396146822201819435536435835285098485990260677737192230985069824/16989892821104122916312992616665764943723222199277412857053896319814438961475117951904867066845412639236790762432996309717924037467863024360211163971190006272168845197000304328480920483651558672879967350982199768255256753110375467976246339260326422891913564151453729285191351273342206198366624882195135056176502724912849575385576554208844814085228696838834665339811027625', + '40153831166521391225489894857551838468150576827129133168972859441122728710165233865309084872159583787083130713473702296122465319276461991457173085312407612757280915853584698420083436946976844240063731333638150599017886937783470942007376523947840104246927580254612991191040951001539641947153847776050138532219595746056076776809812/8873576113581065493273519627544307418139908640325227196912114520903505426722086265723900326735989771660987609604507503750212298754414472822477243293638486047313990878348820573874809636084574108909615044524663712495422565151340037564156235745628465172219286664653343193162657374410926171153516871765680561505768223660055617934625', + '200736374277835272485186523480177159453030082779872429648449412313247640312479214261681899286513818912991945046297334418102711629933437654377760028476767482162031641781499114859553677502002901248124750539270235098492377826240319766677856465093871367817683734079621359186477306173620331748496369122143019303740252461824523590096923561928354020960176605008889579578495073283712/44259440065125442964014453739391594153290923937737136823355312892557975399035370631521014554980129291582486968083228888083079118116286312583989433037097397252506140224404130371160608365777625538148303917306340620019882928744151294738308047800711028158277500317134537540772759486365658024194428796846364486187105343266398338060080611643110071184992193593643605709075927734375', + '488459049325494693259159444507437983381645757291858092983371672334043029615965882574409808932509285079401239403272414271652617474184321852388397021836909585659327974611273820676128650810907598106588433939541654215766888212287311943387232664417347883717203611092450971786083806323404432266197250919880225892099146722474124874256523/107455855466267410923480140898552598306699341366032095904938430084768624148829473848763761936703072253841751966906142283640379336131083461646777390874405323298406517250231389493084918065243079819898691146841695350589992171102939046740169198394671965069747042621265775948217054519855346617967866614303497258042439052681849864192000', + '8430374068596413768975326329313648683222744787097373111477788794692418932056901235174019333392845529821722488982447683794746518712070635063397464904489452840169472596494433874412726723065560358405027764015273789053064830528919091414531400589850704395887250756457780868677553068164933299802944396576360356896758745030955907258232257358056931680617626606715393885378887657377280/1850480358582748412767893656294669486390769246349349706200869446679212812264400876685764762211659843658541567071977813110496329850010288179702589501255367345935389050373021591572539260951149696801665953123597521983257852207280970577287472932167362521740230973547070264273850381896786822127290397075758780174083415923482640313277341120938690594859118603017930500014640169211239', + '58273615882491925540881784328370957720539797010816130007322211512070634295154626813477498607761260613353633996451797686143131893884559909668805628269959901044961666535533876340116728982072626875010391195372759135469446048394835148654407846242721443845351855234347422256700864370273092971804207514736924787577569355949601385705517152/12763172138328432984573837529764902730448666594097466544652830703737195804747682450548488162227364538052585484418491914323658215406327363805353540266048943197822838925184942600367647278764952826664684839953902567694832690683467635727409976388280092262839126779730168889686628630302519063225007497707028600865624133968363921915016625', + '2685278694947152969468407055109959900160804835228466214479525891103128459065131221736713585038130737099067040196326815283171857977706979990467444622550497565521863441617001097354649972660183038850033950647594754644997436624623325003537308428828750008305464174443210079059253920383411910911594636045517662796811001285984447671371943267419151220518726940455756833040934506414133888/586864816044968996825907488721678304211296267371936463272955488077666796280028127333229453342355554450708284934221461946991556216095213627059047984199091625921432110913096253352065238765240063555347292393427726758010888546427415276046238297382474708612526770055488323003698641493939145624184943388159614111350839555141202024991876221874110644879676602924730506882384603409121625', + '27700345710264347957758638741952394530538598225904772664391173119594616680996031886665218221392453628824570256438960349220263575741495285780845399187013582169907518462437966962923592601721119258663490655013419675469864809004562272799012227293974254329929404036071055528766397079743200179924243479370594973626764330855493789112414191/6040997839051213541001279276287478343874107660287651711609067205111574718442229224212245187655300348484144001298428958025052162253854676210451657425437588244676589965820312500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '80160269787574270953020489212619791839643207793889009503234879683494928396231769167082355143564723274576166638869430572977442020236299319718643894871736539109822173538266278165276591962215927816541165548699380129014840956895931285215480350321259054359835472320394089646764361802445068798451796525897304221529850133401768027469126408090577121172238802281079916258951770135485696/17444957084936455555074876721808241311651668802091828589911310253709517226715880722948267977560381436307152342821304525073651379763144281678911047276389231713860392794975453518107055568765009486973017341479560154792340888780337029481646046233967923289468141293699236815196553819373828280398488966665994092877680860643266914861837639322607761804740256920429518434353839878280875', + '2352816897072623416220002134476921108405735955266703519252095150412419264317091785317803024028565851487861978477208755211589867803009700996828082850796103789009194767813611798769297617674631277847910238088040257037678126316866517601388289837575778165301828137259948173292658462765645645868094197736382683775415645307647043205988394700/510973411316690313485681833991645423802776865720580280058344111363363091616558230182669376841383375321912553771027107891991318313938505988903735114191313214327683449514676737796942389784957658395806951715454372568523597420866050825822028817422805046287798054682484158011143949412956697442991898914560191411624040490122951328364833797', + '6403295584873165688372907494046202150046769667837790834896334486679541887567517050446119511695248926941383207478170182650927368177009669717288184903306689332746127845953193587519575304974203099873732502605739219028995266139383163062837007982999189114810534856227848568800302527760100163350814120545587074865568436789021082619398126713943637898657861949091545516403987546145915409024/1387794272010111535893205703999712437783041553900341112488641528986385462810448493444968969845352401058333929711237978223214186693177251566069419805757440174840170213159651962686467523533938145629444468366235554597245713128812532716616087753947246800626006504878203666972651384731498770435755225220796872155249202960801768048854869001310722927230237083418017482134588865244642658875', + '82993247683514419570466529457059660634483860665557779709153549045427987672829778520201315148149878525274005978368939092115193636113741972236218502664881450367443614971109677363668874484696543982239492409231870942414193419634675024621942196087473557914167832058111113476295926250739099284241826553737074679953551191767148712684157318697/17950946423927357725787689855263532224005643859095168852729513034456208872420513601894508438640531171097082516559962755244698695622824386001219435651555513795509616906355363573638916895074349491513539093024980575852693293474288638209680085037286354050958859425647536735341886663074581909148323105020337857959651624760873736590065664000', + '584891611376763781852144397260140844977346305541197362434227194779766612939978629636198589818106137319267243431810481928639442343946346034433828599323416877248326356345631611148749005937144684862502198147087702668524450709118588741606955966569427636630159793409544653944608958808602551061186799401212712216156799273254257486955348236914237644151956226336750212957092204830385441792/126256710861549838395499078249922986417488985866522660521294610280820007499099176190820291214370699289977888813109514854178180265382471972583921477022411657285850911270389720508719377071949050253246021921916685716353393018411683757067093259101151481189635423399611625009617884983318141463140199839724797412514785751549277231259345923662499623640886881048572831787168979644775390625', + '4743054867460856425399742072925732465660626340183690464743217147109403130730445842673866624947360862438925194786600531200056131309608642363389833474026007798643235346104937733349791667694862514383520689594596660275306247615314272223660862212527346572811422915223417783887717426641317921972456913534338708745549252254865615725859881457906376/1021840037832289788284691535543138164288462770384961802287250236519983887262771944174557087207566030730743075334169628971336358708580124427857321953981475719503574867471090607795242192417162791762511940848493176847925838738242153177311649130718886794249673254183907621449520415060660496225939963471753122766421338241291756892571824984344625', + '3784664074155769467702999785016514468281913375341134899878893061325465790589101335015569840325786070795267055386681356241209412947116340524588831510768864231937929260236754881829005065056310226407358204278658699999612596866156294195316867934035877283950841910726224355461522065773816109849107487214275801829843762482082803559183694631856772777313673086715631547326400170962722842240/813783281473223559981291694175087508812520505931454895884442580280342455516154674683217348039336712901850738745276445107477919518905155156380620466135946654952837573797479076027688866326359448543065305071605591497778941561986579230698384305536224430794233462949056326864499827444363206162251104552740175503996670997705423888382328014210171324376593090187604268086334796245552762333', + '1946671258536842642381655747294621776070051525209940130115769153666368932042152311477746728678182920842238801763565726086459485050237860693394471644023999467670017470376746609892484192072297938254898880354014176373253875722410186683852253828299669530022166361304397631667133689128358365296701757782382475692465977794960109690362462330857/417774039698408581013003883929127512062321623871486379101498968145670269174833505080260389860863417408848209525427705249526516766731427603641806256289098209429110794311660844125377702016056984573671024035213045475132134896835814746425864304907974005950155657789157496389234919107772201305672421982198984613601511388413381215220924416000', + '328578487723377153600821813410631465225159589727320893988991729306688663770697528156625398176929288628930496338036815910669579019719633124832162035588583242320537435554101841406566009219059042888412893352417446437227440076869427577701706713564212185163005644118869496869980925214424591264870388237079776503547314587137721633918824664946974571838634860386893404364499977966899068777016064/70382695927096628347637455030970644630942872888311870124896575877585692281624151103204551947776906510366521972834357031777287506848887607457751667816514216907338309372900327919413372044625602555866294337672022362865161523684052940126509854895320132828009551012639156341878073682501185688461720664801829933689280752623773789373199210473995823367199793407048741191786377993390965297664875', + '2617201476921368517857942326432090876874414269689140439151907982631768946799200744678055980827789859579196832718849393046147656447672531861353456343821196812881882023188898815179947651274131166835133965629115749368441605680383605331300030886676081418867305170012032824011912531673468215561506423833911621270680104083533917562622600478548/559567121085534865189976875600841717161617153776904752351231210970240323460800785728739412474960421770806162360125774000625035522428160837855944215417821324758873127567654244465281354654299068546570789547691342609793050301258532586131013585959164719533123826201937801986942606844854123769532447570107525081084531848318874835968017578125', + '636225736038986537559880265988431731529837451289737542395494683393492040808565905376235074534207026537145408562785279823547657299565440309510931336394031904920056464959974743525662459433889398003683078967642651812081450227654478095420306880762753401111260630654049389197602389949892636943971690212484981672010398108426002137114819838399222096538921225458913266993881960732663394566024064/135776510176793971074115131648637508758953050390591773574951317807919051619690313331192027871176160424663811116849856489187562728496099757910540362703888937768555824513740118941387831822900198029266206334350448626733139136083404404120210893986654422850183837974770675600952078956326317698998103770833069712616832650406225828969036781514645731022616236082175582937900731419575337473384125', + '266695771933124633677367149389643417608461366874310588884377151539325854547826373711099517873721616543570605935954334944030816383858485296542260152894035979141266909050267414072982042090341712035518685997484257326212454742816979806460287972757626105526907510197321350895873473656215941034605746494172316089636216915825022339855304925515685/56811706665210352283362623728191218698295056176625217939528332247537278605113496147630185544004654583441448319260578659922931798845493756189402805173037491645434052737405379674607517658118427614090338938517963215812444779184193933749520313676564187507594274551791388039139409235056119788261689087831209441779870873305232021728002651979776', + '3511806683161697708497547617957719390189982761002154386881580160856792742952365159764830433511949678304281539875366378131195670004345568047690216126001067194904446295336734931691743477531830892015690816210752795806120303198745685394015161323982229908105397857791180342330098240702332072396030780386362735967021055250450666535422528637737695053315137238368787607412459874094164027214953984/746741569878639983491390741637813989978804202898438708743258000150996080386381281307609038830698579358879333079215327202911977568726258527646560497079622703052765164031089590965199628534477381843079892123440214378949632707668935001371616475282883095939750704292616758568964786737752325652839226013335092148777788733453702438432279149298482004122593243860829060557386699231448957232420125', + '499543951252504651717279461487337168721376180441322735807713499521878076780205427598086756686061009718016175215146305489885835839673419698751530207404115002383180054704927695269921072232395828258826213554806570423602966743337801741633869267620843626804016742412046844770601322513184123515405692795346790813502805238635003099976693786012816/106032986203682550514602969462803214831559442358811656484036951559991322084935692953211120289352600484987931812632505499612220494311324679562152078883141464406938524087376964935494245010636163705698220308243980789514815579319533341929989455211613890905485981353837321342730307819060825125980500975023479813757024395422041501208805339176375', + '8540849722242122835873311629952985285477986765819584558233324868363310302206006773828897816547299245727087876984857726652932481899766510637403577175623031467933486823994040576431755172220921921877192006685053572215922347418846423419683723609799729359551828522978186208983138345476801247634773975311405913274552616037005854836040162212761099347518242200807692224460514057530656658616850816/1809694575992816440924165741094996511361288430727981159314194000585536832192004274726651828760263522962868944612215633976203596150606503849595633388319338251977160257979846235995654357082321611719654136058257458753105122024859078420173515189641515736029725847315662215778251370398007468665986604529263071981040014321717598469319370651877296085250322599247141397427185438573360443115234375', + '51377057693118720457387330519321684810411289582445982078756989158447711080414032055843805733348558621949380554029375294451303430269197115810494758213980833379214402452992657502777067850631862453239349835215260705131864911194740832694498014048717871429797341104227759557199716944554347879767916801106279081864127192611546757627038037780244683/10867246748205139797826516105458406878398263495890048637741584969144406206902037615496936724683237700974333817527350121106320991054058406504571347680049945745432432384570515723033245430148177097144850158758462968940525568041926860856763884474605057056550146491001458649244094211354153171337463406192651150028767064600270112838159624568832000', + '2517199821548153657910904242290029026229621935918771922146425373057248090467388430999870036608278542449661971830076845113363443421757278612394167195431759807908413353743377586893872930828548256216510734912027450642648789925142482023713336937258932978503310551945630359646777080073329099268529064437120002567758664419443374110941824106727218341795100740316967386497091058915066147604481280/531524260324016969370728057738851340792702640911631807178654723224511108065633433027773388768889035083734041289308124943130099580765995149865288328550751289346866467755881013217287175392101334538392596956248952430438673292186779006015549928829953979591250274457658042926539541422697852390294886719770034058916886894408015624288115732154492554105512936468520265305162358665491880968652687', + '937159485027553069020805864547349554547661693803137696125140510201297417077233061462068930061915381468160677500445535971818609631674361074587732183297775728389124966713749450996643183965795829780345678634356741756033039293590264355732129789110416943789326482663579413229893878422643110852069225671783017610387570935863228248464396615988222968188/197552642195166614912991396771845374656891854357948006221100585931656016640982345159207601253599235265353060942313995345483862152489766993442419668492877390774016299878943454249209917002169480134131612530977452579520564400124908392339263545911443803619972765231094502745980083350567509014991735227390978850048088378721210219321877150756855372125', + '5119346675109082499980756672052066247676641510661024682574296075659671743397880591881419516307292610449161239233139088348510165442597278965718490070997173182184417648987611422446347235659085092530927312760229499083348217392125906851921500360553758600756698605047197529446594606336369425040236883171542367713951500007600355406492128741649090501989105696576697387613035366083536629358444229248/1077349190930018641197987339365056771667455373500846354778079878558146320193384166982231171317419356136237346389570422332278447217033773023556383420372685049620361481309067017697564760747047839930422553324681218253359586320952896352245366885171583221864112040038531116947187482413031483309534553461613717398523132176948321964066833901215878133594739833910960843185464241840039390204295097875', + '58469980853327028628854378052548435225264353747414624620847476036979290604604988637469977624105451959796142112200285366199247075503424204044884512269071061176877225315884188402954249683107639777479663275428465242905314418930053869461904934138752031124685541853599407727446896558064252696813370692668496452783003306386669962110734257851955001481/12284458784412533668960387046583440199646081362484321078872891438551639470826582933879934711861551014258493898870867541349962531447724393381011633785884743889445407749238183080941846528000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '632694338036291086868292541736272151517212971647680989553388199374739841494700315711180961760384484042159533979873604395621016272772109619401112452105475906812313548587880899058905419220497456502543971131226430775692744506047039958551641270070996110661036371362526401525207493270590999387476275629412687473362310414634469788979236895300129227082421021999210841384596617452097273320509348159488/132710874087075221626157136110978536488168313127228703025408943153782802265472978945703742152954881438058373686972477195846162708986982742647038014147903817539418442869011995988024638278746664230046452545499722614320284478925059527086663455478374200352375121910000967044366831275681254530914306064535110528723482282501358749001657608122307686482720926189537227492041135055090784059994118192625', + '2022422468242151190347511883185841333968390991430363660248687284021524206079162212007263606819387821055183192871951076644707111802972354128011904720586729919826758651167984507804867954970064938022106484656560866447290934136610214175516487579122328055482392137619829739560923807880779865442016530712986750711199998190148885726144596663143651240/423528009127070458603837580844559347493775236970253513203186912091140251858605354218715508091394564000304400858761758838182654201653901408046974668599216804223721114331085507752600832390227068985693006017591304916519540245902645712532993889000048992026931475754508248265356181099817467608863068068953756055334213584649136541490530849577363647', + '150542201776735231618709653388506394887777837646651406023174643241783006776019388336318216053045300417086929061565868663031237502137150880313922488106751241006715449458736723129545400133450376339156062387859536289694727635468338235084533552501618659272047966040838509286942825414463511175531001410923014751124732748099355476332649458471135027773371541572374016038525336728276024676601778299264/31475618522527975728853391146702172347290819252268551219771201846701658848754300838383637247014705706066050673096278897933831272607774110183488615338021656923433335661890414740664480671162459271850877855521869741833030384682549648626038314686632195158507693321505500888988183270276134667071014075017317692121986144742473139078857816578133693776368882596780120407949076129685719314703426278875', + '162664351499283182546788129866259011542529017155637405328005084357078048074065548349602626724265307733005745244338982661090506394708170105916177679714664432754153795772811389612700051002336719842784098507498286260450939289109123759859710248621544296008356071415713931721102824434964336667980062728653562369697289761913925215639430496443820411811651/33956433298509232632327667206936379248172049619640091186698677954874297837764235300806836350019565970550533206216590639008637925266405524715041372407701947953283803854153876844320283735281209725651036513491246238622967031979233265136280687995181375582844780830517825595245506321879177965868747546127685256867456319112247572348416655367667187712000', + '27228583713734183629451808889147559240254570554044667607519238865891084165382075658707675405673276615039702916443152954157535195508033284355580742761569101934821088036084480297442018005972653124248838420021440899368869744141698556359295489470499243402799483082406238128875386939389123336303631993557844565927960009744780823166462314116039469622216263221278402673028225214534859076857413557466368/5675134787340359276387085910732530678163604187009567576895331134521132071092293139167824957253050637085885426100206370974479279292804959049999647539684766632278449357893560561608633329561385765035738071726214510832066885156767060324427330401967034889966727638276371320401053302413136551142103678412692111973028587922399670456670380189570979581674699102934733563330382821732200682163238525390625', + '1340937726818688387636018677960518622106989311577445614347061615417831160296369500704524022869612973331444242000788110133157689786976834913297544087249593601420972111789552905846168933014570399562460789075871112224533723259660967481288525220689577290386781169316008987012566216235876842757183233207943071523447855623001920854571597886491163165150556/279054368814522483241446594911968519760869180429079780837994812436395792594449929617293503975169489945136244602350941695478861584261665582123038340824700224583170270427163469762789603657025958695551448477050958557681299495041994050951692916032501884523150972841782658389836059236040091605838583126400660344344601879969354053137568588338096589095875', + '8910951660339249479517731530773509037034977353457185453617569046885132434552771722727442649095007828468878374139001808825741829728375370140505088133244952288239619141544661695065318568074863317054989982620944602245967155400712702259180443238473885690297281525960618905625084639870407113029712510345262279507055197024677292523214467306898573956604928730377318210588837126450942086527893437954864000/1851573255256476362977604759655284263358595254392797078730401113550522214182030016660458780180636930591149034499142464357511158868625309442843792189394292787657774627425816694628839987456477799348874853555586931850788645677998004186437300173080088839930384424797706381282430208961565555953254145002318893188197561315855286173276564036402546798318351229978103276843822345004757661005876638005274037', + '84997663352987536417038496128111159210386455419431137931308301989414462311135708114322046234708053448098538629166672456703651524480738219822858469100454933583774404032968304328859365484249897913676382909050450855222567147661842184538302991526635974158862033287154321908483007506326807469239745851379559259262425675035790574846229470041596801343087041/17634687575122715507915388096857069366146981156897230620371714518707518105157483283253374797127075689236298114335734874553183291806268149747826050838089931351158483980735336772879045027213982661634865556129548356487722537193914986994283729746578044621115589195066212043187229606921062007047940293972381121200723292297716621231320254309060321476608000', + '1648849257486312935416274009474520589799750757877252390543329851029437452988499485179026845292068377957697884739052491455807688429630524210823560524122596610276957537756142278585091315141096146341773982302888007751632401090642472506538817998043326415693840862261701364114923327286462650797963969289743571514890723145251583881752876340628073395708716735192690808352188867334842743895738955762067968/341580411902962140529547622267381834425264893226447658723654120920549166416114849731588552578626041597847002946105459812230979900480135423787717825697955152193526724029890829615845314765605770060975538542976523177976182401820926854406945392167140347444433885786340415437006775075196031433365827874254477351374236932624514469241634422312990555534789607377023339236417434574755566856489013828206625', + '4899626659231633406569362199187192352933567344960498139854460545644210119722856870634652562532083546344965166340063297647520613455037240144734014575196764021102614748218834054157165781057405100578668000290493431869797883155764963238760612333048336953434663867580862692267574574465371615023999531030446250351305046572267686566999209229202587258144318592/1013522377571209303732207048597964555261512342860326827024786500686692475697570284120359840724596444209277948648863014479460188496587721578075151793081823795161404110811480269106846359964436993190293615381593756076515012514100289406862487971220020752628653845345780766244842386136740768474703009091552043945843632199999007070800871588289737701416015625', + '362903847358088423032098939589019598036593525722040887883164115981767777873860799955715587436566233010543937498922435963385091400670303956612881974450548904906949032005878241638546734597308786514086678561900713741746905742866635779432216761799371793260025549698865319760786921444505323758235164807966277024031605673481480590193724980659448692487108892154809490199414370023696404259691299614147712/74959603173756091922435708504751461448318819548943586676159545028063979695880930926805574951275602951047845904770828900645717477098988124553474616764040938121171776556999069351772286262529729056288451037717198273521810585868307402693261479298923209587143468206907278220909447483738730657438961435587752873879244136801168836767273934997417156543866722171766794171623224855358640156858606843576125', + '8521348154958613550574095745596657249924328336262127244827955886474742319277424381360677655239017583606851483318951440691757875270001283229585075137091617073509041644097526047127618954293881264113694362598157456878784814696577083997902588926421675843291157049153701100218784481406086810264243143337016042301806376864424632290226377881206765051896745/1757584673254145109614914862134217345456065873908262753169404848658913774363650863904627183104777337461445063704655164325930891269843669860409763569362863600932267319335889540453302785099532688264425988677082044905948179153286276609910158854349711054144968131934713419772852663860893241364433872853338630216232313607969790083448433666334742108176384', + '1802461420562646993856730082999823508145602238125054717836501201545920604020389361370931345491160549787411668288359013059160331370751496329806488246135100776263777863399096485894306306621852596694700845918608199329091852956315870664531614358379176680326508877329862713333336188556181470928613423972314983964578645688876556351433429494008513812136152576866650152623510296911708111085518974142728903424/371238439252064016214448115231139360835481920731625321084601566992497057371416121407293286027832357816257507287295719261141426130159269433199862002841433235955835172774430690352481806933860390015899973299268361513643524088242973968537595085600335021120523852420135081643310663869327371821064862449426404044951571585406092523478861879148545678639697679215289523234342337347529225543852787599016265125', + '88795280670112240977945082069219541902481768504536167808816453021962616596410396813316064685579412429940019071114917828928080181638058444302439626425155946562362550070187433083541414569447612195370911498321149367969974762244140788494955146280201020345849385865084095189982291190135293114489407801749533448443222584092575861096946605418438181411489276/18262596223069549313969288693970246663726147886364354584825624311486557881239003216971630599389642752648815377795018130449384513824927138566203714891090017787258920529990523519479383032564209629983810866231367438270859421572641494420831435563580050954867101292568399107965982704216434467243223964964535464624988344342084084237484262612100980519602625', + '752911409358158070688133336918078236438086521781731735123294741731669530734909563155732092406099003582749182967246236657021875488130304108082404134004381196925042617909108654109138702993903561036987511410396567636331465803949049946947888490287210013788802287247422993384874670368649616782837096233384279817294778916419067404863458318451722971117669642134894906235066332205740872938406767767019265664/154635332883086377656813458754577288834216641577439356891585125701020579768315520324293435008561626926046988056302745604248220408154454267643222328696606367409715369481567213640103076112146615938180089303294709009813772509602954514391675152597458428326644988225555987883250642389550531163153338541870029415077982539614784206119074702022117790679351778993541426110436276530890609137713909149169921875', + '15286089077439918584953144558775765002061832952090847117286473868694909713791678395221438112006475047633028543501632631679316850512797978594476229228325228403849089079675681042224057234415951253954044235099139983386056610384489202589484570852048157272551098909455253037561994944665563291526597323199530997923272894119350512727802414680772513760081360291/3135161418037836259442831302977219750614726139657718770206960776514822107155928800694375522572523712277389679035632531727155957120566617736817225541673148354829212969778664144907503411589126111888917929495106029890532891919001138770210977012708107496187227751496980000137223047079127096028939624830654227758737339725345681745847598157538483308068864000', + '17723482381737693269787076798246423310802126092500438681864673375704464394105734049099094818738115345706100191580982712146882034618103161268453808819471603805345990162762722549964406042295355916458624836894291267553138041035528315839108252422478610879301656207772491548742981990469454529537049123948604931661837539943536878260665617524824604291547373872716194154198824107266275716083848262625068958720/3630091565725887087605600771358216927099748013831912080380983545872077197366172619225751217619097657405660172801849926220948413919823038425519006413005998509677472226729295278652284861834178791332630676772284971762683579047508016687969040658592436230062049051143001484384767836353233324570686249824980508520186643477313941054900197268584495128610811273834192099099592687485176400539748616360799518357', + '39160514032490258389003214587901781721548011632821053230881239001436341012396166857515197251499460842954863342641015306304854226194551189224721257664420778416733889030680295206179109679627522113592373938602737416822981698362363553492610842201005420727750505681358595340127099855823746410663750133019743170700888375554918213182862789028531284364877435896/8009919337434786244380818390213546866794958389603666737562053423775386173149395943310276429895991903753441584557917565343673605019989336264748588735390182130334278887642569515202100870042209261561001627386525182096090194076839370589780431326097965428311786635366819094633651297179987340498215947144870164066409654464551017761658775207392803825996394875', + '18188388167811476762477659006849121912679763597132233588170406666718758511478154418948855160838212151370453213943784067796172102916618102024199200086478021533171984254799008541948973652219533819511807681148179806051835318645591102296256347927018942348574774821478499092054497390790734798143983945096982240517058861467440217558586822663985584775920517755909444876443211404222400335500608733284461685888/3715278241795087610941547133490827711133909145414878217965273061493740719983191775105550559905283733134189195190321643965858771461560130864714477448011225816210219450040558581571206711791272284197614810026220745160693898421068168630870850392739817340370146186703867097982858560347971281142766235850971837330222970900661653800041795020382847588362774677568059355238174402415384788280852059532958121625', + '46353430636874284402376008361176880938798775506236714098123916668545331718677407145199311191108199195405139575147933283523305343027297808443653460575141799729554209181454676876263582758919969948546918471128299522715691694855904535448703118221685887195383025133924852224568922999317583667985459047407473419196367812949579858081181660610155317911318937349/9455890179897829052705408931064120820559254220394152447502395298827357144081378513080398823188243663943177539817982481713456798757563952162598464929784370305696420245648035222083207596797327498500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '13607414211126412689435920170832779523235279308039712294681570127241403406455725064605210473923965685858012114332021572088551127762628892611459785625407107300355287623761046438724706918300976102581928643740596336865698485838431972050267114940079353996100620311097561405192365496385584164725035842307146351211183443267583872067352881387294766804298681841402861626441205037637282007810811602289501477350144/2772205947240095684743358245950494400934136973845543049938212403703005150954425087866758842625041409060217562878157900197951382737615822655863548356735553650755342108183641353142157728364469706919993519071329069805875834057743793434164720370026073143626055293058601124362130539040404491770346043977106988100969901297669523170508321703496903672333781306743488573086901583022550356345156457496396749797875', + '23264350093315690507131500726914401771633259602904894387312742275462601574788485574913195967854009886824436237740789910851843045306673524324611130536719802717912615609109285261163220007241492964833933975789257681755002054890453747803418465119567389315754801791950553304034666399677630030683759466627797123971742061866928213780452325847848901682792886840980/4733430508316851445525799359992627142007028326865116523033957869609125212251136452396410236930025876982265915963089597825393317909648054808274488356887197933393799241941120691826916741647173843634076299630884655997401370400734954098249519111198509015387869462198674657171808841005212058822254513429836457539629035675822719720295352508916898476137111981397', + '538960934807826161927564251064896730663918579817797131801326848839524373936888402762331254883505157846659830555799909822861853145306547699817986010353774500545305596630924815657016569444824389893996399550343022444937690798126189162954188680257730893424775303801562327757655350002001080811866451425665301674688928515041429654624970707803336800125648742168380676815769457467038849213225206824671025803392/109517448833211208909213522579666376007937648875042608355810687327424621469323788799169718267715808580775296460944943658428295481347915131091297305260674731651980547668762317138753250458639043477453950774629862540017034566407286963562574188172753764978963685535201028286359818524519980885827473904574840285411260037264057197155527809916623487144493019982419606308144576939532055057697567269576800542875', + '24451481554350251656598350124163983489544445991992661049829014268366250968046589127810466483349378560701491445658725080497078590986743512397927747885285095846398852574219367580068049729894667374214861939897557930527646930793431512683750733092448862551580494245486308114129295467976874843852138144539250235632985676953662288536669286915983261820949386845806781/4962240683851186225340479316959605207278948397833849899236383273145853045488131674290332609876751222112917095096150531825255611911377071887145019702433883343849119017643117746273523770961304609989779231048095665388209125939879595509516490230961558353074751312876252070889983946768175488341370453053495319823381943793679003096504200440997908071027962806272000', + '166591964685609920525429719961758335887941461052357297159523350448241484106515158310298216150030768812873781356326923214637847449893575074813991141092681107583183275003570804134461187213143832123628701876041315736048451374824101256980832991214945762266748755723551899001458245164317581617731332466039757786839492869211248064662857987111284518155462148044812626055842061283907063784492325693994911811072/33765943466859756347231737285768516240749714264564410814839966011490233598006227246396923594069488496368994857718734549787000213944990695920130591750377483911116845697231074543526982580529937081378303555262933800375576494067959129682770065443667091668104132884375840812635624862495548888467575848428463895756394050252457592540570490061561616914443009224917652440212112452400106121785938739776611328125', + '142464729519206642943985715465196065680101634264787375740459792619754842396453535646509170066091478069799080172299369994438836484938378991283020978342879057271045033903518255546705211647900945143546293360496661466657600494787228476009216508124736254967761901224547269496682078470378676865981855568197705507111970901389105723589666709580851611656838285777566736/28839643075889294346635430272525349651555039807615358795334663734940126053968249608601725991946342449660829847322517564370606746964714098508298705743689729306507554363954348780144675937510900983622836770223945538844314468550644543502501484521561567788992156632892028591678742014134130878101553633195551605796158103033794695984958987367799607407626319024034625', + '380457154857422076784416625436182734840012277442496600178664323821064977485271288502806124740204295811759917468598315932331219906040253463960961995849761235911145436552991677050565022181516596464968316292789048809559129516930574340710998611088996508613850407294743457197519091160170604426002924022923388338999324182261954685794522778833029950214443271526469607532589775916381786492089432407550224137344640/76922142291839859196033215395526448408989799722090330866256930898223012427332209054699494033511478050536463051459450844520020426942553107946282832072826069342120835351421637970462054707566481086202287686453371483126873188020538661871147778312986131920021929455276187425940666580158173676284545586944833678819622895550345083175685846944633982680908505900099388037050135210936852210263717185637166725605577', + '79999800708369666670693340813609265988600461251844239982904928629083975986385235816587497657678297663151298709076884516168798694292862559083730259839524968813932151813233366175091317046758260934357096757322369138747731224414176608247605314668045838769433811781490030635374856042786947109101585097035794969502111354126773165391577217071956928171336060247633607/16154850265413610119312596299635391433380319311682208755390341659152160578366244774029302281068833963848811596545791104919556346810193385001196395489740571188884873246131813864782581356887110499639544662322008824704830818136660544673293865921754569356583698951612797219776290835161780326945625621758204931298948625885081325268123344406261740843197232316416000', + '26517014004241498798848003208133111307300592620947908933072661812816683085261359696887064436611747292468717739609776825664344064024535028242737616786238200200498291226984541942836459188328346609549239724653950302145388877472274074665166631509490297630344351573245265440164082334092997307729007429218385933622623935704482371360985836659823269511115937443161532987660855490906546130131716829909388620539153152/5348257529530073524280989257577050878033239486885664777340947102417077875628238501791711336803418503616042922880218860266611595594400020206192599559925169589023339295256084579966533260861249529670141854632965231468533448709363477120437277477668173149958267956654411822360053335695956270719680086524322530514249891157439878407417971839744392044064720476417844056346174795601710447583861127956455406902139625', + '71197348290771978405602732839537012310626009318133570890482364371056186523989330157966045289127980687764218363064941391483781008265266963537623770685390148810455244349859354857421474277963009857011653865476081389968122418691934354122230780300833008753686234433642072919923583176333668289998661499146617277981691372057042930131717336250309680764289256080895092/14342680993892886303954853579883363225921109635079605392320944523109187641283117181461662846411912306480691901515655220490850586434947504504317080144378185989596578890875561336496479576651516902048606328604696610314441229321219627021610788398730173161473456954861916116391687748151443335513249810808309201830214153565966339609616397865465842187404632568359375', + '342401018754023891059352629509715357053454506037284788725127073168445189422181540408896452651232759821766616850264122591225331120588918865882061344322996688880669665450231982002597398732201789017495427566260651180470635467002453249479483900268320570004293679361696197905823690145326531180324429318916092471863347575533714871355651558610515931373080997536792242278529575700808878786732127630024064418468916608/68894647682461956089382461406499759613691148650738957016015844830333168307020284281532026442272618372290646283794122772122665531108484704223311663270836645399554940741928358317884620420886609705267248656765357348477857852684140561714900614303731786621656041881975328169543884661780531057279582784036689401206706069028229798913082362331334839997133771880514515972024899408556517159759907883793675672065360125', + '190286571838805495686895463752492174798162827967490343625540320679760700551286395956241059969689020885714190883323266375067607282890063444652926151862650426323339187596278760962390404656342309737428214493422028545912504503974763216230927196187743439226243880933947417760147785405444976177822143722569696182520826218120595298059020305548681823408268556482981325/38242608291912408815838081993045123864617058390583394828737640036167893859507494091864963383596867701436244007905770284939991912884465101575490548813894148831966132970730263562113961685364216989999844961688441914455435934703797359360780356482629495214859917249465567846934284121032247250332634283689409883499295469118485912489058982596725212975311140618764288', + '15345379701153714436938289567639102307887273898834051456693981514956329731572600026866162286097662958712922836383881840211072389867463987895894067288699955858337488812343674818565432780224654423872618380928462939482247900055572079914753652915184313685270722583926261499687728814560680092231526501970431280783177357735398580700300402036873780385926225005561766822479878280891130232036389707452005183370152749056/3080426623087819678602049171756961824196425548529619576972793653585089380165862889092832375934535464160425580449193072125955685099151500755278763306599307218302635233595329082702148677300945556130106197481236122437978375963261172524503625593066102601042060541617791008722569799735202779005407381267021641545644264334109052258081225159748043792288699674153722046659880085850980843460715080965607434114275256125', + '36443601662144279337033484452272105547257065463167427418525882851507989449375452076227652634440476502702584889057376829320548464677623038648843251719168810701744338735202853412651160681259533015293997501525430222870696235141468274274064390273214484922080219643095978640300874086585386991338440415203863901714217818222666715642467237183267303214471854472286478344/7307237990148312451155090506264642657872224684879860008436735430879314349554023096366846856470375025555139219961666754780864835035902856314981482268319886395881809659368275720375576152008289949085453326722156553603358879420820012964006978808113307340321824660484441618487292138631579616351777191144073820686595597986548374588839763719082323114663844043046087375', + '1575294930860663922843149113097927168024893739962464327016763741314068438447271326302129681931564068796631337571133444163024224071270063174829664848259482066723104251718808936762938249028582992557822551950212098797718627418782376027133576209718376784834456568694813577764300716139817031545408608110880973380162736536313173329973171844528083756524804574267826691156368696436273087171467254161930265910972517504/315498546654512047637461608750018349421492986465373840347519942494904200733840189483934245529561231604050629653992944140351106887747105945984325466452136415067807666316202346895657914393512001212656936450425268507235863153258769360296536050028695266363553141327896064944506216017933135251875087069031098099192325817395958019524956320840925176589616439477134480262100237057953933117460110224783420562744140625', + '7692057599553133417225997786980128299372439542315125030077404519679450372805989755158000864378924877036225189784045171045139667691524763609840822628706422918926410377185494048355128046783294954373197082851501927410350313585640237550107070936498164597231431242858771457898152684298047446530220873884022213114819556822050178579707210264243087257050468000092842761/1538814487241112760739561704846381585063002784795057066438330325566843108114787083741788078680861495340026117682606689156065008672344707615420501964123121620401073675430690541617930520947170306763541251152854304975131871537939618684186766168376514664494488691311397307418298556185724993104760692216619178358268934740746932420562664234738576394742559349407744000', + '15190836470550557926140012263310230378455340797095100474720776113765300081463960235506950799869073894127884189857505951314428810423734978025409205413789025720642609035117121385453856877706229778165533034683130064865256448444765902482146676668197546098809055341050873494446231793312407516463576380102073171090898973046611572020564659042146465345334273595086941562425006763960197801018796906114925005810069684480/3035556598829526968124942916297025416522606357010637652635799096498208643970273828496655227572100159414352020909846298817521082538898702393916708804775798552206401718323929157863173504795127180177622667376867235806208021329981136637175570409791442328468473111768140746895005561471574356889047981458002257872092408771959478275534098789982899056118498417649167444480913552548249005268380838457505944462494874581', + '2923754549090941424546974281011770689534961442264350984393235983189168024079617848190236639328676436485460311093391982825027512234467853098757486086492333410753919927528867817406389678461354257841007722472106464785557179844602277350411593959136520732862935740433163383885526249897183805981219416420390137516812715245569037561966929572448247077578419534651990827044/583599691615378545100950777601104007812792435102945248411439102112424251478512952087517463400054567832353967731887062349496616445419782499817548928078665212077449420216892583997505908858360011413290038645573504472422610780073975450391707084016931661068694024023083417126286468909117254531307085545768412513447197205482285022417611018126886491416133665625891075125', + '441922815213568908489589193556560586318864326425397702047965372289295962228254658331201274291697626694859414786292266008476137667843874780506886929181835469302142365772763129838217316953835831059616268288399119642314758261677639945004547833810080700127847214666763863338091523753898362699222880772298183760217731228628442782431573977490761665091005756534420789258276075174436091798475838370152192077494553263232/88113527373573049332749294663402406454524640221866407165839162576979477138694653404281889227416977618913249275712917582743426231744980461918772793673202077716234026090308791893409200158969108910565467273725064769890443408324386897017825489615600787634985397885230455168591240328585041836771921929567914891379235573982442311269392541694564675053636078990836491104116280492254795616055818375774141667319774630875', + '91197133767962483852278456285591810579974783077874307589555559715577599604626819245000672929518645004771085106770784762994476973842713035791487204122653245797126906189270204029735872508462106527240320932458036231707311992868347221176460407003487373754014365881209649632547685657666691589606571865740595829983849941049450471212387808617247938941417000965198123059467/18163679611214677813643455199979098261849405189805364986036911945012016248297645596270602508015864371788974684317239728239847888960155365342460843559658233069613218442910226128113196984801126248862273503232000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '733396724434554481470656964167233690851279923085197144879644750437402627936723531635898542414136943003236478432848965516925891956615111678444586384493883113385335463639674699443808084827715994137410743992130712903997815471369201406683862985677774155704267039224546352414417632475628136460919900347977754644214725932476856018335195927604771408949980634142255497812566866795799230501026066145434755838336990365184/145912495644159810313026420025053989609942265686388630266226856650796088768164358961644113076809511566125145395699271530802891523741636707532676065833327082092521989958971154798465784847092870457944857590623097781956884297103961713362268264159015928554707158239398721416218953003215109202771651993622244788505649098445109438104466256408060944298437033058380804126574878377874459371251056800895394904459469832875', + '167606423853658713615749101123336326908381619586838606922565378505211386185130407423587264776519589295420758009764678759213587906216543160801059029133457062067529468464636376639307690657945597847080706144898920305379653754920571797771178341550808041652360635584401789901525709166172283390177673009483341826911179861584213314395893323035527116283758445038413646757280/33310364848744488727470761889861069993071965180756187311178124887286133554324042422503263994717993129539448310743598138364172889305532401505890384806078790034557049074169245078416704866867380089103064821732673967120808611636618526737138435792818828990403374766228037003927569643972203993263329403469747342204808360947840103843317533210523853631711601450617095736473', + '22466735478643771803624909352181817912387732117378538284107485621001246177152644141853604633676939694208131243081227668237716194316732125445041260568403752325416203858329469477774720998849334961751541191761521224644520742691981372782009394826739249604257098248751626957528543558542443122470657230835176462612530464044652140581367666271771882198747182114685116336512604038833327707163107207664705120728068987996560512/4460342260599110071819142535903990817893424192972341872394014748315090430999224107274777695306032783105944721337053156074851240201179902835353097683269298594190456286888326067932777720561181760369783486872666974623910515795208353605697065447942804414120030678603538761538385793829787734830129764152263691296719726909633580318897787729004972323599773548689272008712427926734258913271101899780538635213826239542884125', + '1972862002066252798837977155272535342727415329632252213508267781390870646909686724955861608046325765650407340102472833428905279473185818898693854344138403744757857928883601637909604082588338869612221777263582365745419719805587946558326048087138197694016224613118304623864709183681172362693106460535117459192992832969130895829916210933710820035479169562970982784580861/391262854245936198649509275719624152305206807261382621390946060636032376057385723427601113233050260359684153963520782285097664628279596618521963444574543409076428660874564026651742234728997339844161514273669735686516761239076529088145208958938384732800586298943281728435207452253170935715239392534815683367114290676180839217919539381651841151674859715992309727232000', + '25408691426286653276004556426513401767718328640048945917745783225459709487487356095164164652218100925888847689099153320777389381851598236046402124684511968385325107497047025485302333503075085562533472482645023963717347867696861885363417595298751490212495932766872748125111115039311507374363000062513977436921772096443948159289707996749061007112458126301618769295162649600673247799961118662602756889448152517364992/5033870770817104909025512331238419776407331281944565644457345507262881100618908405924951559853352841154730802006750688581603270849880756026262217651685813584900300357685874044482329601894134443542753690048962260914642678445058891387041919581376195543193199307524572671344323361282633828144708683536721118910549933336800461248338059209853766305058943888965279863852107507075916981165164543199352920055389404296875', + '616565940481577765011174617619124841631603944267256178093555235591208387588894068005162766513720546626399308168923904625210748051788652328072241494243040185611373035059619136712559505023703153194815611268503291295059648269772663939137446253437248541496631852986499688052678961495116210133458596866684413931188348798244969797973846568160999564540652111599082181871436/122025682149627484528651507368051367620578469392233875493556828623437869650284356703295836998178256875766319508797903923638967073687991229708051285605187358616531926112736402511567721889845693991963610192161413692928378708223712791930113386840109729368772998347036090902364801014045328348830748307179590755723160142468563977011700764517880467116766150322736003147125', + '979882056834583241477188686550561625289337025747787132773853690143359657519481397575561812727394470125172272246774578404543257210124412718725081536309818383310896102958167337333430555168671327992395916735022711297435263157670501117734412587691271084981140167017421900852550822579410636184333142944411060788723653596377015359036101170054828623669169047118551010102258802465554033173968468183492058271365801755986560/193732066620504775981759812703028757608600125742477178751189607602983198559778039141192492589734277739017909889924488699372940787977794555519474807920198944109549023547259912324279741006975129518191081373554817584214941629021088206977532599055014150433581544599721903881284581257812850697423350805715586436114842183295730680490110793225514602133304729484547181740290534056445482186430729398701311563285494964645419', + '1138059441890428903336718712159907364975959482163004276901676063186939296217569849990951930955270849692775077813359573394435924934540269310629731801376134549169322946310756738187834128956671195358413024062230774111935109203207768898525882417078119314003162592087920755572050993355843681931139257303484526223461840939880759908449181135029327442039666513325916729112853/224777393704985303260433439401503507705148576727364388720387174977154204629336985298082893443205223017966145098064861566754244624732671470935725326043491911142467178027013112527668183783450932992534517827863293825061296257420183669383719043873666284378647952385401303733274771042385666436214517205678674213118374889121542369996042313879258622144464565403138392064000', + '614262617634904509410555695398092190166323289787703954329113412647342230855826568873946233136303566178604275296873234926652080462259552775254755109931312516616838091852915397846984381183732773411263828600108443070778861986849272794442105107285234729077818614332155890946804257724112143631642663084660012362691100872465148864413343598067567142977134846266425075195546265101720326272502009234689388355365794889444257557504/121200854655310345878069155190734381476914416893031941761756379148622591310152793328910334357724762079139509813381997287954319983165971628429295803162563856219211627796200599196263868781937234015821367575512548522790158101892507996780675863234181700253545912643548466256938936835469646114659598727592390628209362096738343417775009361678599075671154405137663077798419897932404104579113056038271753988669813988076515476125', + '106893445531995430131837569895703697604978445859412739331042151546083234373443601923983714252801103551874986033841637079479794895452077803182698291822629713500021995241940133037200268381793282318691568234728329378829301282121562652452936297422136961147432559766896143081059518238234777055931999714973999553456547746825617168005650221542417216291412324174772893635832/21070287546583154895102927029203125577385528350171457634203261197137238923923245382493868134869009844094435068284335909266152042903157700510534904847257436666980671067329084409346149729385625108233234542133838924281113454526471855573271066865534480974484196644173270141056470857666632902827625475719738076524047736123182255306429677688129231682978570461273193359375', + '1112742798776748551928744983308584493817112400536084744850969025063360679785378789764875076529575583792571198510306186417773101346425704969671256564494135750783642910394213727189639661184379341961737135764570942961692159177189753139209638929302928699705042832922774012622017270172331722295881412111812933593370743210214549038212395941675257558442878012136484052827439007483777859391215222647499741572327725689840850048/219121905514652575155579185322367175400669386131891843382447432851122462422992147756333729893028362013318264155489537138133372703295804002840920402482110260590055749843170064921843460498824128274420925357547023876488939786863662801268798987685503391058250902634999352652063668663245829496836853509616873246554292796823779397317819134891873583857621623143253788854530146303641400986028503534408870194989478421253057625', + '983995170259637497940121707857673268620603982844439610076334749792119502557380361624096960070148627663305171311646851566811683351232268240169848810658089698606409195744471343416197434926513684882439596141368794175162315633192028977211909698595778839497844847281262559899914504080092844816319298286511436690429175757804604673752838786852277255064434365401871903307643705/193579499709739777577893639665643417521924510242119561765932078835121983190401762561436208758210519231103767232899529457440602166918701550386152070813526760823513884391055054076485295945972185402977095815609448651316824455250005505410192101148149284756928250258632031981439345164037318129987999546683113483700660879431801728391911161900686707231543216283304182063038464', + '30691013076836380319652884848585431917439358375118437035944950498156836885409110087616602043242747413728866747975631727566613593257623099549583001362246649928075859944695060947434885707102684568632559339383409378997568182445019183928058902445738702610436187907199482604867148445903921474846103586253320950728989228898324829144701782792767763794895137313050381112872972806712570559942583906619373159053654481250809168128/6031936190594376585238407905469035598668211679521553144958053099463931217856780171872253258738635531385711702176347575597387389685526098889278278029314982721284691198967204664771178761416891317672705719303088746953644978471289001577698554025410820041497108311270431130911021653860874238212040640737171103823631718483572464361276703378587106311153455557896527384426327053165214814954154328483257657424873929502112904625', + '33633002641218571027682690377239300567277297301791434890431645909447040830457253186262970747405514897340315490970846553666201190386457221846694126637998462873786334837901001833771510353365825087187582807843940479192544772559118293851131234910638705258664901225494129023244916125545283621607920983287799648992632814461828998517949449887522866259724675318233522060729654796/6603800575190864815125307817934327053530311915572323544225173935830559756077637251538096164499112411672913910767032482188570876526606118068929913515497400397562722669603861813453418621246538114872330536044519242663538662076583511078523573320035084744399535581216815034837553165853576616422592790103728141443697362408727056813977234078214386771115586436985105038873998875', + '9090550743395453237608373761422705922989356346696677314277463900103283296889488490217663400349361962695279888433677856471792856853109469433355422802663947305503479561493033851081993247556828544308258821557994512133708656174864544457143793106703578476914924328960109390350040954158797845288786321215010324565010420734044059968931558099077932776525974187717655105951506223649947806568984848883258144616897526257038550656/1783219994432430133950472014582130883873515063182143523712063859964133099770763874744121895885645346884371273930727450069430704145915261127061538288702855011220545530326588799412076879985026006593893648911637986337776174555198865379578424838210466703683642106934154603710587408512357351455891557594977191641529451749437270208258072920514602243985184744955476557012910446023911358037761232253615162335336208343505859375', + '396226087129511787616470716244996965529391571019620342657409150252952330537366537352182763477820160355931003174946109480036915624974500187179367618457008802667089004678925595775249725629495848146669773574395699032785024522882892752987825063933573399691764866816284097232869198405981830106568826923407996367321353003569052419999880089180418759265274331299787468134974811/77651132902382550540201641628906752945078964002822585000619340261128555048628863996784740469908853602590877726845475670110127527874088625214912598995012971630852593597027649154487264595100300180591016581254965295280089902497540043916128232871685159131211213497239568483141031137909208031392464815187505714066235868748950184065923236670418822911614780061752905170944000', + '1288523130900767412477856952035690867153496584747510253984686336382980221603917538996138294311274357713893956725672062832264871415025750136690143411884111580335458539824573239771267043414225271729141508235839982033182605445173050590151912297720800168687369852280323356691345317649445843078291369394324862039143247087442309135074453265658730028145500871559458519282433012494054724678302957276371418936282902434203225600/252284450515080370603949213084529993395050575951188044032908270470514999880554862449223347761507020354321422785760279154913310488296485989979706208254206974075438570521939826124860483842758137231373182496438568202110337751291106165654945352183167578675508602832478752400011567482434301614269579368882858973850914513870297071866544875650189106576672289734326492492059816669288907018095832842249738081022227281715919357', + '559506782175368748187145067236900803925823792088293851453005882621173124590368101213652371965740365119375069053747415415684193092607523211636912074219215053809881814859129515591479780669511533619258835199715146655774919418434080807837587396115955105126806762961292429904380132116986984435528307613054828929294602185927322238461491059740342387266084431988727321711184308176/109446188216864405154936682374609448082436347600301765944233278303643744736544109262794383069197271409398615846380701459882784547857821831816863216080069944818453908760476475874319791512782445408136169878207358663250499628002971644430164185578966399908622640732411885568638054229180673755690889471388585521113862492432946110902070139679164884208988489955874131206343597375', + '365842109776085256207829452385781129402656684569922320812625438296958174120084800064669048017227033185969596458778213306152397766619692644694641677223213185977626940193107608138827771259334379895020377700759789774994800008933137569543592829220611543060189849601828342989023692876182148818363689293009174358168826340010491728715549904988196347316703985894355217235339548078187164176255405596310602921647203428200269666176/71497266937783826306774367225149833789788300609530680399994804704196212252148324552193149332755057982662844988588678159610516634667597707204818296457616315484973288944637139939912166034691155272078609876888919170894915224022982785138928008133003473320018151370486895637936102520353951511525996197267572355782608352075229547764732602557133494648237258900273354364050662921889937355682486287799239363976556848320046933875', + '59378684894605773397070783936113244261297966863668546282887298999811950599772732377979382620766180662159460666220894896896564357533604962977968843474062092083279165837424210591042655271577974272394203253875965589611493384384257117088518521598379301169916060951350650621757237206805037261878212424267780312614942173870651684587166691539909068452932891649554575072205080763/11593916877920131260484148322810317629326850763057953025674154928920977965095141485536566217873796019006709247938019404840440511433467040918770165456925609124166970862430104329357419669175323763143377917913760028500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '440706629842402075137926797732405268015915199334341565218443748798479563505125605478061664327658137598204192554352688830679475840386882350034150077506804799859586772904929402683276898098601814091430598259347980573084686932949502809068565420937301632494387691284902026088950093888426872222813857928092871267130505744095697172494119689779474840037266295586359244899477781345249657455186847452249040895151288114085352974933708544/85971876318334010542313048056773419567285701852816309711738644074302467763075671531248793080425349570584102078764464196653843973936119037322000695949209283327606280423406302517302874806092835694196353778161663338667091195199032831587897410329509487165187753418615356836636352319310103639079123698118997500068644500893282438137614420288839230827096109835206605385673197123994357393257304049093683934224826006704198171592304125', + '13220371178312284248107863849287558696210741535280184003301555188086761767176560823174196915070378763091889333203635784037294769747641959860810039816533609140662043493962707445564057197150603226797446933532133956054671103942520142568674339411250001585991743608723150284270101326419464088290151483823061613403673956918458456165552482913079283537536214967294211244361353060/2576683013797055224377707713704899315377379548630511095136568220145808780162671473907260623503552385384482723173913562423316997528913651044277418227616447815609873991028073011738113927961824632868956802271260292075825561646156680740323686126811928372672670917332613490241615850953660246271541592056143611581223295045062157404996958100136358649109693659623847024011265147', + '1538372578072246705266078247950089219881197139399081643650271348770110178217959856819170325400476655944115553238940541695152110949081675985299189913515975242840473616169775880272489805123124548256666658584129873597192213080038937409046114908719298170078476325423466734429870048697383304265342125425087105698152643145531760853127801123272389134514333752173173161562273487752064850461823726948588221913540799816489566883798912/299566102446745552813544808169323270315327122249327394279467999308669285416179486075255023523726449428151807672528368606071047812907935426406886253284580296349349558797128593446316786243666290649442956134448480598303527795996222703837663605600624271807461605295956674925277658331760000117761013381199880905748188151556184620779667670278558839116983636972680749836497346863752662521197700717702790359162307617024566689271625', + '281696937222363685333688097915539146272117417024070728031063821781398985795813333650547982965341298894634762289669256129805545549280355643122501181852136332529515756074502902204877664395456278205770206019663082511098956468724829475220996427130295010430751942126263620206886404244432409164651524408198286831329682605959881993845189585640874220032515465014097345759490750307207/54806274927935495596891312029173862473687610099325998583092091915101265393157631244775387832621032230434007486602816972765210205596482942091518459849775458089575743699797096697542306049628939919466378189656065461708686421130956996965004776108499273871055615413566043083634090083096967567965373203683231171594922347812966905149873265654962896456451681332216735396249206784000', + '228506238533410805238253122961471637501498406104430046727793420497895852412513560203475440156216063574654372726194215013407325207763369075250819386897086702757125418327084312933061738392406321003231853349865003322697360659712771167066453430309997639548320673279964210730701373707765051246595806673865646607841876463608039111252607452843372970797479959213447237540830563469297144446069689149549515457318899981763489792/44418759813188470246596822974281932219088697005123285598909329328419879212164564561558656547678211265529803086702843380128457744445765859393426675396393053094588240457569380155526179040891009717323059641035189017918495485630657360525410506453554255734005459614858972904374739673834145083512160883402151636664208253116362089211658645312778404886460203600762546810897889629330022309705583438699250109493732452392578125', + '2396110297187570506257836426251930521245117960980667897194680897478996322590925380310917400763581787981992131769075961259393685331523600287377841990678117113566991100720996059187144934569703045966210306282348527729533806882408323961336166190162075154579344185244957673138372347997134388801085788353482276257401005734929059700346687783400772331280644647089520646562669857652648/465370154262863030800729180763091967573522779260407615567563274096037297593764955820626213827962697601334935773802201919483657310282236100250543992334865461800976838791035335861420892967699843608855185710753433852279737724446294443337363478679601049432404435672469098850676302728116369979460222148206944323730586138087862719386327396334468302608499235112520318322763857676375', + '1188272888075247520453568790651819187486389546755745961952033150576243575862120569399808467067800830890874391487822723879717525264875722720416173276963170672622214121308038530968291758445510982186014289745026439477197148445087614813262170975056866881909056450023324743409114115141145340480846088869807540798073417035168139476153897066325127540609756310068558610112731607522096096821938163134080426356894899245575410494080/230586762395914863625709048789913840511134004337945700368117795195769576856434588144361459740833007125516905017415184787078710001969815416281381427387949263792200577791644278234661952852149216540491852757174842146376788280677956350901082838474705227642420261641783722891948710833898248566893322144435457967452449965772543077421407816225704952270697016152452032506898311260979594550143344971416786595888053149375287120733', + '1072184299258468249004114536023934869518152618369895613271618698908405514630154819453979477077738526673716264830106745031385951736108376866253955319711026042389282009348594845497252059906901748860111986811345742998968284077119165082435598381810306691078037018432766116865792677955520465319184311529667642995217214507658693259694586107847099911341219913576038605872218680562047/207882129535607118520818565811162643811811563516006836924341123976848022970024793004700710992938987476728640943834012473687892481059618823520721737479364352887178017866425027953255856440099446112508663603319745066750522936408781591578321031173620445865113295407603090322283539974563690157418931060565698315406021569491751612761888641141446406531645379264720684248398299136000', + '158094023904374464057702010126993231151433474361820266584020400101533651587072432728050776506672527718975042616651177034635188178967462982926261516748842073587981953038445977514446838733557926696939591069712397947055261777416704028506441173540338327591514650398349069348381309688677335132100441905603664282735310299686268721634369502296867409514777359935022301425165750780988231639828675424239931533849896744198012747670272/30626384366513923671063955469537776291182594080083494737341604579575446319190916924682809546024369330706012653799783122328775875399363759308193432327358837772825609356553955350401725689682838701996446404559005896479618311357573203081153855961484398360670837599853294627153740224543175497655414585918231215639906924313779933211774291565425360415684556655021031131349317390382320286092667852173815259422737956767922154798375', + '12216278924286401897470540169822542417611356797676160128176706721343511598951866107858729405615631246003458062949723290192242594609994613705183460814797527214549142043573863947658832694800184691222426521703728394203722420336773310908687071670107794543022567943478843065246011544888390757810534211554666736239506503613750756246364621509359804902243487113673014588325806249988332/2364584127734680580481708368927921125934330000162935118371174269642831409084634375654239735054363928748162229294829526339927907778510530444646754603322373712788147193691037688552857183116332292048731726305451516954679277320777955342295564000586923227775208580772868590742136150431152495718132142742153643081187691849828217592954519485869135930755646768375299870967864990234375', + '578939195017447864385837341975558679292903716580114421030538728778491606282153588930983966100514147976321305616196463174070345884292672842497715966933722088536619333912687108737657068701352918582656105418491672377271304970407730786932913490869797471081081431072836814581099271832696990064828717287258860166842791984158634684078346453202928137987930328826212011774670396356151265781053547412633310327737900786639842674288716416/111966291649651600701875186827360807558960817589719096407887999108024557254221008849510152469119211567152616745769256123968832250104006560061907741567034087109512483357851919402017950812795426393415409198268246337542639470834168040295134130657073201020340432236723824670803498949071870597039309004562024033656488150406105175401372709011082725359382691270659245312133919518760328422827067578306005644825927050994860723405473375', + '8041514821545833865350873212853873042112343327573364523948780030849543753072688526717886590013028227714585965095216381829914996539132359247488639692828639170075726301411777307168737667077730792892326979446426349813642680537699905651125068091362135055902152994216233622733424128982011248923051410141033146604688083721413402395290842738021365861670415833356403364696759583009635/1553937833499559104910551811592138833189892464446761247731330672284191313590708439680460540329836344767557599746234213006493842842879398684745864919973051635127080738612248150108793397855113122368507483429209127035263504194847798809612278864438287815470711036548695032421148125849669175729331459149833945183228413961985381338162155596008680906676498435954178226973272371625984', + '12059659399309169167301353842123191409306995788633965680414620302285606896146667751472818541537183787935352700725982053536032930866708413010971856856768384974849695774437178790890015242323873769697407411558706626105140596160049735605389301414460825674631122462760014451298494875244754354054447697502521388381253293175367558014453865905104467382184997528879171136492277222127399012368677604910497789360212571238146627048080568400384/2328494054002703067219004484782931471919751585969487160602269605916676223315055450845942763690581877771159615428854802721943114463834992106300185328876910271612375509166975628540839858771078495267566008071532876816826751069408153323840515037497664871689085370699991860738368371510578076683395101062439493831773194498579620513073693022191702398601471237166387045790480861932005012807234071429628725981935147069738988682858417182625', + '683009426705008850682549700382901603742691705123356866338951397347368059105140826655486518416578697931244347554322654428162554903861783227680935341690740579867651545205562690172425016836559678937794312287639193745517811234257062356826622207475918293015140472942579713388671782202262396642735640315948612572908444501108579457520714632371056288874351015666995549139003946314085696/131769214246522869780429817236150406548549044658474416909806179734873399109656411633488099249198882165279745627038984487365589625562901914030850178330133080409420340227179447548044572594184601425691821617928130423410997853384425373999967759499571890326740433081063424903098252502791477854302204200941376983295622430177445507316706609802292121282957519571162158620971952084486375', + '1418116259749091420309271913909337143756589314597557128553117870573928055109322864794169592355058370891251425125187770724828108988587888087023982451205475980371440423992838481547236638456717520835668859093413956339261802305597694403381021863258753441889536361061407579332570493634254542013337999844407181346815549594836374497512447969751514466481466717590255369810506813543577735080228718660747940956248427356785784199073569408/273368882980516556922532339653303822104510203916024760371844491705599093435133738774975538364241630079468712981056336655766117378160822393876348755370711942273876284944422610974149712212944211513462921649872232656449940987552227525504529710373144228818606127541937831103642415534318303187393593236209298242574677293352798419428712873685798636474667204083938875672934235816396371477146089168375198141802684403955936431884765625', + '546411077581845999248238069684194831621691193366466972898628141511422317954477211865167402284211971296286346492698354214043840080181702652132835681239302798339667835439189275457116051279366194551632712819028177402221815140045210690908631767786711675029194359998826252332434573121740830827417293557234891406021102083555472155479148332697839972702631641910213821827820374586459319/105246945889799140338072754365901705544530661781295362563214285953881890233987516230568663143342952799036341701952267011236663118111226623815975847494372076420199418712941018288738590264475520229651370015377608638075866459684692488079223686296541020897904588347083045723167762682959422858996673894180805702299907478685374065593161226849447131100462022089783265346597553176576000', + '2398330640958841474772606439916070050977544535580605737383995160447105736276950196885906408317628083110923322157113892928963237845914017845444295040924101784423382681801754191301860383927129006953354739240926643562987838836997453985855576402628166875869041032631651591871962852884189548538272285387092843044669499688035134181859376665409767886188304314888753894905317929877238322615838524354191263502347881033855441181420399360/461588070868590122892265681879734295007029130965626060552783760068897000195207878227714842617470320231527222074701444349530952699708435668339712860464533455345665068841333232359698449088497137068713309811942968433868609329301082001752617420002377892756821532220676085014874112083615054550278903960627185675459015343606391094523511117705747842645927349130302549554534056269331809016770715819934970200483161548527932617036185253', + '6041015879424725383006424536130409209607854044642113747266098198777011981328765528361630516108680392500990580908509403483891763219659726090675140672989657743882183951954294745396417829943469201306594018454995862321821016087416840247422350906412007336103086620396467456181771583200365740253389107968122850063607085957109965406634738740996318415514360956028575560979203447735121436/1161752799109428422288020947061281540989708937450568100764830251908850596717606701047413407636907934320789870175907792017513896999208892282137299070761467096211814586909598705615312819596495636017728313513520193786266452836805291464826226833593878504804389728477191170027729963773716267868284479768397603444919008915279522376004326398403851684761808785381609370767169521034383625', + '13240077436443988749179508462267267187169441948722358165090554769250505713747934643200804819418670147225695324432684266924694524337920816452346599774452681831320005286326986675907899608537972384924882996757503264622991355949039882526389342174307168805166215838138277557052303430492669193939212362638263582899713198716541723383138016564027766560215944409353427176135895982596327685665844815618402881202645610620284792793420780517248/2544223084468158291883698813309541801455311468982232546872485444308211415529998472787377800559884210837213042932180479090277285630234238711851480232520137856848809986631784843528381778520727465146661792797924458540957133423665746229799675650290296217658444899605236550972043549278128087645211909479009099766619355677984218929672461506691980442071860591767266913041147587815452007726513853820116629482732060593116624596368806566625', + '1953999166296955830935495158735359200362904181792947794529339487489730042568305997099959302322956898299616194932283060554261566410988618045107398092345476532371402134206635235570281738377188438407703089325315446371127042537576093536896282955524842632708645655481028161471313608974238110718242273935956977555610147714316158486553633871312187084618154014921190595222799283957140353/375191165084882521037046014569185165885459082629136124177286500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', +]; + +if (typeof module !== 'undefined') { + module.exports = { + PRIMES, + PRIMES_SET, + LONG_PI, + LONG_E, + BIG_LOG_CACHE, + }; +} diff --git a/tools/ui/src/lib/vendors/nerdamer-prime/nerdamer.core.js b/tools/ui/src/lib/vendors/nerdamer-prime/nerdamer.core.js new file mode 100644 index 000000000..9fdcdff34 --- /dev/null +++ b/tools/ui/src/lib/vendors/nerdamer-prime/nerdamer.core.js @@ -0,0 +1,17838 @@ +/* + * Author : Martin Donk + * Website : http://www.nerdamer.com + * Email : martin.r.donk@gmail.com + * Source : https://github.com/jiggzson/nerdamer + */ + +// Type imports for JSDoc ====================================================== +// These typedefs provide type aliases for the interfaces defined in index.d.ts. +// They enable proper type checking when working with the classes defined in this file. +// +// Usage patterns: +// - For return types: @returns {NerdamerSymbolType} +// - For parameters: @param {NerdamerSymbolType} symbol +// - For variable declarations: /** @type {NerdamerSymbolType} */ +// +// Note: When casting local class instances to interface types, use the pattern: +// /** @type {InterfaceType} */ (/** @type {unknown} */ (localInstance)) +// This is needed because TypeScript sees local classes and interfaces as separate types. + +/** + * Core type aliases from index.d.ts + * + * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType + * + * @typedef {import('./index').NerdamerCore.Frac} FracType + * + * @typedef {import('./index').NerdamerCore.Vector} VectorType + * + * @typedef {import('./index').NerdamerCore.Matrix} MatrixType + * + * @typedef {import('./index').NerdamerCore.Parser} ParserType + * + * @typedef {import('./index').NerdamerCore.Collection} CollectionType + * + * @typedef {import('./index').NerdamerCore.NerdamerSet} SetType + * + * @typedef {import('./index').NerdamerCore.Settings} SettingsType + * + * @typedef {import('./index').NerdamerExpression} ExpressionType + * + * @typedef {typeof import('./index')} NerdamerType + * + * @typedef {import('./index').NerdamerCore.Token} TokenType + * + * @typedef {import('./index').NerdamerCore.ScopeArray} ScopeArrayType + * + * Arithmetic operand type (Symbol, Vector, or Matrix) + * + * @typedef {import('./index').ArithmeticOperand} ArithmeticOperand + * + * Expand options type + * + * @typedef {import('./index').ExpandOptions} ExpandOptions + * + * LaTeX token types + * + * @typedef {import('./index').LaTeXToken} LaTeXTokenType + * + * @typedef {import('./index').FilteredLaTeXToken} FilteredLaTeXTokenType + * + * Output and parameter types + * + * @typedef {import('./index').OutputType} OutputType + * + * @typedef {import('./index').ExpressionParam} ExpressionParam + * + * @typedef {import('./index').SortFn} SortFn + * + * Constructor types (for factory functions) + * + * @typedef {import('./index').NerdamerCore.FracConstructor} FracConstructor + * + * @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor + * + * @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor + * + * @typedef {import('./index').NerdamerCore.MatrixConstructor} MatrixConstructor + * + * @typedef {import('./index').NerdamerCore.ExpressionConstructor} ExpressionConstructor + * + * @typedef {import('./index').NerdamerCore.SetConstructor} SetConstructor + * + * @typedef {import('./index').NerdamerCore.CollectionConstructor} CollectionConstructor + * + * @typedef {import('./index').NerdamerCore.Fraction} FractionInterface + * + * @typedef {import('./index').NerdamerCore.ScientificConstructor} ScientificConstructor + * + * @typedef {import('./index').NerdamerCore.ParserConstructor} ParserConstructor + * + * @typedef {import('./index').NerdamerCore.LaTeX} LaTeXInterface + * + * @typedef {import('./index').NerdamerCore.Math2} Math2Interface + * + * @typedef {import('./index').NerdamerCore.Build} BuildInterface + * + * @typedef {import('./index').NerdamerCore.CoreUtils} CoreUtilsInterface + * + * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface + * + * @typedef {import('./index').NerdamerCore.InternalParseResult} InternalParseResult + * + * Exceptions object type (for CoreDeps.exceptions) + * + * @typedef {{ + * DivisionByZero: CustomErrorConstructor; + * ParseError: CustomErrorConstructor; + * OutOfFunctionDomainError: CustomErrorConstructor; + * UndefinedError: CustomErrorConstructor; + * MaximumIterationsReached: CustomErrorConstructor; + * NerdamerTypeError: CustomErrorConstructor; + * ParityError: CustomErrorConstructor; + * OperatorError: CustomErrorConstructor; + * OutOfRangeError: CustomErrorConstructor; + * DimensionError: CustomErrorConstructor; + * InvalidVariableNameError: CustomErrorConstructor; + * ValueLimitExceededError: CustomErrorConstructor; + * NerdamerValueError: CustomErrorConstructor; + * SolveError: CustomErrorConstructor; + * InfiniteLoopError: CustomErrorConstructor; + * UnexpectedTokenError: CustomErrorConstructor; + * }} ExceptionsType + * Exception types + * + * @typedef {import('./index').NerdamerCore.DivisionByZero} DivisionByZeroType + * + * @typedef {import('./index').NerdamerCore.ParseError} ParseErrorType + * + * @typedef {import('./index').NerdamerCore.NerdamerTypeError} NerdamerTypeErrorType + * + * @typedef {import('./index').NerdamerCore.Core} CoreType + * + * @typedef {import('./index').NerdamerCore.PowerValue} PowerValueType + * + * @typedef {import('big-integer').BigInteger} BigIntegerType + * + * @typedef {import('big-integer').BigIntegerStatic} BigIntegerStaticType + * + * @typedef {import('decimal.js').Decimal} DecimalType + * + * @typedef {typeof import('decimal.js').default} DecimalStaticType + * + * Custom error constructor type - used for exception classes + * + * @typedef {new (message?: string) => Error} CustomErrorConstructor + * + * @typedef {Record< + * string, + * [Function, number] | [Function, number[]] | [Function, number, { name: string; params: string[]; body: string }] + * >} FunctionMapType + */ + +// externals ==================================================================== +/* BigInteger.js v1.6.28 https://github.com/peterolson/BigInteger.js/blob/master/LICENSE */ +const nerdamerBigInt = + typeof globalThis.nerdamerBigInt === 'undefined' ? require('big-integer') : globalThis.nerdamerBigInt; +/* Decimal.js v10.2.1 https://github.com/MikeMcl/decimal.js/LICENCE */ +const nerdamerBigDecimal = + typeof globalThis.nerdamerBigDecimal === 'undefined' ? require('decimal.js') : globalThis.nerdamerBigDecimal; + +// Set BigDecimal precision immediately after import +nerdamerBigDecimal.set({ precision: 250 }); + +/* Mathematical constants */ +const nerdamerConstants = + typeof globalThis.nerdamerConstants === 'undefined' ? require('./constants.js') : globalThis.nerdamerConstants; + +// ============================================================================ +// Runtime state variables - declared before CoreDeps to avoid forward references +// ============================================================================ +// Custom operators registry - populated by IIFE +/** @type {{ [key: string]: { precedence: number; operator: string; action: string; postfix?: boolean } }} */ +const CUSTOM_OPERATORS = {}; + +// Runtime state arrays - used by CoreDeps.state getters +/** @type {ExpressionType[]} */ +const EXPRESSIONS = []; +/** @type {Record} */ +const VARS_STORE = {}; +/** @type {string[]} */ +const RESERVED = []; +/** @type {string[]} */ +const WARNINGS = []; +/** @type {string[]} */ +const USER_FUNCTIONS = []; + +// Late-binding references container - populated after classes are defined +// Used by CoreDeps getters to avoid forward reference issues +/** + * @type {{ + * Settings: SettingsType | null; + * Math2: Math2Interface | null; + * }} + */ +const LateRefs = { + Settings: /** @type {SettingsType | null} */ (null), + Math2: /** @type {Math2Interface | null} */ (null), +}; + +// CoreDeps - Centralized Dependency Registry ================================== +// This single registry replaces 45+ scattered *Deps objects with a unified, +// hierarchical structure. Benefits: +// - Single source of truth for all shared dependencies +// - Clear initialization order (externals -> constants -> classes -> parser) +// - Lazy getters for values defined later in initialization +// - Type-safe access patterns +// +// Structure: +// CoreDeps.ext - External imports (bigInt, bigDec, constants) +// CoreDeps.groups - Symbol group constants (N, P, S, EX, FN, PL, CB, CP) +// CoreDeps.fnNames - Function name constants (SQRT, ABS, FACTORIAL, etc.) +// CoreDeps.settings - Settings reference +// CoreDeps.state - Runtime state (EXPRESSIONS, VARS, RESERVED, etc.) +// CoreDeps.classes - Class constructors (Frac, NerdamerSymbol, Vector, etc.) +// CoreDeps.utils - Utility functions +// CoreDeps.parser - Parser instance (set during IIFE init) +// CoreDeps.core - Core object C (set during IIFE init) + +/** + * @type {{ + * ext: { + * bigInt: BigIntegerStaticType; + * bigDec: DecimalStaticType; + * PRIMES: number[]; + * PRIMES_SET: Record; + * LONG_PI: string; + * LONG_E: string; + * BIG_LOG_CACHE: string[]; + * }; + * groups: { + * N: 1; + * P: 2; + * S: 3; + * EX: 4; + * FN: 5; + * PL: 6; + * CB: 7; + * CP: 8; + * }; + * fnNames: { + * SQRT: 'sqrt'; + * ABS: 'abs'; + * FACTORIAL: 'factorial'; + * DOUBLEFACTORIAL: 'dfactorial'; + * PARENTHESIS: 'parens'; + * LOG: 'log'; + * CONST_HASH: '#'; + * }; + * settings: SettingsType; + * state: { + * EXPRESSIONS: ExpressionType[]; + * VARS: Record; + * CONSTANTS: Record; + * RESERVED: string[]; + * WARNINGS: string[]; + * USER_FUNCTIONS: string[]; + * CUSTOM_OPERATORS: { + * [key: string]: { precedence: number; operator: string; action: string; postfix?: boolean }; + * }; + * }; + * classes: { + * Frac: FracConstructor; + * Fraction: FractionInterface; + * NerdamerSymbol: SymbolConstructor; + * Vector: VectorConstructor; + * Matrix: MatrixConstructor; + * Expression: ExpressionConstructor; + * Collection: CollectionConstructor; + * NerdamerSet: SetConstructor; + * Scientific: ScientificConstructor; + * Parser: ParserConstructor; + * LaTeX: LaTeXInterface; + * Math2: Math2Interface; + * Build: BuildInterface; + * }; + * utils: { + * isSymbol: (x: unknown) => boolean; + * isVector: (x: unknown) => boolean; + * isMatrix: (x: unknown) => boolean; + * isExpression: (x: unknown) => boolean; + * isNumericSymbol: (symbol: NerdamerSymbolType) => boolean; + * isFraction: (x: unknown) => boolean; + * isArray: (arr: unknown) => boolean; + * isInt: (n: number | string | unknown) => boolean; + * text: (symbol: NerdamerSymbolType, opt?: OutputType, useGroup?: number, decp?: number) => string; + * variables: (obj: NerdamerSymbolType | FracType, poly?: boolean, vars?: unknown) => string[]; + * scientificToDecimal: (num: number) => string; + * err: (msg: string, ErrorObj?: CustomErrorConstructor) => void; + * block: (setting: string, f: Function, opt?: boolean, obj?: unknown) => unknown; + * evaluate: (symbol: NerdamerSymbolType, o?: Record) => NerdamerSymbolType; + * reserveNames: (obj: object) => void; + * nround: (x: string | number, s?: number) => string | number; + * remove: (arr: unknown[], index: number) => unknown; + * _setFunction: (fnName: string | Function, fnParams?: string[], fnBody?: string) => boolean; + * _clearFunctions: () => void; + * symfunction: (fname: string, args: NerdamerSymbolType[]) => NerdamerSymbolType; + * callfunction: (fname: string, args: NerdamerSymbolType[]) => NerdamerSymbolType; + * }; + * exceptions: ExceptionsType; + * parser: ParserType; + * core: CoreType; + * libExports: typeof nerdamer; + * version: string; + * }} + */ +const CoreDeps = { + // External imports - available immediately + ext: { + bigInt: nerdamerBigInt, + bigDec: nerdamerBigDecimal, + PRIMES: nerdamerConstants.PRIMES, + PRIMES_SET: nerdamerConstants.PRIMES_SET, + LONG_PI: nerdamerConstants.LONG_PI, + LONG_E: nerdamerConstants.LONG_E, + BIG_LOG_CACHE: nerdamerConstants.BIG_LOG_CACHE, + }, + + // Symbol group constants - available immediately + groups: { + N: 1, // A number + P: 2, // A number with a rational power e.g. 2^(3/5) + S: 3, // A single variable e.g. x + EX: 4, // An exponential + FN: 5, // A function + PL: 6, // Same name, different powers e.g. 1/x + x^2 + CB: 7, // Multiplication composite e.g. x*y + CP: 8, // Addition composite e.g. x+1 or x+y + }, + + // Function name constants - available immediately + fnNames: { + SQRT: 'sqrt', + ABS: 'abs', + FACTORIAL: 'factorial', + DOUBLEFACTORIAL: 'dfactorial', + PARENTHESIS: 'parens', + LOG: 'log', + CONST_HASH: '#', + }, + + // Settings reference - getter using LateRefs for forward reference safety + get settings() { + return LateRefs.Settings; + }, + + // Runtime state - arrays now defined before CoreDeps + state: { + get EXPRESSIONS() { + return EXPRESSIONS; + }, + get VARS() { + return VARS_STORE; + }, + CONSTANTS: /** @type {Record} */ ({}), + get RESERVED() { + return RESERVED; + }, + get WARNINGS() { + return WARNINGS; + }, + get USER_FUNCTIONS() { + return USER_FUNCTIONS; + }, + get CUSTOM_OPERATORS() { + return CUSTOM_OPERATORS; + }, + }, + + // Class constructors - set by IIFE after class definitions + classes: { + Frac: /** @type {FracConstructor} */ (null), + Fraction: /** @type {FractionInterface} */ (null), + NerdamerSymbol: /** @type {SymbolConstructor} */ (null), + Vector: /** @type {VectorConstructor} */ (null), + Matrix: /** @type {MatrixConstructor} */ (null), + Expression: /** @type {ExpressionConstructor} */ (null), + Collection: /** @type {CollectionConstructor} */ (null), + NerdamerSet: /** @type {SetConstructor} */ (null), + Scientific: /** @type {ScientificConstructor} */ (null), + Parser: /** @type {ParserConstructor} */ (null), + LaTeX: /** @type {LaTeXInterface} */ (null), + Math2: /** @type {Math2Interface} */ (null), + Build: /** @type {BuildInterface} */ (null), + }, + + // Utility functions - use getters for module-scope functions + // symfunction and callfunction are set by IIFE since they need parser binding + utils: { + get isSymbol() { + return isSymbol; + }, + get isVector() { + return isVector; + }, + get isMatrix() { + return isMatrix; + }, + get isExpression() { + return isExpression; + }, + get isNumericSymbol() { + return isNumericSymbol; + }, + get isFraction() { + return isFraction; + }, + get isArray() { + return isArray; + }, + get isInt() { + return isInt; + }, + get text() { + return text; + }, + get variables() { + return variables; + }, + get scientificToDecimal() { + return scientificToDecimal; + }, + get err() { + return err; + }, + get block() { + return block; + }, + get evaluate() { + return evaluate; + }, + get reserveNames() { + return reserveNames; + }, + get nround() { + return nround; + }, + get remove() { + return remove; + }, + get _setFunction() { + return _setFunction; + }, + get _clearFunctions() { + return _clearFunctions; + }, + // Parser-bound methods - set by IIFE after parser instantiation + symfunction: /** @type {(fname: string, args: NerdamerSymbolType[]) => NerdamerSymbolType} */ (null), + callfunction: /** @type {(fname: string, args: NerdamerSymbolType[]) => NerdamerSymbolType} */ (null), + }, + + // Exception classes - assigned after exception definitions (see below Frac class) + exceptions: /** @type {ExceptionsType} */ (null), + + // Parser instance - set by IIFE after Parser creation + parser: /** @type {ParserType} */ (null), + + // Core object C - set by IIFE at end + core: /** @type {CoreType} */ (null), + + // Library exports function - set by IIFE + libExports: /** @type {typeof nerdamer} */ (null), + + // Version string + version: '1.1.16', +}; + +// Groups object - maps to CoreDeps.groups for external access +const Groups = { + N: CoreDeps.groups.N, + P: CoreDeps.groups.P, + S: CoreDeps.groups.S, + EX: CoreDeps.groups.EX, + FN: CoreDeps.groups.FN, + PL: CoreDeps.groups.PL, + CB: CoreDeps.groups.CB, + CP: CoreDeps.groups.CP, +}; + +// ============================================================================ +// Math Polyfills +// ============================================================================ +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ +Math.sign ||= function sign(x) { + x = Number(x); // Convert to a number + if (x === 0 || isNaN(x)) { + return x; + } + return x > 0 ? 1 : -1; +}; + +Math.cosh ||= function cosh(x) { + const y = Math.exp(x); + return (y + 1 / y) / 2; +}; + +Math.sech ||= function sech(x) { + return 1 / Math.cosh(x); +}; + +Math.csch ||= function csch(x) { + return 1 / Math.sinh(x); +}; + +Math.coth ||= function coth(x) { + return 1 / Math.tanh(x); +}; + +Math.sinh ||= function sinh(x) { + const y = Math.exp(x); + return (y - 1 / y) / 2; +}; + +Math.tanh ||= function tanh(x) { + if (x === Infinity) { + return 1; + } + if (x === -Infinity) { + return -1; + } + const y = Math.exp(2 * x); + return (y - 1) / (y + 1); +}; + +Math.asinh ||= function asinh(x) { + if (x === -Infinity) { + return x; + } + return Math.log(x + Math.sqrt(x * x + 1)); +}; + +Math.acosh ||= function acosh(x) { + return Math.log(x + Math.sqrt(x * x - 1)); +}; + +Math.atanh ||= function atanh(x) { + return Math.log((1 + x) / (1 - x)) / 2; +}; + +Math.trunc ||= function trunc(x) { + if (isNaN(x)) { + return NaN; + } + if (x > 0) { + return Math.floor(x); + } + return Math.ceil(x); +}; + +// ============================================================================ +// Scientific notation helper +// ============================================================================ +// Extracted as standalone function to avoid forward-reference to Scientific class + +/** + * Checks if a string is in scientific notation (e.g., "1.5e10", "2E-5") + * + * @param {string} num + * @returns {boolean} + */ +function isScientificNotation(num) { + return /\d+\.?\d*e[+-]*\d+/iu.test(num); +} + +// Fraction Object ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// This static utility object converts decimals to fractions. + +/** Static utility object for converting decimals to fractions. */ +const Fraction = { + /** + * Converts a decimal to a fraction + * + * @param {number | string} value + * @param {object} [_opts] + * @returns {Array} An array containing the numerator and the denominator + */ + convert(value, _opts) { + const numValue = Number(value); + let frac; + if (numValue === 0) { + frac = [0, 1]; + } else if (Math.abs(numValue) < 1e-6 || Math.abs(numValue) > 1e20) { + const qc = this.quickConversion(numValue); + if (qc[1] <= 1e16) { + const abs = Math.abs(numValue); + const sign = numValue / abs; + frac = this.fullConversion(abs.toFixed(`${qc[1]}`.length - 1)); + frac[0] *= sign; + } else { + frac = qc; + } + } else { + frac = this.fullConversion(numValue); + } + return frac; + }, + /** + * If the fraction is too small or too large this gets called instead of fullConversion method + * + * @param {number | string} value + * @returns {Array} An array containing the numerator and the denominator as strings + */ + quickConversion(value) { + const stripSign = function (s) { + // Explicitely convert to a string + if (typeof s !== 'string') { + s = s.toString(); + } + + let sign = ''; + + // Remove and store the sign + const start = s.charAt(0); + if (start === '-') { + s = s.substr(1, s.length); + sign = '-'; + } else if (start === '+') { + // Just remove the plus sign + s = s.substr(1, s.length); + } + + return { + sign, + value: s, + }; + }; + + function convert(val) { + // Explicitely convert to a decimal + if (isScientificNotation(val)) { + val = scientificToDecimal(val); + } + + // Split the value into the sign and the value + const nparts = stripSign(val); + + // Split it at the decimal. We'll refer to it as the coeffient parts + const cparts = nparts.value.split('.'); + + // Combine the entire number by removing leading zero and adding the decimal part + // This would be teh same as moving the decimal point to the end + let num; + // We're dealing with integers + if (cparts.length === 1) { + num = cparts[0]; + } else { + num = cparts[0] + cparts[1]; + } + const n = cparts[1] ? cparts[1].length : 0; + // Generate the padding for the zeros + const den = `1${'0'.repeat(n)}`; + + if (num !== '0') { + num = num.replace(/^0+/u, ''); + } + return [nparts.sign + num, den]; + } + + return convert(value); + }, + /** + * Returns a good approximation of a fraction. This method gets called by convert + * http://mathforum.org/library/drmath/view/61772.html Decimal To Fraction Conversion - A Simpler Version Dr + * Peterson + * + * @param {number | string} dec + * @returns {Array} An array containing the numerator and the denominator + */ + fullConversion(dec) { + const numDec = Number(dec); + // This doesn't work for values approaching as small as epsilon + const epsilon = Math.abs(numDec) > 1e10 ? 1e-16 : 1e-30; + let done = false; + // You can adjust the epsilon to a larger number if you don't need very high precision + let n1 = 0; + let d1 = 1; + let n2 = 1; + let d2 = 0; + let n = 0; + let q = numDec; + let num; + let den; + // Relative epsilon for rounding large q values to nearest integer. + // This fixes floating-point precision errors in reciprocals (e.g., 1/1e-15 = 999999999999999.9). + // We use ~45x Number.EPSILON to allow for accumulated rounding errors. + // This is independent of Settings.PRECISION since we're dealing with IEEE 754 double limits. + const roundingEpsilon = 1e-14; // ~45 * Number.EPSILON (2.2e-16) + while (!done) { + n++; + // For very large q values, round to nearest integer if within floating-point error + let a; + if (Math.abs(q) > 1e10) { + const rounded = Math.round(q); + const relDiff = Math.abs(q - rounded) / Math.abs(rounded); + a = relDiff < roundingEpsilon ? rounded : Math.floor(q); + } else { + a = Math.floor(q); + } + num = n1 + a * n2; + den = d1 + a * d2; + const e = q - a; + if (e < epsilon) { + done = true; + } + q = 1 / e; + n1 = n2; + d1 = d2; + n2 = num; + d2 = den; + if (Math.abs(num / den - numDec) < epsilon || n > 30) { + done = true; + } + } + return [num, den]; + }, +}; + +// Assign Fraction to CoreDeps immediately +CoreDeps.classes.Fraction = /** @type {FractionInterface} */ (/** @type {unknown} */ (Fraction)); + +// CustomError Function ============================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// This function creates custom error classes. + +/** + * Creates a custom error class with the given name. + * + * @param {string} name - The name of the custom error class + * @returns {new (message?: string) => Error} A custom error constructor + */ +function customError(name) { + const E = function (message) { + this.name = name; + this.message = message === undefined ? '' : message; + const error = new Error(this.message); + error.name = this.name; + this.stack = error.stack; + }; // Create an empty error + E.prototype = Object.create(Error.prototype); + return E; +} + +// DivisionByZero Error ================================================================ +/** + * Error thrown for division by zero. + * + * @type {new (message?: string) => Error} + */ +const DivisionByZero = customError('DivisionByZero'); + +// ParseError Error ==================================================================== +/** + * Error thrown if an error occurred during parsing. + * + * @type {new (message?: string) => Error} + */ +const ParseError = customError('ParseError'); + +// UndefinedError Error ================================================================ +/** + * Error thrown if the expression results in undefined. + * + * @type {new (message?: string) => Error} + */ +const UndefinedError = customError('UndefinedError'); + +// OutOfFunctionDomainError Error ====================================================== +/** + * Error thrown if input is out of the function domain. + * + * @type {new (message?: string) => Error} + */ +const OutOfFunctionDomainError = customError('OutOfFunctionDomainError'); + +// MaximumIterationsReached Error ====================================================== +/** + * Error thrown if a function exceeds maximum iterations. + * + * @type {new (message?: string) => Error} + */ +const MaximumIterationsReached = customError('MaximumIterationsReached'); + +// NerdamerTypeError Error ============================================================= +/** + * Error thrown if the parser receives an incorrect type. + * + * @type {new (message?: string) => Error} + */ +const NerdamerTypeError = customError('NerdamerTypeError'); + +// ParityError Error =================================================================== +/** + * Error thrown if bracket parity is not correct. + * + * @type {new (message?: string) => Error} + */ +const ParityError = customError('ParityError'); + +// OperatorError Error ================================================================= +/** + * Error thrown if an unexpected or incorrect operator is encountered. + * + * @type {new (message?: string) => Error} + */ +const OperatorError = customError('OperatorError'); + +// OutOfRangeError Error =============================================================== +/** + * Error thrown if an index is out of range. + * + * @type {new (message?: string) => Error} + */ +const OutOfRangeError = customError('OutOfRangeError'); + +// DimensionError Error ================================================================ +/** + * Error thrown if dimensions are incorrect (mostly for matrices). + * + * @type {new (message?: string) => Error} + */ +const DimensionError = customError('DimensionError'); + +// InvalidVariableNameError Error ====================================================== +/** + * Error thrown if variable name violates naming rule. + * + * @type {new (message?: string) => Error} + */ +const InvalidVariableNameError = customError('InvalidVariableNameError'); + +// ValueLimitExceededError Error ======================================================= +/** + * Error thrown if the limits of the library are exceeded for a function. + * + * @type {new (message?: string) => Error} + */ +const ValueLimitExceededError = customError('ValueLimitExceededError'); + +// NerdamerValueError Error ============================================================ +/** + * Error thrown if the value is an incorrect LH or RH value. + * + * @type {new (message?: string) => Error} + */ +const NerdamerValueError = customError('NerdamerValueError'); + +// SolveError Error ==================================================================== +/** + * Error thrown for solve-related errors. + * + * @type {new (message?: string) => Error} + */ +const SolveError = customError('SolveError'); + +// InfiniteLoopError Error ============================================================= +/** + * Error thrown for an infinite loop. + * + * @type {new (message?: string) => Error} + */ +const InfiniteLoopError = customError('InfiniteLoopError'); + +// UnexpectedTokenError Error ========================================================== +/** + * Error thrown if an operator is found when there shouldn't be one. + * + * @type {new (message?: string) => Error} + */ +const UnexpectedTokenError = customError('UnexpectedTokenError'); + +// Assign CoreDeps.exceptions now that all exception classes are defined +CoreDeps.exceptions = { + DivisionByZero, + ParseError, + OutOfFunctionDomainError, + UndefinedError, + MaximumIterationsReached, + NerdamerTypeError, + ParityError, + OperatorError, + OutOfRangeError, + DimensionError, + InvalidVariableNameError, + ValueLimitExceededError, + NerdamerValueError, + SolveError, + InfiniteLoopError, + UnexpectedTokenError, +}; + +// Frac Class =================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are accessed via CoreDeps for centralized management. + +/** + * Dependency accessor for Frac class. Uses CoreDeps as the single source of truth for all dependencies. + * + * Note: bigInt and bigDec are typed as BigIntegerStaticType and DecimalStaticType. While these types don't expose + * constructor signatures in TypeScript, the libraries support 'new' at runtime. Type assertions are used at call + * sites. + * + * @type {{ + * bigInt: BigIntegerStaticType; + * bigDec: DecimalStaticType; + * isInt: (n: number | string | unknown) => boolean; + * scientificToDecimal: (num: number) => string; + * DivisionByZero: CustomErrorConstructor; + * Settings: SettingsType; + * Fraction: typeof Fraction; + * }} + */ +const FracDeps = { + get bigInt() { + return CoreDeps.ext.bigInt; + }, + get bigDec() { + return CoreDeps.ext.bigDec; + }, + get isInt() { + return CoreDeps.utils.isInt; + }, + get scientificToDecimal() { + return CoreDeps.utils.scientificToDecimal; + }, + get DivisionByZero() { + return DivisionByZero; + }, + get Settings() { + return CoreDeps.settings; + }, + get Fraction() { + return Fraction; + }, +}; + +/** + * High-precision fraction class. + * + * @implements {FracType} + */ +class Frac { + /** @type {BigIntegerType} */ + num; + /** @type {BigIntegerType} */ + den; + + /** @param {number | string | Frac} [n] */ + constructor(n) { + if (n instanceof Frac) { + // eslint-disable-next-line no-constructor-return -- Frac is designed to return existing instances + return n; + } + if (n === undefined) { + // eslint-disable-next-line no-constructor-return -- Early return for undefined + return this; + } + try { + if (FracDeps.isInt(n)) { + try { + // @ts-expect-error - bigInt accepts string | number at runtime + this.num = FracDeps.bigInt(n); + this.den = FracDeps.bigInt(1); + } catch (e) { + if (/** @type {Error} */ (e).message === 'timeout') { + throw e; + } + // eslint-disable-next-line no-constructor-return -- Fallback to simple parsing + return Frac.simple(n); + } + } else { + const frac = + /** @type {unknown} */ (n) instanceof FracDeps.bigDec + ? FracDeps.Fraction.quickConversion(n) + : FracDeps.Fraction.convert(n); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + this.num = new FracDeps.bigInt(frac[0]); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + this.den = new FracDeps.bigInt(frac[1]); + } + } catch (e) { + if (/** @type {Error} */ (e).message === 'timeout') { + throw e; + } + // eslint-disable-next-line no-constructor-return -- Fallback to simple parsing + return Frac.simple(n); + } + } + + /** + * Safe to use with negative numbers or other types + * + * @param {number | string | FracType} n + * @returns {FracType} + */ + static create(n) { + if (n instanceof Frac) { + return n; + } + n = n.toString(); + const isNeg = n.charAt(0) === '-'; + if (isNeg) { + n = n.substr(1, n.length - 1); + } + const frac = new Frac(n); + if (isNeg) { + frac.negate(); + } + return frac; + } + + /** + * @param {unknown} o + * @returns {o is FracType} + */ + static isFrac(o) { + return o instanceof Frac; + } + + /** + * @param {string | number} n + * @param {string | number} d + * @returns {FracType} + */ + static quick(n, d) { + const frac = new Frac(); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + frac.num = new FracDeps.bigInt(n); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + frac.den = new FracDeps.bigInt(d); + return frac; + } + + /** + * @param {number | string} n + * @returns {FracType} + */ + static simple(n) { + const nstr = String(FracDeps.scientificToDecimal(/** @type {number} */ (n))); + const mDc = nstr.split('.'); + const num = mDc.join(''); + /** @type {string} */ + let den = '1'; + const l = (mDc[1] || '').length; + for (let i = 0; i < l; i++) { + den += '0'; + } + const frac = Frac.quick(num, den); + return frac.simplify(); + } + + /** + * @param {FracType} m + * @returns {FracType} + */ + multiply(m) { + if (this.isOne()) { + return m.clone(); + } + if (m.isOne()) { + return this.clone(); + } + + const c = this.clone(); + c.num = c.num.multiply(m.num); + c.den = c.den.multiply(m.den); + + return c.simplify(); + } + + /** + * @param {FracType} m + * @returns {FracType} + */ + divide(m) { + if (m.equals(0)) { + throw new FracDeps.DivisionByZero('Division by zero not allowed!'); + } + return this.clone().multiply(m.clone().invert()).simplify(); + } + + /** + * @param {FracType} m + * @returns {FracType} + */ + subtract(m) { + return this.clone().add(m.clone().neg()); + } + + /** + * Alias for subtract + * + * @param {FracType} m + * @returns {FracType} + */ + sub(m) { + return this.subtract(m); + } + + /** @returns {this} */ + neg() { + this.num = this.num.multiply(-1); + return this; + } + + /** + * @param {FracType} m + * @returns {FracType} + */ + add(m) { + const n1 = this.den; + const n2 = m.den; + const c = this.clone(); + const a = c.num; + const b = m.num; + if (n1.equals(n2)) { + c.num = a.add(b); + } else { + c.num = a.multiply(n2).add(b.multiply(n1)); + c.den = n1.multiply(n2); + } + + return c.simplify(); + } + + /** + * @param {FracType} m + * @returns {FracType} + */ + mod(m) { + const a = this.clone(); + const b = m.clone(); + a.num = a.num.multiply(b.den); + a.den = a.den.multiply(b.den); + b.num = b.num.multiply(this.den); + b.den = b.den.multiply(this.den); + a.num = a.num.mod(b.num); + return a.simplify(); + } + + /** @returns {this} */ + simplify() { + const gcd = FracDeps.bigInt.gcd(this.num, this.den); + this.num = this.num.divide(gcd); + this.den = this.den.divide(gcd); + return this; + } + + /** @returns {FracType} */ + clone() { + const m = new Frac(); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + m.num = new FracDeps.bigInt(this.num); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + m.den = new FracDeps.bigInt(this.den); + return m; + } + + /** + * @param {number} [prec] + * @returns {string} + */ + decimal(prec) { + const sign = this.num.isNegative() ? '-' : ''; + if (this.num.equals(this.den)) { + return '1'; + } + prec ||= FracDeps.Settings.PRECISION; + prec += 2; + const narr = []; + let n = this.num.abs(); + const d = this.den; + let i; + for (i = 0; i < prec; i++) { + const w = n.divide(d); + const r = n.subtract(w.multiply(d)); + narr.push(w); + if (r.equals(0)) { + break; + } + n = r.times(10); + } + const whole = narr.shift(); + if (narr.length === 0) { + return sign + whole.toString(); + } + + if (i === prec) { + const lt = []; + for (let j = 0; j < 2; j++) { + lt.unshift(narr.pop()); + } + narr.push(Math.round(Number(lt.join('.')))); + } + + const dec = `${whole.toString()}.${narr.join('')}`; + return sign + dec; + } + + /** + * @param {number} [prec] + * @returns {string | number} + */ + toDecimal(prec) { + prec ||= FracDeps.Settings.PRECISION; + if (prec) { + return this.decimal(prec); + } + return this.num.valueOf() / this.den.valueOf(); + } + + /** + * @param {FracType} n + * @returns {[BigIntegerType, BigIntegerType]} + */ + qcompare(n) { + return [this.num.multiply(n.den), n.num.multiply(this.den)]; + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + equals(n) { + if (!isNaN(/** @type {number} */ (n))) { + n = new Frac(/** @type {number} */ (n)); + } + const q = this.qcompare(/** @type {FracType} */ (n)); + return q[0].equals(q[1]); + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + absEquals(n) { + if (!isNaN(/** @type {number} */ (n))) { + n = new Frac(/** @type {number} */ (n)); + } + const q = this.qcompare(/** @type {FracType} */ (n)); + return q[0].abs().equals(q[1]); + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + greaterThan(n) { + if (!isNaN(/** @type {number} */ (n))) { + n = new Frac(/** @type {number} */ (n)); + } + const q = this.qcompare(/** @type {FracType} */ (n)); + return q[0].gt(q[1]); + } + + /** + * Alias for greaterThan + * + * @param {number | FracType} n + * @returns {boolean} + */ + gt(n) { + return this.greaterThan(n); + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + gte(n) { + return this.greaterThan(n) || this.equals(n); + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + lte(n) { + return this.lessThan(n) || this.equals(n); + } + + /** + * @param {number | FracType} n + * @returns {boolean} + */ + lessThan(n) { + if (!isNaN(/** @type {number} */ (n))) { + n = new Frac(/** @type {number} */ (n)); + } + const q = this.qcompare(/** @type {FracType} */ (n)); + return q[0].lt(q[1]); + } + + /** + * Alias for lessThan + * + * @param {number | FracType} n + * @returns {boolean} + */ + lt(n) { + return this.lessThan(n); + } + + /** @returns {boolean} */ + isInteger() { + return this.den.equals(1); + } + + /** @returns {this} */ + negate() { + this.num = this.num.multiply(-1); + return this; + } + + /** @returns {this} */ + invert() { + const t = this.den; + if (!this.num.equals(0)) { + const isnegative = this.num.isNegative(); + this.den = this.num.abs(); + this.num = t; + if (isnegative) { + this.num = this.num.multiply(-1); + } + } + return this; + } + + /** @returns {boolean} */ + isOne() { + return this.num.equals(1) && this.den.equals(1); + } + + /** @returns {-1 | 1} */ + sign() { + return this.num.isNegative() ? -1 : 1; + } + + /** @returns {this} */ + abs() { + this.num = this.num.abs(); + return this; + } + + /** + * @param {FracType} f + * @returns {FracType} + */ + gcd(f) { + // @ts-expect-error - bigInt.gcd accepts BigInteger at runtime + return Frac.quick(FracDeps.bigInt.gcd(f.num, this.num), FracDeps.bigInt.lcm(f.den, this.den)); + } + + /** @returns {string} */ + toString() { + return this.den.equals(1) ? this.num.toString() : `${this.num.toString()}/${this.den.toString()}`; + } + + /** @returns {number | DecimalType} */ + valueOf() { + if (FracDeps.Settings.USE_BIG) { + return new FracDeps.bigDec(this.num.toString()).div(new FracDeps.bigDec(this.den.toString())); + } + const retval = this.num.valueOf() / this.den.valueOf(); + return retval; + } + + /** @returns {boolean} */ + isNegative() { + return /** @type {number} */ (this.toDecimal()) < 0; + } + + /** + * Checks if this fraction contains the given number (i.e., is divisible by it) + * + * @param {number | FracType} n + * @returns {boolean} + */ + contains(n) { + const fracN = typeof n === 'number' ? new Frac(n) : n; + return this.mod(fracN).equals(0); + } +} + +// Assign Frac to CoreDeps immediately for early access +CoreDeps.classes.Frac = Frac; + +// NerdamerSet Class ==================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are accessed via CoreDeps for centralized management. + +/** + * Dependency accessor for NerdamerSet class. Uses CoreDeps as the single source of truth. + * + * @type {{ + * isVector: (x: unknown) => boolean; + * Vector: VectorConstructor; + * remove: (arr: unknown[], index: number) => unknown; + * }} + */ +const SetDeps = { + get isVector() { + return CoreDeps.utils.isVector; + }, + get Vector() { + return CoreDeps.classes.Vector; + }, + get remove() { + return remove; + }, // Remove is defined later in file +}; + +/** + * NerdamerSet class for mathematical set operations. + * + * @implements {SetType} + */ +class NerdamerSet { + /** @type {NerdamerSymbolType[]} */ + elements = []; + + /** + * @param {VectorType | NerdamerSymbolType | undefined} [setArg] + * @param {...NerdamerSymbolType} rest + */ + constructor(setArg, ...rest) { + // If the first object isn't an array, convert it to one. + if (typeof setArg === 'undefined') { + // No arguments passed + return; + } + let setVal = /** @type {VectorType} */ (setArg); + if (!SetDeps.isVector(setArg)) { + setVal = SetDeps.Vector.fromArray([/** @type {NerdamerSymbolType} */ (setArg), ...rest]); + } + + if (setVal) { + const { elements } = setVal; + for (let i = 0, l = elements.length; i < l; i++) { + this.add(/** @type {NerdamerSymbolType} */ (elements[i])); + } + } + } + + /** + * @param {NerdamerSymbolType[]} arr + * @returns {SetType} + */ + static fromArray(arr) { + const newSet = new NerdamerSet(); + for (const item of arr) { + newSet.add(item); + } + return newSet; + } + + /** @param {NerdamerSymbolType} x */ + add(x) { + if (!this.contains(x)) { + this.elements.push(x.clone()); + } + } + + /** + * @param {NerdamerSymbolType} x + * @returns {boolean} + */ + contains(x) { + for (let i = 0; i < this.elements.length; i++) { + const e = this.elements[i]; + if (x.equals(e)) { + return true; + } + } + return false; + } + + /** + * @param {(e: NerdamerSymbolType, inputSet: SetType, i: number) => void} f + * @returns {SetType} + */ + each(f) { + const { elements } = this; + const newSet = new NerdamerSet(); + for (let i = 0, l = elements.length; i < l; i++) { + const e = elements[i]; + f.call(this, e, newSet, i); + } + return newSet; + } + + /** @returns {SetType} */ + clone() { + const newSet = new NerdamerSet(); + this.each(e => { + newSet.add(e.clone()); + }); + return newSet; + } + + /** + * @param {SetType} inputSet + * @returns {SetType} + */ + union(inputSet) { + const _union = this.clone(); + inputSet.each(e => { + _union.add(e); + }); + + return _union; + } + + /** + * @param {SetType} inputSet + * @returns {SetType} + */ + difference(inputSet) { + const diff = this.clone(); + inputSet.each(e => { + diff.remove(e); + }); + return diff; + } + + /** + * @param {NerdamerSymbolType} element + * @returns {boolean} + */ + remove(element) { + for (let i = 0, l = this.elements.length; i < l; i++) { + const e = this.elements[i]; + if (e.equals(element)) { + SetDeps.remove(this.elements, i); + return true; + } + } + return false; + } + + /** + * @param {SetType} inputSet + * @returns {SetType} + */ + intersection(inputSet) { + const _intersection = new NerdamerSet(); + const A = this; + inputSet.each(e => { + if (A.contains(e)) { + _intersection.add(e); + } + }); + + return _intersection; + } + + /** + * @param {SetType} inputSet + * @returns {boolean} + */ + intersects(inputSet) { + return this.intersection(inputSet).elements.length > 0; + } + + /** + * @param {SetType} inputSet + * @returns {boolean} + */ + isSubset(inputSet) { + const { elements } = inputSet; + for (let i = 0, l = elements.length; i < l; i++) { + if (!this.contains(elements[i])) { + return false; + } + } + return true; + } + + /** @returns {string} */ + toString() { + return `{${this.elements.join(',')}}`; + } +} + +// Assign NerdamerSet to CoreDeps immediately +CoreDeps.classes.NerdamerSet = NerdamerSet; + +// Collection Class ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via CollectionDeps which is set by the IIFE after initialization. + +/** + * Dependency container for Collection class. Populated by the IIFE during initialization. + * + * @type {{ + * _: ParserType; + * block: (setting: string, f: Function, opt?: boolean, obj?: unknown) => unknown; + * }} + */ +const CollectionDeps = { + get _() { + return CoreDeps.parser; + }, + get block() { + return CoreDeps.utils.block; + }, +}; + +/** + * Class used to collect arguments for functions + * + * @implements {CollectionType} + */ +class Collection { + /** @type {NerdamerSymbolType[]} */ + elements = []; + + /** @param {NerdamerSymbolType} [e] */ + constructor(e) { + if (e) { + this.elements.push(e); + } + } + + /** + * @param {NerdamerSymbolType} [e] + * @returns {CollectionType} + */ + static create(e) { + return new Collection(e); + } + + /** @param {NerdamerSymbolType} e */ + append(e) { + this.elements.push(e); + } + + /** @returns {NerdamerSymbolType[]} */ + getItems() { + return this.elements; + } + + /** @returns {string} */ + toString() { + return CollectionDeps._.prettyPrint(this.elements); + } + + /** @returns {number} */ + dimensions() { + return this.elements.length; + } + + /** + * @param {string} [options] + * @returns {string} + */ + text(options) { + return `(${this.elements.map(e => e.text(options)).join(',')})`; + } + + /** @returns {CollectionType} */ + clone() { + const c = Collection.create(); + c.elements = this.elements.map(e => e.clone()); + return c; + } + + /** + * @param {ExpandOptions} [options] + * @returns {this} + */ + expand(options) { + this.elements = /** @type {NerdamerSymbolType[]} */ ( + this.elements.map(e => CollectionDeps._.expand(e, options)) + ); + return this; + } + + /** + * @param {Record} [options] + * @returns {this} + */ + evaluate(options) { + this.elements = /** @type {NerdamerSymbolType[]} */ ( + this.elements.map(e => CollectionDeps._.evaluate(e, options)) + ); + return this; + } + + /** + * @param {Function} lambda + * @returns {CollectionType} + */ + map(lambda) { + const c2 = this.clone(); + c2.elements = c2.elements.map((x, i) => lambda(x, i + 1)); + return c2; + } + + /** + * Returns the result of adding the argument to the vector + * + * @param {CollectionType} c2 + * @returns {CollectionType | null} + */ + add(c2) { + return /** @type {CollectionType | null} */ ( + CollectionDeps.block( + 'SAFE', + () => { + const V = c2.elements; + if (this.elements.length !== V.length) { + return null; + } + return this.map((x, i) => CollectionDeps._.add(x, V[i - 1])); + }, + undefined, + this + ) + ); + } + + /** + * Returns the result of subtracting the argument from the vector + * + * @param {CollectionType} vector + * @returns {CollectionType | null} + */ + subtract(vector) { + return /** @type {CollectionType | null} */ ( + CollectionDeps.block( + 'SAFE', + () => { + const V = vector.elements; + if (this.elements.length !== V.length) { + return null; + } + return this.map((x, i) => CollectionDeps._.subtract(x, V[i - 1])); + }, + undefined, + this + ) + ); + } +} + +// Assign Collection to CoreDeps immediately +CoreDeps.classes.Collection = Collection; + +// Scientific Class ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via ScientificDeps which is set by the IIFE after initialization. + +/** + * Dependency container for Scientific class. Populated by the IIFE during initialization. + * + * @type {{ + * Settings: SettingsType; + * nround: Function; + * }} + */ +const ScientificDeps = { + get Settings() { + return CoreDeps.settings; + }, + get nround() { + return CoreDeps.utils.nround; + }, +}; + +/* + * Javascript has the toExponential method but this allows you to work with string and therefore any number of digits of your choosing + * For example Scientific('464589498449496467924197545625247695464569568959124568489548454'); + */ +class Scientific { + /** @type {number} */ + sign; + + /** @type {string} */ + coeff; + + /** @type {number} */ + exponent; + + /** @type {string} */ + wholes; + + /** @type {string} */ + dec; + + /** @type {number} */ + decp; + + /** @param {string | number} [num] */ + constructor(num) { + num = String(typeof num === 'undefined' ? 0 : num); // Convert to a string + + // remove the sign + if (num.startsWith('-')) { + this.sign = -1; + // Remove the sign + num = num.substr(1, num.length); + } else { + this.sign = 1; + } + + if (Scientific.isScientific(num)) { + this.fromScientific(num); + } else { + this.convert(num); + } + } + + /** @param {string} num */ + static isScientific(num) { + return isScientificNotation(num); + } + + /** @param {string} num */ + static leadingZeroes(num) { + const match = num.match(/^(?0*).*$/u); + return match ? match[1] : ''; + } + + /** @param {string} num */ + static removeLeadingZeroes(num) { + const match = num.match(/^0*(?.*)$/u); + return match ? match[1] : ''; + } + + /** @param {string} num */ + static removeTrailingZeroes(num) { + const match = num.match(/0*$/u); + return match ? num.substring(0, num.length - match[0].length) : ''; + } + + /** + * @param {string} c + * @param {number} n + */ + static round(c, n) { + let coeff = String(ScientificDeps.nround(c, n)); + const m = coeff.includes('.') ? coeff.split('.').pop() : ''; + const d = n - m.length; + // If we're asking for more significant figures + if (d > 0) { + if (!coeff.includes('.')) { + coeff += '.'; + } + coeff += new Array(d + 1).join('0'); + } + return coeff; + } + + /** + * @param {string} num + * @returns {this} + */ + fromScientific(num) { + const parts = String(num).toLowerCase().split('e'); + this.coeff = parts[0]; + this.exponent = Number(parts[1]); // Convert to number for consistent === 0 checks in toString() + + const coeffParts = this.coeff.split('.'); + this.wholes = coeffParts[0] || ''; + this.dec = coeffParts[1] || ''; + const { dec } = this; // If it's undefined or zero it's going to blank + this.decp = dec === '0' ? 0 : dec.length; + + return this; + } + + /** + * @param {string} num + * @returns {this} + */ + convert(num) { + // Get wholes and decimals + const parts = num.split('.'); + // Make zero go away + let w = parts[0] || ''; + let d = parts[1] || ''; + // Convert zero to blank strings + w = Scientific.removeLeadingZeroes(w); + d = Scientific.removeTrailingZeroes(d); + // Find the location of the decimal place which is right after the wholes + const dotLocation = w.length; + // Add them together so we can move the dot + const n = w + d; + // Find the next number + const zeroes = Scientific.leadingZeroes(n).length; + // NerdamerSet the exponent + this.exponent = dotLocation - (zeroes + 1); + // NerdamerSet the coeff but first remove leading zeroes + const coeff = Scientific.removeLeadingZeroes(n); + this.coeff = `${coeff.charAt(0)}.${Scientific.removeTrailingZeroes(coeff.substr(1, coeff.length)) || '0'}`; + + // The coeff decimal places + const dec = this.coeff.split('.')[1] || ''; // If it's undefined or zero it's going to blank + + this.decp = dec === '0' ? 0 : dec.length; + // Decimals + this.dec = d; + // Wholes + this.wholes = w; + + return this; + } + + /** + * @param {number} num + * @returns {Scientific} + */ + round(num) { + const n = this.copy(); + + num = Number(num); // Cast to number for safety + // since we know it guaranteed to be in the format {digit}{optional dot}{optional digits} + // we can round based on this + if (num === 0) { + n.coeff = n.coeff.charAt(0); + } else { + // Get up to n-1 digits + const rounded = this.coeff.substring(0, num + 1); + // Get the next two + const nextTwo = this.coeff.substring(num + 1, num + 3); + // The extra digit + let ed = Number(nextTwo.charAt(0)); + + if (Number(nextTwo.charAt(1)) > 4) { + ed++; + } + + n.coeff = rounded + ed; + } + + return n; + } + + /** @returns {Scientific} */ + copy() { + const n = new Scientific(0); + n.coeff = this.coeff; + n.exponent = this.exponent; + n.sign = this.sign; + return n; + } + + /** + * @param {number} [n] + * @returns {string} + */ + toString(n) { + let retval; + + if (ScientificDeps.Settings.SCIENTIFIC_IGNORE_ZERO_EXPONENTS && this.exponent === 0 && this.decp < n) { + if (this.decp === 0 && this.wholes !== undefined) { + retval = this.wholes; + } else { + retval = this.coeff; + } + } else { + let coeff = + typeof n === 'undefined' ? this.coeff : Scientific.round(this.coeff, Math.min(n, this.decp || 1)); + let exp = this.exponent; + if (coeff.startsWith('10.')) { + // Edge case when coefficient is 9.999999 rounds to 10 + coeff = + typeof n === 'undefined' + ? coeff.replace(/^10\./u, '1.0') + : Scientific.round(coeff.replace(/^10\./u, '1.0'), Math.min(n, this.decp || 1)); + exp = Number(exp) + 1; + } + retval = this.exponent === 0 ? coeff : `${coeff}e${exp}`; + } + + return (this.sign === -1 ? '-' : '') + retval; + } +} + +// Assign Scientific to CoreDeps immediately +CoreDeps.classes.Scientific = Scientific; + +// IsArray Function ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if an object is an array + * + * @param {unknown} arr + * @returns {arr is unknown[]} + */ +function isArray(arr) { + return Array.isArray(arr); +} + +// InBrackets Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * @param {unknown} str + * @returns {string} - Returns a formatted string surrounded by brackets + */ +function inBrackets(str) { + return `(${str})`; +} + +// SameSign Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if numbers are both negative or are both positive + * + * @param {number} a + * @param {number} b + * @returns {boolean} + */ +function sameSign(a, b) { + return a < 0 === b < 0; +} + +// Format Function ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * A helper function to replace multiple occurences in a string. Takes multiple arguments + * + * @example + * format('{0} nice, {0} sweet', 'something'); + * //returns 'something nice, something sweet' + * + * @param {...unknown} args + * @returns {string} + */ +function format(...args) { + const str = /** @type {string} */ (args.shift()); + const newStr = str.replace(/\{(?\d+)\}/gu, (match, index) => { + const arg = args[index]; + return typeof arg === 'function' ? arg() : arg; + }); + + return newStr; +} + +// Range Function =================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Generates an array with values within a range. Multiplies by a step if provided + * + * @param {number} start + * @param {number} end + * @param {number} [step] + * @returns {number[]} + */ +function range(start, end, step) { + const arr = []; + step ||= 1; + for (let i = start; i <= end; i++) { + arr.push(i * step); + } + return arr; +} + +// Stringify Function =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Safely stringify object + * + * @param {unknown} o + * @returns {string} + */ +function stringify(o) { + if (!o) { + return ''; + } + return String(o); +} + +// StringReplace Function =========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * A helper function to replace parts of string + * + * @param {string} str - The original string + * @param {number} from - The starting index + * @param {number} to - The ending index + * @param {string} withStr - The replacement string + * @returns {string} - A formatted string + */ +function stringReplace(str, from, to, withStr) { + return str.substr(0, from) + withStr + str.substr(to, str.length); +} + +// CustomType Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * The Parser uses this to check if it's allowed to convert the obj to type NerdamerSymbol + * + * @param {object} obj + * @returns {boolean} + */ +function customType(obj) { + return obj !== undefined && obj.custom; +} + +// ArrayMax Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Returns the maximum number in an array + * + * @param {number[]} arr + * @returns {number} + */ +function arrayMax(arr) { + return Math.max.apply(undefined, arr); +} + +// ArrayMin Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Returns the minimum number in an array + * + * @param {number[]} arr + * @returns {number} + */ +function arrayMin(arr) { + return Math.min.apply(undefined, arr); +} + +// Even Function ==================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if a number is an even number + * + * @param {number | string | FracType | { valueOf(): number | string | DecimalType }} num + * @returns {boolean} + */ +function even(num) { + return Number(num) % 2 === 0; +} + +// EvenFraction Function ============================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if a fraction is divisible by 2 + * + * @param {number} num + * @returns {boolean} + */ +function evenFraction(num) { + return (1 / (num % 1)) % 2 === 0; +} + +// ArrayUnique Function ============================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Strips duplicates out of an array + * + * @template T + * @param {T[]} arr + * @returns {T[]} + */ +function arrayUnique(arr) { + const l = arr.length; + const a = []; + for (let i = 0; i < l; i++) { + const item = arr[i]; + if (a.indexOf(item) === -1) { + a.push(item); + } + } + return a; +} + +// Arguments2Array Function ========================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Converts function arguments to an array. Now used by gcd and lcm in Algebra.js :) + * + * @param {Parameters['0']} obj + * @returns {unknown[]} + */ +function arguments2Array(obj) { + return [].slice.call(obj); +} + +// IsInt Function =================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if a number is an integer + * + * @param {number | string | { toString(): string }} num + * @returns {boolean} + */ +function isInt(num) { + if (typeof num === 'number') { + return Number.isInteger(num); + } + return typeof num !== 'undefined' && /^[-+]?\d+e?\+?\d*$/gimu.test(num.toString()); +} + +// ArrayEqual Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if two arrays are equal + * + * @template T + * @param {T[]} arr1 + * @param {T[]} arr2 + * @returns {boolean} + */ +function arrayEqual(arr1, arr2) { + arr1.sort(); + arr2.sort(); + + // The must be of the same length + if (arr1.length === arr2.length) { + for (let i = 0; i < arr1.length; i++) { + // If any two items don't match we're done + if (arr1[i] !== arr2[i]) { + return false; + } + } + // Otherwise they're equal + return true; + } + + return false; +} + +// ArrayClone Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Clones array with clonable items + * + * @template T + * @param {T[]} arr + * @returns {T[]} + */ +function arrayClone(arr) { + const newArray = []; + const l = arr.length; + for (let i = 0; i < l; i++) { + newArray[i] = /** @type {{ clone: () => unknown }} */ (arr[i]).clone(); + } + return /** @type {T[]} */ (newArray); +} + +// IsNumber Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks if n is a number + * + * @param {string | number} n + * @returns {boolean} + */ +function isNumber(n) { + return /^\d+\.?\d*$/u.test(String(n)); +} + +// Nround Function ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Rounds a number up to x decimal places + * + * @param {number | string} x + * @param {number} [s] + * @returns {number | string} + */ +function nround(x, s = 14) { + if (isInt(x)) { + if (Number(x) >= Number.MAX_VALUE) { + return x.toString(); + } + return Number(x); + } + return Math.round(/** @type {number} */ (x) * 10 ** s) / 10 ** s; +} + +// ArrayAddSlices Function ========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Fills numbers between array values + * + * @param {number[]} arr + * @param {number} [slices] + * @returns {number[]} + */ +function arrayAddSlices(arr, slices) { + slices ||= 20; + const retval = []; + let c; + let delta; + let e; + retval.push(arr[0]); // Push the beginning + for (let i = 0; i < arr.length - 1; i++) { + c = arr[i]; + delta = arr[i + 1] - c; // Get the difference + e = delta / slices; // Chop it up in the desired number of slices + for (let j = 0; j < slices; j++) { + c += e; // Add the mesh to the last slice + retval.push(c); + } + } + + return retval; +} + +// Each Function ==================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Loops through each item in object and calls function with item as param + * + * @param {object | unknown[]} obj + * @param {Function} fn + */ +function each(obj, fn) { + if (isArray(obj)) { + const l = obj.length; + for (let i = 0; i < l; i++) { + fn.call(obj, i); + } + } else { + for (const x in obj) { + if (Object.hasOwn(obj, x)) { + fn.call(obj, x); + } + } + } +} + +// Remove Function ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Removes an item from either an array or an object. If the object is an array, the index must be specified after the + * array. If it's an object then the key must be specified + * + * @template T + * @param {Record | T[]} obj + * @param {number | string} indexOrKey + * @returns {T | undefined} + */ +function remove(obj, indexOrKey) { + let result; + if (isArray(obj)) { + result = obj.splice(/** @type {number} */ (indexOrKey), 1)[0]; + } else { + result = obj[indexOrKey]; + delete obj[indexOrKey]; + } + return result; +} + +// KnownVariable Function =========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Generates an object with known variable value for evaluation + * + * @param {string} variable + * @param {string | number | NerdamerSymbolType} value Any stringifyable object + * @returns {Record} + */ +function knownVariable(variable, value) { + /** @type {Record} */ + const o = {}; + o[variable] = value; + return o; +} + +// AllNumeric Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if an array contains only numeric values + * + * @param {(string | number)[]} arr + * @returns {boolean} + */ +function allNumeric(arr) { + for (let i = 0; i < arr.length; i++) { + if (!isNumber(arr[i])) { + return false; + } + } + return true; +} + +// ScientificToDecimal Function ===================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Convert number from scientific format to decimal format + * + * @param {number} num + * @returns {string} + */ +function scientificToDecimal(num) { + const nsign = Math.sign(num); + // Remove the sign + /** @type {number | string} */ + let n = Math.abs(num); + // If the number is in scientific notation remove it + if (/\d+\.?\d*e[+-]*\d+/iu.test(String(n))) { + const zero = '0'; + const parts = String(n).toLowerCase().split('e'); // Split into coeff and exponent + const e = parts.pop(); // Store the exponential part + let l = Math.abs(Number(e)); // Get the number of zeros + const sign = Math.sign(Number(e)); + const coeffArray = parts[0].split('.'); + if (sign === -1) { + // Return "("+parts[0]+"/1"+"0".repeat(l)+")"; + l -= coeffArray[0].length; + if (l < 0) { + n = `${coeffArray[0].slice(0, l)}.${coeffArray[0].slice( + l + )}${coeffArray.length === 2 ? coeffArray[1] : ''}`; + } else { + n = `${zero}.${new Array(l + 1).join(zero)}${coeffArray.join('')}`; + } + } else { + const dec = coeffArray[1]; + if (dec) { + l -= dec.length; + } + if (l < 0) { + n = `${coeffArray[0] + dec.slice(0, l)}.${dec.slice(l)}`; + } else { + n = coeffArray.join('') + new Array(l + 1).join(zero); + } + } + } + + return nsign < 0 ? `-${n}` : String(n); +} + +// AllSame Function ============================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see that all items in array are equal using the equals method + * + * @param {{ equals(other: unknown): boolean }[]} arr + * @returns {boolean} + */ +function allSame(arr) { + const last = arr[0]; + for (let i = 1, l = arr.length; i < l; i++) { + if (!arr[i].equals(last)) { + return false; + } + } + return true; +} + +// RemoveDuplicates Function ======================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Removes duplicates from an array + * + * @param {Array} arr + * @param {Function} [condition] + * @returns {Array} + */ +function removeDuplicates(arr, condition) { + const conditionType = typeof condition; + + if (conditionType !== 'function') { + condition = function (a, b) { + return a === b; + }; + } + + const seen = []; + + while (arr.length) { + const a = arr[0]; + // Only one element left so we're done + if (arr.length === 1) { + seen.push(a); + break; + } + const temp = []; + seen.push(a); // We already scanned these + for (let i = 1; i < arr.length; i++) { + const b = arr[i]; + // If the number is outside the specified tolerance + if (!condition(a, b)) { + temp.push(b); + } + } + // Start over with the remainder + arr = temp; + } + + return seen; +} + +// ComboSort Function =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Sorts two arrays together, keeping elements at matching indices paired. Sorts by the first array's values + * numerically. + * + * @template T, U + * @param {T[]} a + * @param {U[]} b + * @returns {[T[], U[]]} + */ +function comboSort(a, b) { + const l = a.length; + /** @type {[T, U][]} */ + const combined = []; // The linker + for (let i = 0; i < a.length; i++) { + combined.push([a[i], b[i]]); // Create the map + } + + combined.sort((x, y) => Number(x[0]) - Number(y[0])); + + /** @type {T[]} */ + const na = []; + /** @type {U[]} */ + const nb = []; + + for (let i = 0; i < l; i++) { + na.push(combined[i][0]); + nb.push(combined[i][1]); + } + + return [na, nb]; +} + +// IsCollection Function =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if the object provided is a Collection + * + * @param {object} obj + * @returns {obj is CollectionType} + */ +function isCollection(obj) { + return obj instanceof Collection; +} + +// IsSet Function ====================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Checks to see if the object provided is a NerdamerSet + * + * @param {object} obj + * @returns {obj is SetType} + */ +function isSet(obj) { + return obj instanceof NerdamerSet; +} + +// FirstObject Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Returns the first encountered item in an object. Items do not have a fixed order in objects so only use if you need + * any first random or if there's only one item in the object + * + * @template T + * @param {Record} obj + * @param {string} [key] - Return this key as first object + * @param {boolean} [both] - Return both key and object + * @returns {string | T | { key: string; obj: T }} + */ +function firstObject(obj, key, both) { + const objKeys = Object.keys(obj); + const x = objKeys[0]; + if (key) { + return x; + } + if (both) { + return { + key: x, + obj: obj[x], + }; + } + return obj[x]; +} + +// Keys Alias ====================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** Alias for Object.keys - returns an array of all the keys in an object */ +const { keys } = Object; + +// GroupConstantsDeps ============================================================== +// Accessor for group constants. These constants define the type groups for nerdamer symbols. +// Uses CoreDeps as the single source of truth. + +/** + * @type {{ + * N: number; + * P: number; + * S: number; + * EX: number; + * FN: number; + * PL: number; + * CB: number; + * CP: number; + * }} + */ +const GroupConstantsDeps = { + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get PL() { + return CoreDeps.groups.PL; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get CP() { + return CoreDeps.groups.CP; + }, +}; + +// ParserDeps ====================================================================== +// Accessor for Parser dependencies. Uses CoreDeps as the single source of truth. + +/** + * @type {{ + * _: ParserType; + * N: number; + * P: number; + * S: number; + * EX: number; + * FN: number; + * PL: number; + * CB: number; + * CP: number; + * SQRT: string; + * ABS: string; + * FACTORIAL: string; + * DOUBLEFACTORIAL: string; + * PARENTHESIS: string; + * bigDec: DecimalStaticType; + * PRIMES: number[]; + * VARS: Record; + * }} + */ +const ParserDeps = { + get _() { + return CoreDeps.parser; + }, + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get PL() { + return CoreDeps.groups.PL; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get CP() { + return CoreDeps.groups.CP; + }, + get SQRT() { + return CoreDeps.fnNames.SQRT; + }, + get ABS() { + return CoreDeps.fnNames.ABS; + }, + get FACTORIAL() { + return CoreDeps.fnNames.FACTORIAL; + }, + get DOUBLEFACTORIAL() { + return CoreDeps.fnNames.DOUBLEFACTORIAL; + }, + get PARENTHESIS() { + return CoreDeps.fnNames.PARENTHESIS; + }, + get bigDec() { + return CoreDeps.ext.bigDec; + }, + get PRIMES() { + return CoreDeps.ext.PRIMES; + }, + get VARS() { + return CoreDeps.state.VARS; + }, +}; + +/** + * Checks to see if a symbol is in group N (number) or P (power) + * + * @param {NerdamerSymbolType} symbol + * @returns {boolean} + */ +function isNumericSymbol(symbol) { + return symbol.group === GroupConstantsDeps.N || symbol.group === GroupConstantsDeps.P; +} + +/** + * Checks to see if a symbol is a variable with no multiplier nor power + * + * @param {NerdamerSymbolType} symbol + * @returns {boolean} + */ +function isVariableSymbol(symbol) { + return symbol.group === GroupConstantsDeps.S && symbol.multiplier.equals(1) && symbol.power.equals(1); +} + +/** + * Checks to see if all arguments are numbers + * + * @param {object} args + * @returns {boolean} + */ +function allNumbers(args) { + for (let i = 0; i < args.length; i++) { + if (args[i].group !== GroupConstantsDeps.N) { + return false; + } + } + return true; +} + +// ReservedDeps ==================================================================== +// Shared dependency container for RESERVED array access. Populated by the IIFE during initialization. + +/** + * @type {{ + * RESERVED: (string | undefined)[]; + * }} + */ +const ReservedDeps = { + get RESERVED() { + return CoreDeps.state.RESERVED; + }, +}; + +/** + * Reserves the names in an object so they cannot be used as function names + * + * @param {object} obj + */ +function reserveNames(obj) { + const add = function (item) { + if (ReservedDeps.RESERVED.indexOf(item) === -1) { + ReservedDeps.RESERVED.push(item); + } + }; + + if (typeof obj === 'string') { + add(obj); + } else { + each(obj, x => { + add(x); + }); + } +} + +/** + * Clears the u variable so it's no longer reserved + * + * @param {string} u + */ +function clearU(u) { + const indx = ReservedDeps.RESERVED.indexOf(u); + if (indx !== -1) { + ReservedDeps.RESERVED[indx] = undefined; + } +} + +// ValidateName Function =========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via ValidateNameDeps which is set by the IIFE after initialization. + +/** + * Dependency container for validateName function. Populated by the IIFE during initialization. + * + * @type {{ + * ALLOW_CHARS: string[]; + * VALIDATION_REGEX: RegExp; + * }} + */ +const ValidateNameDeps = { + get ALLOW_CHARS() { + return CoreDeps.settings?.ALLOW_CHARS ?? []; + }, + VALIDATION_REGEX: /^[a-z_][a-z\d_]*$/iu, +}; + +/** + * Enforces rule: "must start with a letter or underscore and can have any number of underscores, letters, and numbers + * thereafter." + * + * @param {string} name The name of the symbol being checked + * @param {string} [typ] - The type of symbols that's being validated + * @throws {Error} - Throws an exception on fail + */ +function validateName(name, typ = 'variable') { + if (ValidateNameDeps.ALLOW_CHARS.indexOf(name) !== -1) { + return; + } + const regex = ValidateNameDeps.VALIDATION_REGEX; + if (!regex.test(name)) { + throw new InvalidVariableNameError(`${name} is not a valid ${typ} name`); + } +} + +/** + * Checks to see if value is one of nerdamer's reserved names + * + * @param {string} value + * @returns {boolean} + */ +function isReserved(value) { + return ReservedDeps.RESERVED.indexOf(value) !== -1; +} + +// Warn Function =================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via WarnDeps which is set by the IIFE after initialization. + +/** + * Dependency container for warn function. Populated by the IIFE during initialization. + * + * @type {{ + * WARNINGS: string[]; + * SHOW_WARNINGS: boolean; + * }} + */ +const WarnDeps = { + get WARNINGS() { + return CoreDeps.state.WARNINGS; + }, + get SHOW_WARNINGS() { + return !(CoreDeps.settings?.SILENCE_WARNINGS ?? true); + }, +}; + +/** + * Used to pass warnings or low severity errors about the library + * + * @param {string} msg + */ +function warn(msg) { + WarnDeps.WARNINGS.push(msg); + if (WarnDeps.SHOW_WARNINGS && console && console.warn) { + console.warn(msg); + } +} + +/** + * Get nerdamer generated warnings + * + * @returns {string[]} + */ +function getWarnings() { + return WarnDeps.WARNINGS; +} + +// NumExpressions Function ======================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses NumExpressionsDeps for dependency injection of EXPRESSIONS array. + +/** + * Dependency container for numExpressions function. Populated by the IIFE during initialization. + * + * @type {{ + * EXPRESSIONS: ExpressionType[]; + * }} + */ +const NumExpressionsDeps = { + get EXPRESSIONS() { + return CoreDeps.state.EXPRESSIONS; + }, +}; + +/** + * Returns the number of equations/expressions currently loaded + * + * @returns {number} + */ +function numExpressions() { + return NumExpressionsDeps.EXPRESSIONS.length; +} + +// GetSetting Function =========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses GetSettingDeps for dependency injection of Settings object. + +/** + * Dependency container for getSetting function. Populated by the IIFE during initialization. + * + * @type {{ + * Settings: SettingsType; + * }} + */ +const GetSettingDeps = { + get Settings() { + return CoreDeps.settings; + }, +}; + +/** + * Get the value of a nerdamer setting + * + * @param {string} setting + * @returns {boolean | number | string | object | undefined} + */ +function getSetting(setting) { + return GetSettingDeps.Settings[setting]; +} + +// ValidVarName Function ========================================================= +// Uses ReservedDeps.RESERVED and validateName. + +/** + * Validates if the provided string is a valid variable name + * + * @param {string} varname Variable name + * @returns {boolean} + */ +function validVarName(varname) { + try { + validateName(varname); + return ReservedDeps.RESERVED.indexOf(varname) === -1; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return false; + } +} + +// Reserved Function ============================================================= +// Uses ReservedDeps.RESERVED. + +/** + * Returns reserved variable names + * + * @param {boolean} [asArray] If true, returns as array; otherwise returns comma-separated string + * @returns {string | string[]} + */ +function reserved(asArray) { + if (asArray) { + return ReservedDeps.RESERVED; + } + return ReservedDeps.RESERVED.join(', '); +} + +// Version Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses VersionDeps for dependency injection of _version and C (Core object). + +/** + * Dependency container for version function. Populated by the IIFE during initialization. + * + * @type {{ + * _version: string; + * C: CoreType | null; + * }} + */ +const VersionDeps = { + get _version() { + return CoreDeps.version; + }, + get C() { + return CoreDeps.core; + }, +}; + +/** + * Get the version of nerdamer or a loaded add-on + * + * @param {string} [addOn] - The add-on being checked + * @returns {string} Returns the version of nerdamer + */ +function version(addOn) { + if (addOn) { + try { + return VersionDeps.C[addOn].version; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return `No module named ${addOn} found!`; + } + } + return CoreDeps.version; +} + +// GetCore Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses VersionDeps.C which is already initialized with the Core object. + +/** + * Exports the nerdamer core functions and objects + * + * @returns {object} The Core object + */ +function getCore() { + return VersionDeps.C; +} + +// Supported Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses SupportedDeps.functions which provides access to _.functions. + +/** + * Dependencies for the supported function. Initialized inside the IIFE. + * + * @type {{ + * functions: object | null; + * }} + */ +const SupportedDeps = { + get functions() { + return CoreDeps.parser?.functions; + }, +}; + +/** + * Returns an array of all supported function names + * + * @returns {string[]} Array of function names + */ +function supported() { + return keys(SupportedDeps.functions); +} + +// GetConstant Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses GetConstantDeps.CONSTANTS which provides access to _.CONSTANTS. + +/** + * Dependencies for the getConstant function. Initialized inside the IIFE. + * + * @type {{ + * CONSTANTS: object | null; + * }} + */ +const GetConstantDeps = { + get CONSTANTS() { + return CoreDeps.parser?.CONSTANTS; + }, +}; + +/** + * Returns the value of a previously set constant + * + * @param {string} constant The name of the constant + * @returns {string} The string value of the constant + */ +function getConstant(constant) { + return String(GetConstantDeps.CONSTANTS[constant]); +} + +// GetVar Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses GetVarDeps.VARS which provides access to VARS. + +/** + * Dependencies for the getVar function. Initialized inside the IIFE. + * + * @type {{ + * VARS: Record; + * }} + */ +const GetVarDeps = { + get VARS() { + return CoreDeps.state.VARS; + }, +}; + +/** + * Returns the value of a previously set variable + * + * @param {string} v The name of the variable + * @returns {NerdamerSymbolType | undefined} The value of the variable + */ +function getVar(v) { + return GetVarDeps.VARS[v]; +} + +/** + * Returns an object containing all stored variables, optionally formatted. + * + * @param {string} [output] Output format: 'object' (raw VARS), 'text' (default), or 'latex' + * @param {string | string[]} [option] Formatting option passed to text/latex methods + * @returns {object} Object with variable names as keys + */ +function getVars(output, option) { + output ||= 'text'; + let result = {}; + if (output === 'object') { + result = GetVarDeps.VARS; + } else { + for (const v in GetVarDeps.VARS) { + if (!Object.hasOwn(GetVarDeps.VARS, v)) { + continue; + } + if (output === 'latex') { + result[v] = GetVarDeps.VARS[v].latex(option); + } else if (output === 'text') { + result[v] = GetVarDeps.VARS[v].text(option); + } + } + } + return result; +} + +// ConvertToLaTeX Function ======================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses ConvertToLaTeXDeps._ which provides access to the core object. + +/** + * Dependencies for core wrapper functions. Initialized inside the IIFE. + * + * @type {{ + * _: ParserType | null; + * C: CoreType | null; + * }} + */ +const ConvertToLaTeXDeps = { + get _() { + return CoreDeps.parser; + }, + get C() { + return CoreDeps.core; + }, +}; + +/** + * Generates LaTeX from expression string + * + * @param {string} e + * @param {object} opt + * @returns {string} + */ +function convertToLaTeX(e, opt) { + return ConvertToLaTeXDeps._.toTeX(e, opt); +} + +/** + * Returns the operator object for a given operator string + * + * @param {string} operator + * @returns {{ symbol: string; precedence: number; leftAssoc: boolean; operation?: Function } | undefined} + */ +function getOperator(operator) { + return ConvertToLaTeXDeps._.getOperator(operator); +} + +/** + * Creates an alias for an operator + * + * @param {string} operator + * @param {string} withOperator + * @returns {void} + */ +function aliasOperator(operator, withOperator) { + ConvertToLaTeXDeps._.aliasOperator(operator, withOperator); +} + +/** + * Sets an operator + * + * @param {string | { symbol: string; precedence?: number; leftAssoc?: boolean }} operator + * @param {Function} [action] + * @param {'over' | 'under'} [shift] + * @returns {void} + */ +function setOperator(operator, action, shift) { + ConvertToLaTeXDeps._.setOperator(operator, action, shift); +} + +/** + * Adds a peeker function + * + * @param {string} name + * @param {Function} f + * @returns {void} + */ +function addPeeker(name, f) { + if (ConvertToLaTeXDeps._.peekers[name]) { + ConvertToLaTeXDeps._.peekers[name].push(f); + } +} + +/** + * Removes a peeker function + * + * @param {string} name + * @param {Function} f + * @returns {void} + */ +function removePeeker(name, f) { + const peekers = ConvertToLaTeXDeps._.peekers[name]; + if (peekers) { + const index = peekers.indexOf(f); + if (index !== -1) { + remove(peekers, index); + } + } +} + +/** + * Returns the tree representation of an expression + * + * @param {string} expression + * @returns {object} Tree node representation + */ +function tree(expression) { + // The Parser's tree method is overloaded to accept both string and Token[] + // TypeScript definition only shows the string version, so we use type assertion + const tokens = ConvertToLaTeXDeps._.toRPN(ConvertToLaTeXDeps._.tokenize(expression)); + // @ts-expect-error - tree method accepts Token[] at runtime but TypeScript types only show string overload + return ConvertToLaTeXDeps._.tree(tokens); +} + +/** + * Parses an expression string into an array of symbols + * + * @param {string} e + * @returns {NerdamerSymbolType[]} + */ +function parse(e) { + return String(e) + .split(';') + .map(x => ConvertToLaTeXDeps._.parse(x)); +} + +/** + * Converts expression into rpn form + * + * @param {string} expression + * @returns {object[]} + */ +function rpn(expression) { + return ConvertToLaTeXDeps._.toRPN(ConvertToLaTeXDeps._.tokenize(expression)); +} + +/** + * Generates an HTML tree representation of an expression + * + * @param {string} expression + * @param {number} [indent] + * @returns {string} + */ +function htmlTree(expression, indent) { + const treeResult = tree(expression); + + return ( + `
\n` + + `
    \n` + + `
  • \n${treeResult.toHTML(3, indent)}\n` + + `
  • \n` + + `
\n` + + `
` + ); +} + +/** + * Replaces an internal function with a new implementation + * + * @param {string} name The name of the function to replace + * @param {Function} fn A factory function that receives (existingFn, C) and returns the new function + * @param {number} [numArgs] Optional number of arguments (defaults to existing function's numArgs) + * @returns {void} + */ +function replaceFunction(name, fn, numArgs) { + const existing = ConvertToLaTeXDeps._.functions[name]; + const newNumArgs = typeof numArgs === 'undefined' ? existing[1] : numArgs; + ConvertToLaTeXDeps._.functions[name] = /** @type {[Function, number] | [Function, number[]]} */ ([ + fn(existing[0], ConvertToLaTeXDeps.C), + newNumArgs, + ]); +} + +// Expressions Function =========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. + +/** + * Dependencies for expressions and related functions. Initialized inside the IIFE. + * + * @type {{ + * EXPRESSIONS: ExpressionType[]; + * USER_FUNCTIONS: string[]; + * LaTeX: LaTeXInterface; + * text: (obj: unknown, option?: string | string[]) => string; + * functions: Record; + * Math2: Math2Interface; + * _: ParserType | null; + * Expression: ExpressionConstructor | null; + * }} + */ +const ExpressionsDeps = { + get EXPRESSIONS() { + return CoreDeps.state.EXPRESSIONS; + }, + get USER_FUNCTIONS() { + return CoreDeps.state.USER_FUNCTIONS; + }, + get LaTeX() { + return CoreDeps.classes.LaTeX; + }, + get text() { + return CoreDeps.utils.text; + }, + get functions() { + return CoreDeps.parser?.functions ?? {}; + }, + get Math2() { + return CoreDeps.classes.Math2; + }, + get _() { + return CoreDeps.parser; + }, + get Expression() { + return CoreDeps.classes.Expression; + }, +}; + +/** + * Returns stored expressions as an array or object, optionally in LaTeX format. + * + * @param {boolean} [asObject] Return as object with 1-based indices as keys + * @param {boolean} [asLaTeX] Convert expressions to LaTeX + * @param {string | string[]} [option] Formatting option + * @returns {Record | string[]} + */ +function expressions(asObject, asLaTeX, option) { + /** @type {Record | string[]} */ + const result = asObject ? {} : []; + for (let i = 0; i < ExpressionsDeps.EXPRESSIONS.length; i++) { + const eq = asLaTeX + ? ExpressionsDeps.LaTeX.latex(ExpressionsDeps.EXPRESSIONS[i], option) + : ExpressionsDeps.text(ExpressionsDeps.EXPRESSIONS[i], option); + asObject ? (result[i + 1] = eq) : /** @type {string[]} */ (result).push(eq); + } + return result; +} + +/** + * Returns user-defined functions as an array or object. + * + * @param {boolean} [asObject] Return as object with 1-based indices as keys + * @param {string | string[]} [option] Formatting option + * @returns {Record | string[]} + */ +function getFunctions(asObject, option) { + const result = asObject ? {} : []; + for (let i = 0; i < ExpressionsDeps.USER_FUNCTIONS.length; i++) { + let params; + let body; + const fnName = ExpressionsDeps.USER_FUNCTIONS[i]; + const fnDef = ExpressionsDeps.functions[fnName][2]; + if (fnDef) { + ({ params, body } = fnDef); + } else { + const fnString = ExpressionsDeps.Math2[fnName].toString(); + [, params] = /\((?.*?)\)/u.exec(fnString); + params = params.split(',').map(x => x.trim()); + body = '{JavaScript}'; + } + const fn = `${fnName}(${params.join(', ')})=${body}`; + const eq = ExpressionsDeps.text(fn, option); + asObject ? (result[i + 1] = eq) : result.push(eq); + } + return result; +} + +/** + * Converts LaTeX to a nerdamer expression. Very basic at the moment - handles subscripts, superscripts, and fractions. + * + * @param {string} e LaTeX string to convert + * @returns {ExpressionType} Expression object + */ +function convertFromLaTeX(e) { + // Convert x_2a => x_2 a + e = e.replace(/_(?[A-Za-z0-9])/gu, (...g) => `${g[0]} `); + // Convert x^2 => x^{2} + e = e.replace(/\^(?[A-Za-z0-9])/gu, (...g) => `^{${g[1]}}`); + // Convert \frac12 => \frac{1}2 + e = e.replace(/(?\\[A-Za-z]+)(?\d)/gu, (...g) => `${g[1]}{${g[2]}}`); + // Convert \frac{1}2 => \frac{1}{2} + e = e.replace(/(?\\[A-Za-z]+\{.*?\})(?\d)/gu, (...g) => `${g[1]}{${g[2]}}`); + const txt = ExpressionsDeps.LaTeX.parse(ExpressionsDeps._.tokenize(e)); + return new ExpressionsDeps.Expression(ExpressionsDeps._.parse(txt)); +} + +// Chain Functions =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// These functions return libExports for method chaining. +// Uses ChainDeps for dependency injection of libExports, VARS, and helper functions. + +/** + * Dependencies for chain functions. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: typeof nerdamer; + * VARS: Record; + * _clearFunctions: () => void; + * _initConstants: () => void; + * clear: (equationNumber: number | 'all' | 'last' | 'first', keepExpressionsFixed?: boolean) => typeof nerdamer; + * }} + */ +const ChainDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get VARS() { + return CoreDeps.state.VARS; + }, + get _clearFunctions() { + return CoreDeps.utils._clearFunctions; + }, + get _initConstants() { + return CoreDeps.parser?.initConstants ?? (() => {}); + }, + get clear() { + return clear; + }, +}; + +/** + * Clears all user-defined variables + * + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function clearVars() { + // Reset VARS to empty object - we need to clear the actual VARS object + for (const key in ChainDeps.VARS) { + if (Object.hasOwn(ChainDeps.VARS, key)) { + delete ChainDeps.VARS[key]; + } + } + return ChainDeps.libExports; +} + +/** + * Clears all added functions + * + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function clearFunctions() { + ChainDeps._clearFunctions(); + return ChainDeps.libExports; +} + +/** + * Clears all user-defined constants + * + * BUG: The original implementation used `_.initConstants.bind(_)` which creates a bound function but does NOT call it. + * This means clearConstants() is a no-op and doesn't actually clear any constants. The test confirms this bug by + * expecting constants to persist after calling clearConstants(). + * + * To actually clear constants, the code should be: `_.initConstants()` or `_.initConstants.call(_)` instead of + * `_.initConstants.bind(_)`. + * + * This bug is preserved for backwards compatibility - fixing it would be a breaking change. See issue #XX (TODO: file + * issue). + * + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function clearConstants() { + // Original code was: _.initConstants.bind(_); + // This just creates a bound function but doesn't call it - confirmed bug. + // Preserving original (buggy) behavior for backwards compatibility. + return ChainDeps.libExports; +} + +/** + * Alias for nerdamer.clear('all') - clears all stored expressions + * + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function flush() { + ChainDeps.clear(/** @type {'all'} */ ('all')); + return ChainDeps.libExports; +} + +/** + * Loads a custom loader function with nerdamer as `this` context + * + * @param {(this: typeof nerdamer) => void} loader - The loader function to call + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function load(loader) { + loader.call(ChainDeps.libExports); + return ChainDeps.libExports; +} + +// SetConstant Function ========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses SetConstantDeps for dependency injection. + +/** + * Dependencies for setConstant function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: typeof nerdamer | null; + * CONSTANTS: Record; + * }} + */ +const SetConstantDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get CONSTANTS() { + return CoreDeps.state.CONSTANTS; + }, +}; + +/** + * Set the value of a constant + * + * @param {string} constant - The name of the constant + * @param {number | 'delete' | ''} value - The value of the constant or 'delete' to remove + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function setConstant(constant, value) { + validateName(constant); + if (!isReserved(constant)) { + // Fix for issue #127 + if (value === 'delete' || value === '') { + delete SetConstantDeps.CONSTANTS[constant]; + } else { + if (isNaN(/** @type {number} */ (value))) { + throw new NerdamerTypeError('Constant must be a number!'); + } + SetConstantDeps.CONSTANTS[constant] = value; + } + } + return SetConstantDeps.libExports; +} + +// SetVar Function =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses SetVarDeps for dependency injection. + +/** + * Dependencies for setVar function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: typeof nerdamer | null; + * VARS: Record; + * CONSTANTS: Record; + * parse: (expression: ExpressionParam) => NerdamerSymbolType; + * isSymbol: (obj: unknown) => boolean; + * }} + */ +const SetVarDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get VARS() { + return CoreDeps.state.VARS; + }, + get CONSTANTS() { + return CoreDeps.state.CONSTANTS; + }, + get parse() { + const { parser } = CoreDeps; + return parser?.parse?.bind(parser) ?? (() => null); + }, + get isSymbol() { + return CoreDeps.utils.isSymbol; + }, +}; + +/** + * Set the value of a variable + * + * @param {string} v - Variable to be set + * @param {string | number | NerdamerSymbolType | 'delete'} val - Value of variable. This can be a variable expression + * or number + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function setVar(v, val) { + validateName(v); + // Check if it's not already a constant + if (v in SetVarDeps.CONSTANTS) { + err(`Cannot set value for constant ${v}`); + } + if (val === 'delete' || val === '') { + delete SetVarDeps.VARS[v]; + } else { + SetVarDeps.VARS[v] = /** @type {NerdamerSymbolType} */ (SetVarDeps.isSymbol(val) ? val : SetVarDeps.parse(val)); + } + return SetVarDeps.libExports; +} + +// SetFunction Function ========================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses SetFunctionDeps for dependency injection. + +/** + * Dependencies for setFunction function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: typeof nerdamer | null; + * _setFunction: (fnName: string | Function, fnParams?: string[], fnBody?: string) => boolean; + * }} + */ +const SetFunctionDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get _setFunction() { + return CoreDeps.utils._setFunction; + }, +}; + +/** + * Set a custom function + * + * @example + * nerdamer.setFunction('f',['x'], 'x^2+2'); + * OR nerdamer.setFunction('f(x)=x^2+2'); + * OR function custom(x , y) { + * return x + y; + * } + * nerdamer.setFunction(custom); + * + * @param {string | Function} fnName - The name of the function + * @param {string[] | undefined} fnParams - A list containing the parameter name of the functions + * @param {string | undefined} fnBody - The body of the function + * @returns {typeof nerdamer} Returns nerdamer if succeeded and throws on fail + */ +function setFunction(fnName, fnParams, fnBody) { + if (!SetFunctionDeps._setFunction(fnName, fnParams, fnBody)) { + throw new Error('Failed to set function!'); + } + return SetFunctionDeps.libExports; +} + +// Clear Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses ClearDeps for dependency injection. + +/** + * Dependencies for clear function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: NerdamerType; + * EXPRESSIONS: ExpressionType[]; + * }} + */ +const ClearDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get EXPRESSIONS() { + return CoreDeps.state.EXPRESSIONS; + }, +}; + +/** + * Clear expressions from history + * + * @param {number | 'all' | 'last' | 'first'} equationNumber - The number of the equation to clear. If 'all' is supplied + * then all equations are cleared + * @param {boolean} [keepExpressionsFixed] - Use true if you don't want to keep EXPRESSIONS length fixed + * @returns {typeof nerdamer} Returns the nerdamer object for chaining + */ +function clear(equationNumber, keepExpressionsFixed = false) { + if (/** @type {unknown} */ (equationNumber) === 'all') { + ClearDeps.EXPRESSIONS.length = 0; + } else if (/** @type {unknown} */ (equationNumber) === 'last') { + ClearDeps.EXPRESSIONS.pop(); + } else if (/** @type {unknown} */ (equationNumber) === 'first') { + ClearDeps.EXPRESSIONS.shift(); + } else { + const index = equationNumber ? /** @type {number} */ (equationNumber) - 1 : ClearDeps.EXPRESSIONS.length; + keepExpressionsFixed === true + ? (ClearDeps.EXPRESSIONS[index] = undefined) + : remove(ClearDeps.EXPRESSIONS, index); + } + return ClearDeps.libExports; +} + +// Register Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses RegisterDeps for dependency injection. + +/** + * Dependencies for the register function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: typeof nerdamer; + * Settings: SettingsType; + * functions: Record; + * }} + */ +const RegisterDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get Settings() { + return CoreDeps.settings; + }, + get functions() { + return CoreDeps.parser?.functions ?? {}; + }, +}; + +/** + * Register modules/addons with nerdamer + * + * @param {object | object[]} obj - The addon object or array of addon objects to register + * @returns {void} + */ +function register(obj) { + const core = RegisterDeps.libExports.getCore(); + + if (isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + if (obj) { + register(obj[i]); + } + } + } else if (obj && RegisterDeps.Settings.exclude.indexOf(obj.name) === -1) { + // Make sure all the dependencies are available + if (obj.dependencies) { + for (let i = 0; i < obj.dependencies.length; i++) { + if (!core[obj.dependencies[i]]) { + throw new Error(format('{0} requires {1} to be loaded!', obj.name, obj.dependencies[i])); + } + } + } + // If no parent object is provided then the function does not have an address and cannot be called directly + const parentObj = obj.parent; + const fn = obj.build.call(core); // Call constructor to get function + if (parentObj) { + if (!core[parentObj]) { + core[obj.parent] = {}; + } + + const refObj = parentObj === 'nerdamer' ? RegisterDeps.libExports : core[parentObj]; + // Attach the function to the core + refObj[obj.name] = fn; + } + if (obj.visible) { + RegisterDeps.functions[obj.name] = [fn, obj.numargs]; + } // Make the function available + } +} + +// Set Function ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses SettingsDeps for dependency injection. + +/** + * Dependencies for the set function. Initialized inside the IIFE with actual references. + * + * @type {{ + * bigDec: DecimalStaticType; + * Settings: SettingsType; + * functions: Record; + * symfunction: Function; + * NerdamerSymbol: SymbolConstructor; + * }} + */ +const SettingsDeps = { + get bigDec() { + return CoreDeps.ext.bigDec; + }, + get Settings() { + return CoreDeps.settings; + }, + get functions() { + return CoreDeps.parser?.functions ?? {}; + }, + get symfunction() { + return CoreDeps.utils.symfunction; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, +}; + +/** + * Set the value of a setting + * + * @param {string | object} setting - The setting to be changed + * @param {boolean | number | string} [value] - The value to set + * @returns {void} + */ +function set(setting, value) { + // Current options: + // PARSE2NUMBER, suppress_errors + if (typeof setting === 'object' && setting !== null) { + const settingObj = /** @type {Partial} */ (setting); + for (const x in settingObj) { + if (!Object.hasOwn(settingObj, x)) { + continue; + } + set(x, settingObj[x]); + } + } + + const disallowed = ['SAFE']; + if (disallowed.indexOf(/** @type {string} */ (setting)) !== -1) { + err(`Cannot modify setting: ${setting}`); + } + + if (setting === 'PRECISION') { + // @ts-expect-error - bigDec.set precision accepts number | string at runtime + SettingsDeps.bigDec.set({ precision: value }); + SettingsDeps.Settings.PRECISION = /** @type {number} */ (value); + + // Avoid that nerdamer puts out garbage after 21 decimal place + if (/** @type {number} */ (value) > 21) { + set('USE_BIG', true); + } + } else if (setting === 'USE_LN' && value === true) { + // Set log as LN + SettingsDeps.Settings.LOG = 'LN'; + // Set log10 as log + SettingsDeps.Settings.LOG10 = 'log'; + // Point the functions in the right direction + SettingsDeps.functions.log = SettingsDeps.Settings.LOG_FNS.log10; // Log is now log10 + // the log10 function must be explicitly set + SettingsDeps.functions.log[0] = function log10Wrapper(x) { + if (x.isConstant()) { + return new SettingsDeps.NerdamerSymbol(Math.log10(x)); + } + return SettingsDeps.symfunction(SettingsDeps.Settings.LOG10, [x]); + }; + SettingsDeps.functions.LN = SettingsDeps.Settings.LOG_FNS.log; // LN is now log + + // remove log10 + delete SettingsDeps.functions.log10; + } else { + SettingsDeps.Settings[/** @type {string} */ (setting)] = value; + } +} + +// UpdateAPI Function ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses UpdateAPIDeps for dependency injection. + +/** + * Dependencies for the updateAPI function. Initialized inside the IIFE with actual references. + * + * @type {{ + * libExports: NerdamerType; + * functions: Record; + * parse: (expression: ExpressionParam) => NerdamerSymbolType; + * callfunction: Function; + * Expression: ExpressionConstructor; + * }} + */ +const UpdateAPIDeps = { + get libExports() { + return CoreDeps.libExports; + }, + get functions() { + return CoreDeps.parser?.functions ?? {}; + }, + get parse() { + const { parser } = CoreDeps; + return parser?.parse?.bind(parser) ?? (() => null); + }, + get callfunction() { + return CoreDeps.utils.callfunction; + }, + get Expression() { + return CoreDeps.classes.Expression; + }, +}; + +/** + * Makes internal functions available externally + * + * @param {boolean} [override] - Override the functions when calling updateAPI if it exists + * @returns {void} + */ +function updateAPI(override = false) { + // Map internal functions to external ones + const linker = function linker(fname) { + return function linkedFunction(...args) { + for (let i = 0; i < args.length; i++) { + args[i] = UpdateAPIDeps.parse(args[i]); + } + return new UpdateAPIDeps.Expression(block('PARSE2NUMBER', () => UpdateAPIDeps.callfunction(fname, args))); + }; + }; + // Perform the mapping + for (const x in UpdateAPIDeps.functions) { + if (!(x in UpdateAPIDeps.libExports) || override) { + UpdateAPIDeps.libExports[x] = linker(x); + } + } +} + +// Err Function ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses ErrDeps for dependency injection of suppress_errors setting. + +/** + * Dependencies for the err function. Initialized inside the IIFE with actual Settings values. + * + * @type {{ + * suppress_errors: boolean; + * }} + */ +const ErrDeps = { + get suppress_errors() { + return CoreDeps.settings?.suppress_errors ?? false; + }, +}; + +/** + * Use this when errors are suppressible + * + * @param {string} msg + * @param {new (message?: string) => Error} [ErrorObj] + */ +function err(msg, ErrorObj = undefined) { + if (!ErrDeps.suppress_errors) { + if (ErrorObj) { + throw new ErrorObj(msg); + } else { + throw new Error(msg); + } + } +} + +// IsPrime Function ============================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses IsPrimeDeps for dependency injection of PRIMES_SET. + +/** + * Dependencies for the isPrime function. Initialized inside the IIFE with actual PRIMES_SET reference. + * + * @type {{ + * PRIMES_SET: Record; + * }} + */ +const IsPrimeDeps = { + get PRIMES_SET() { + return CoreDeps.ext.PRIMES_SET; + }, +}; + +/** + * Checks if number is a prime number + * + * @param {number} n - The number to be checked + * @returns {boolean} + */ +function isPrime(n) { + if (n in IsPrimeDeps.PRIMES_SET) { + return true; + } + const q = Math.floor(Math.sqrt(n)); + for (let i = 2; i <= q; i++) { + if (n % i === 0) { + return false; + } + } + return true; +} + +// Timeout Functions =============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses TimeoutDeps for shared state and dependency injection of Settings.TIMEOUT. + +/** + * Dependencies for the timeout functions. Contains shared state and is initialized inside the IIFE. + * + * @type {{ + * starttime: number; + * timeout: number; + * TIMEOUT: number; + * }} + */ +const TimeoutDeps = { + starttime: 0, + timeout: 0, + get TIMEOUT() { + return CoreDeps.settings?.TIMEOUT ?? 800; + }, +}; + +/** Arms the timeout mechanism with current time and timeout setting */ +function armTimeout() { + TimeoutDeps.starttime = Date.now(); + TimeoutDeps.timeout = TimeoutDeps.TIMEOUT; +} + +/** Disarms the timeout mechanism */ +function disarmTimeout() { + TimeoutDeps.starttime = 0; +} + +/** + * Checks if timeout has been exceeded and throws if so + * + * @throws {Error} If timeout has been exceeded + */ +function checkTimeout() { + if (TimeoutDeps.starttime !== 0 && Date.now() > TimeoutDeps.starttime + TimeoutDeps.timeout) { + throw new Error('timeout'); + } +} + +// PrimeFactors Function ============================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses PrimeFactorsDeps for dependency injection of PRIMES and PRIMES_SET. + +/** + * Dependencies for the primeFactors function. Initialized inside the IIFE. + * + * @type {{ + * PRIMES: number[]; + * PRIMES_SET: Record; + * }} + */ +const PrimeFactorsDeps = { + get PRIMES() { + return CoreDeps.ext.PRIMES; + }, + get PRIMES_SET() { + return CoreDeps.ext.PRIMES_SET; + }, +}; + +/** + * Calculates prime factors for a number. It first checks if the number is a prime number. If it's not then it will + * calculate all the primes for that number. + * + * @param {number} num + * @returns {number[]} + */ +function primeFactors(num) { + checkTimeout(); + + if (isPrime(num)) { + return [num]; + } + + let l = num; + let i = 1; + const factors = []; + const epsilon = 2.2204460492503130808472633361816e-16; + while (i < l) { + checkTimeout(); + const quotient = num / i; + const whole = Math.floor(quotient); + const remainder = quotient - whole; + + if (remainder <= epsilon && i > 1) { + // If the prime wasn't found but calculated then save it and + // add it as a factor. + if (isPrime(i)) { + if (!PrimeFactorsDeps.PRIMES_SET[i]) { + PrimeFactorsDeps.PRIMES.push(i); + PrimeFactorsDeps.PRIMES_SET[i] = true; + } + factors.push(i); + } + + // Check if the remainder is a prime + if (isPrime(whole)) { + factors.push(whole); + break; + } + + l = whole; + } + i++; + } + + return factors.sort((a, b) => a - b); +} + +/** + * Generates prime numbers up to a specified number + * + * @param {number} upto + */ +function generatePrimes(upto) { + // Get the last prime in the array + const lastPrime = PrimeFactorsDeps.PRIMES[PrimeFactorsDeps.PRIMES.length - 1] || 2; + // No need to check if we've already encountered the number. Just check the cache. + for (let i = lastPrime; i < upto; i++) { + if (isPrime(i)) { + PrimeFactorsDeps.PRIMES.push(i); + } + PrimeFactorsDeps.PRIMES_SET[i] = true; + } +} + +// Block Function ================================================================ +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via BlockDeps which is set by the IIFE after initialization. + +/** + * Dependency container for block function. Populated by the IIFE during initialization. + * + * @type {{ + * Settings: SettingsType; + * }} + */ +const BlockDeps = { + get Settings() { + return CoreDeps.settings; + }, +}; + +/** + * Creates a temporary block in which one of the global settings is temporarily modified while the function is called. + * For instance if you want to parse directly to a number rather than have a symbolic answer for a period you would set + * PARSE2NUMBER to true in the block. + * + * @example + * block('PARSE2NUMBER', function(){//symbol being parsed to number}, true); + * + * @template T + * @param {string} setting - The setting being accessed + * @param {() => T} f + * @param {boolean} [opt] - The value of the setting in the block + * @param {unknown} [obj] - The obj of interest. Usually a NerdamerSymbol but could be any object + * @returns {T} + */ +function block(setting, f, opt = undefined, obj = undefined) { + const currentSetting = BlockDeps.Settings[setting]; + BlockDeps.Settings[setting] = opt === undefined ? true : !!opt; + const retval = f.call(obj); + BlockDeps.Settings[setting] = currentSetting; + return retval; +} + +// Evaluate Function ================================================================ +// Uses ParserDeps._ for parser access. + +/** + * As the name states. It forces evaluation of the expression + * + * @param {string | NerdamerSymbolType} symbol + * @param {Record} [o] + * @returns {NerdamerSymbolType} + */ +function evaluate(symbol, o = undefined) { + return block('PARSE2NUMBER', () => ParserDeps._.parse(symbol, o), true); +} + +// Expression Class ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are accessed via CoreDeps for centralized management. + +/** + * Dependency accessor for Expression class. Uses CoreDeps as the single source of truth. + * + * @type {{ + * EXPRESSIONS: ExpressionType[]; + * Settings: SettingsType & { precision?: number }; + * LaTeX: LaTeXInterface; + * text: Function; + * variables: Function; + * isVector: (x: unknown) => boolean; + * isSymbol: (x: unknown) => boolean; + * isExpression: (x: unknown) => boolean; + * isNumericSymbol: (x: unknown) => boolean; + * isFraction: (x: unknown) => boolean; + * isArray: (x: unknown) => boolean; + * _: ParserType; + * Build: BuildInterface; + * }} + */ +const ExpressionDeps = { + get EXPRESSIONS() { + return CoreDeps.state.EXPRESSIONS; + }, + get Settings() { + return CoreDeps.settings; + }, + get LaTeX() { + return CoreDeps.classes.LaTeX; + }, + get text() { + return CoreDeps.utils.text; + }, + get variables() { + return CoreDeps.utils.variables; + }, + get isVector() { + return CoreDeps.utils.isVector; + }, + get isSymbol() { + return CoreDeps.utils.isSymbol; + }, + get isExpression() { + return CoreDeps.utils.isExpression; + }, + get isNumericSymbol() { + return CoreDeps.utils.isNumericSymbol; + }, + get isFraction() { + return CoreDeps.utils.isFraction; + }, + get isArray() { + return CoreDeps.utils.isArray; + }, + get _() { + return CoreDeps.parser; + }, + get Build() { + return CoreDeps.classes.Build; + }, +}; + +/** + * Wraps a symbol in an Expression for user-facing API. + * + * @implements {ExpressionType} + */ +class Expression { + /** @type {NerdamerSymbolType} */ + symbol; + + /** @param {NerdamerSymbolType} symbol */ + constructor(symbol) { + // We don't want arrays wrapped + this.symbol = symbol; + } + + /** + * Returns stored expression at index. For first index use 1 not 0. + * + * @param {number | string} expressionNumber + * @param {boolean} [_asType] + */ + static getExpression(expressionNumber, _asType = undefined) { + if (expressionNumber === 'last' || !expressionNumber) { + expressionNumber = ExpressionDeps.EXPRESSIONS.length; + } + if (expressionNumber === 'first') { + expressionNumber = 1; + } + const index = Number(expressionNumber) - 1; + const expression = ExpressionDeps.EXPRESSIONS[index]; + const retval = expression ? new Expression(/** @type {NerdamerSymbolType} */ (expression.symbol)) : expression; + return retval; + } + + /** + * Returns the text representation of the expression + * + * @param {string} [opt] - Option of formatting numbers + * @param {number} [n] The number of significant figures + * @returns {string} + */ + text(opt = 'decimals', n = undefined) { + n ||= ExpressionDeps.Settings.EXPRESSION_DECP; + const sym = /** @type {NerdamerSymbolType} */ (this.symbol); + if (sym.text_) { + return sym.text_(opt); + } + + return ExpressionDeps.text(this.symbol, opt, undefined, n); + } + + /** + * Returns the latex representation of the expression + * + * @param {OutputType} option - Option for formatting numbers + * @returns {string} + */ + latex(option) { + if (this.symbol.latex) { + return this.symbol.latex(option); + } + return ExpressionDeps.LaTeX.latex(this.symbol, option); + } + + /** @returns {number | string | DecimalType} */ + valueOf() { + return this.symbol.valueOf(); + } + + /** + * Evaluates the expression and tries to reduce it to a number if possible. If an argument is given in the form of + * %{integer} it will evaluate that expression. Other than that it will just use it's own text and reparse + * + * @returns {ExpressionType} + */ + evaluate(...args) { + // Don't evaluate an empty vector + if ( + ExpressionDeps.isVector(this.symbol) && + /** @type {VectorType} */ (/** @type {unknown} */ (this.symbol)).dimensions() === 0 + ) { + return this; + } + + const firstArg = args[0]; + let expression; + let idx = 1; + + // Enable getting of expressions using the % so for example %1 should get the first expression + if (typeof firstArg === 'string') { + // TODO Replace substr with slice, and test it + expression = firstArg.charAt(0) === '%' ? Expression.getExpression(firstArg.substr(1)).text() : firstArg; + } else if (firstArg instanceof Expression || ExpressionDeps.isSymbol(firstArg)) { + expression = firstArg.text(); + } else { + expression = this.symbol.text(); + idx--; + } + + const subs = args[idx] || {}; + + const retval = new Expression(block('PARSE2NUMBER', () => ExpressionDeps._.parse(expression, subs), true)); + + return retval; + } + + /** + * Converts a symbol to a JS function. Pass in an array of variables to use that order instead of the default + * alphabetical order + * + * @param {string[]} vars + * @returns {(...args: number[]) => number} + */ + buildFunction(vars) { + return /** @type {(...args: number[]) => number} */ (ExpressionDeps.Build.build(this.symbol, vars)); + } + + /** + * Checks to see if the expression is just a plain old number + * + * @returns {boolean} + */ + isNumber() { + return ExpressionDeps.isNumericSymbol(this.symbol); + } + + /** + * Checks to see if the expression is infinity + * + * @returns {boolean} + */ + isInfinity() { + return Math.abs(/** @type {number} */ (this.symbol.multiplier.valueOf())) === Infinity; + } + + /** + * Checks to see if the expression contains imaginary numbers + * + * @returns {boolean} + */ + isImaginary() { + return evaluate(ExpressionDeps._.parse(this.symbol)).isImaginary(); + } + + /** + * Returns all the variables in the expression + * + * @returns {Array} + */ + variables() { + return ExpressionDeps.variables(this.symbol); + } + + /** @returns {string} */ + toString() { + try { + if (ExpressionDeps.isArray(this.symbol)) { + return `[${this.symbol.toString()}]`; + } + return this.symbol.toString(); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return ''; + } + } + + /** + * Forces the symbol to be returned as a decimal + * + * @param {number} [prec] + * @returns {string} + */ + toDecimal(prec) { + ExpressionDeps.Settings.precision = prec; + const dec = ExpressionDeps.text(this.symbol, 'decimals'); + ExpressionDeps.Settings.precision = undefined; + return dec; + } + + /** + * Checks to see if the expression is a fraction + * + * @returns {boolean} + */ + isFraction() { + return ExpressionDeps.isFraction(this.symbol); + } + + /** + * Checks to see if the symbol is a multivariate polynomial + * + * @returns {boolean} + */ + isPolynomial() { + return this.symbol.isPoly(); + } + + /** + * Performs a substitution + * + * @param {string | NerdamerSymbolType} symbol + * @param {string | number | NerdamerSymbolType} forSymbol + * @returns {ExpressionType} + */ + sub(symbol, forSymbol) { + return new Expression(this.symbol.sub(ExpressionDeps._.parse(symbol), ExpressionDeps._.parse(forSymbol))); + } + + /** + * @param {string} otype + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + operation(otype, symbol) { + /** @type {NerdamerSymbolType} */ + let sym; + if (ExpressionDeps.isExpression(symbol)) { + sym = /** @type {ExpressionType} */ (symbol).symbol; + } else if (ExpressionDeps.isSymbol(symbol)) { + sym = /** @type {NerdamerSymbolType} */ (symbol); + } else { + sym = ExpressionDeps._.parse(/** @type {string | number} */ (symbol)); + } + return new Expression(ExpressionDeps._[otype](this.symbol.clone(), sym.clone())); + } + + /** + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + add(symbol) { + return this.operation('add', symbol); + } + + /** + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + subtract(symbol) { + return this.operation('subtract', symbol); + } + + /** + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + multiply(symbol) { + return this.operation('multiply', symbol); + } + + /** + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + divide(symbol) { + return this.operation('divide', symbol); + } + + /** + * @param {string | number | NerdamerSymbolType | ExpressionType} symbol + * @returns {ExpressionType} + */ + pow(symbol) { + return this.operation('pow', symbol); + } + + /** @returns {ExpressionType} */ + expand() { + return new Expression(/** @type {NerdamerSymbolType} */ (ExpressionDeps._.expand(this.symbol))); + } + + /** + * @param {Function} callback + * @param {boolean} [deep] + */ + each(callback, deep) { + if (this.symbol.each) { + this.symbol.each(/** @type {(symbol: NerdamerSymbolType, key: string) => void} */ (callback), deep); + } else if (ExpressionDeps.isArray(this.symbol)) { + for (let idx = 0; idx < this.symbol.length; idx++) { + callback.call(this.symbol, this.symbol[idx], idx); + } + } else { + callback.call(this.symbol); + } + } + + /** + * @param {string | number | NerdamerSymbolType} value + * @returns {boolean} + */ + eq(value) { + if (!ExpressionDeps.isSymbol(value)) { + value = /** @type {NerdamerSymbolType} */ (ExpressionDeps._.parse(value)); + } + try { + const d = /** @type {NerdamerSymbolType} */ ( + ExpressionDeps._.subtract(this.symbol.clone(), /** @type {NerdamerSymbolType} */ (value)) + ); + return d.equals(0); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return false; + } + } + + /** + * @param {string | number | NerdamerSymbolType} value + * @returns {boolean} + */ + lt(value) { + if (!ExpressionDeps.isSymbol(value)) { + value = /** @type {NerdamerSymbolType} */ (ExpressionDeps._.parse(value)); + } + try { + const d = evaluate( + /** @type {NerdamerSymbolType} */ ( + ExpressionDeps._.subtract(this.symbol.clone(), /** @type {NerdamerSymbolType} */ (value)) + ) + ); + return d.lessThan(0); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return false; + } + } + + /** + * @param {string | number | NerdamerSymbolType} value + * @returns {boolean} + */ + gt(value) { + if (!ExpressionDeps.isSymbol(value)) { + value = /** @type {NerdamerSymbolType} */ (ExpressionDeps._.parse(value)); + } + try { + const d = evaluate( + /** @type {NerdamerSymbolType} */ ( + ExpressionDeps._.subtract(this.symbol.clone(), /** @type {NerdamerSymbolType} */ (value)) + ) + ); + return d.greaterThan(0); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return false; + } + } + + /** + * @param {string | number | NerdamerSymbolType} value + * @returns {boolean} + */ + gte(value) { + return this.gt(value) || this.eq(value); + } + + /** + * @param {string | number | NerdamerSymbolType} value + * @returns {boolean} + */ + lte(value) { + return this.lt(value) || this.eq(value); + } + + /** @returns {ExpressionType} */ + numerator() { + return new Expression(this.symbol.getNum()); + } + + /** @returns {ExpressionType} */ + denominator() { + return new Expression(this.symbol.getDenom()); + } + + /** + * @param {string | string[]} f + * @returns {boolean} + */ + hasFunction(f) { + return this.symbol.containsFunction(f); + } + + /** + * @param {string} variable + * @returns {boolean} + */ + contains(variable) { + return this.symbol.contains(variable); + } + + /** + * Alias for latex + * + * @param {OutputType} option + * @returns {string} + */ + toTeX(option) { + return this.latex(option); + } +} + +// Assign Expression to CoreDeps immediately +CoreDeps.classes.Expression = Expression; + +// Vector Class ===================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via VectorDeps which is set by the IIFE after initialization. + +/** + * Dependency container for Vector class. Populated by the IIFE during initialization. Only IIFE-scope values and + * forward-referenced values need injection. + * + * @type {{ + * _: ParserType; + * Settings: { PRECISION: number }; + * LaTeX: LaTeXInterface; + * NerdamerSymbol: SymbolConstructor; + * }} + */ +const VectorDeps = { + get _() { + return CoreDeps.parser; + }, + get Settings() { + return CoreDeps.settings; + }, + get LaTeX() { + return CoreDeps.classes.LaTeX; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, +}; + +/** + * Vector class - Ported from Sylvester.js + * + * @implements {VectorType} + */ +class Vector { + /** @type {FracType} */ + multiplier; + + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ + elements; + + /** @type {boolean | undefined} */ + rowVector; + + /** + * Custom marker for parser + * + * @type {true} + */ + custom = true; + + /** + * @param {VectorType | MatrixType | NerdamerSymbolType[] | NerdamerSymbolType | undefined} [v] + * @param {...NerdamerSymbolType} rest + */ + constructor(v, ...rest) { + this.multiplier = new Frac(1); + if (isVector(v)) { + this.elements = /** @type {(NerdamerSymbol | Vector | Matrix)[]} */ ( + /** @type {unknown[]} */ (v.elements)?.slice(0) ?? [] + ); + } else if (isArray(v)) { + this.elements = v.slice(0); + } else if (isMatrix(v)) { + if (v.elements.length === 1) { + this.elements = [...v.elements[0]]; + this.rowVector = true; + } else if (v.elements.length > 1 && Array.isArray(v.elements[0]) && v.elements[0].length === 1) { + this.elements = v.elements.map(row => row[0]); + this.rowVector = false; + } + } else if (typeof v === 'undefined') { + this.elements = []; + } else { + this.elements = [v, ...rest]; + } + } + + /** + * Generates a pre-filled array + * + * @param {number} n + * @param {NerdamerSymbolType | number} [val] + * @returns {(NerdamerSymbolType | number)[]} + */ + static arrayPrefill(n, val) { + const a = []; + val ||= 0; + for (let i = 0; i < n; i++) { + a[i] = val; + } + return a; + } + + /** + * Generate a vector from an array + * + * @param {(NerdamerSymbolType | VectorType | MatrixType | string | number)[]} a + * @returns {VectorType} + */ + static fromArray(a) { + const v = new Vector(); + v.elements = /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ (a); + return v; + } + + /** + * Convert a NerdamerSet to a Vector + * + * @param {SetType} nerdamerSet + * @returns {VectorType} + */ + static fromSet(nerdamerSet) { + return Vector.fromArray(nerdamerSet.elements); + } + + /** + * Returns element i of the vector + * + * @param {number} i + * @returns {NerdamerSymbolType | VectorType | MatrixType | null} + */ + e(i) { + return i < 1 || i > this.elements.length ? null : this.elements[i - 1]; + } + + /** + * @param {number} i + * @param {NerdamerSymbolType | string | number} val + */ + set(i, val) { + if (isSymbol(val)) { + this.elements[i] = val; + } else { + this.elements[i] = new VectorDeps.NerdamerSymbol(/** @type {string | number} */ (val)); + } + } + + /** + * Returns the number of elements the vector has + * + * @returns {number} + */ + dimensions() { + return this.elements.length; + } + + /** + * Returns the modulus ('length') of the vector + * + * @returns {NerdamerSymbolType} + */ + modulus() { + return /** @type {NerdamerSymbolType} */ ( + block( + 'SAFE', + () => VectorDeps._.pow(this.dot(this.clone()), new VectorDeps.NerdamerSymbol(0.5)), + undefined, + this + ) + ); + } + + /** + * Returns true iff the vector is equal to the argument + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {boolean} + */ + eql(vector) { + let n = this.elements.length; + const V = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + if (n !== V.length) { + return false; + } + do { + if ( + Math.abs(/** @type {number} */ (VectorDeps._.subtract(this.elements[n - 1], V[n - 1]).valueOf())) > + VectorDeps.Settings.PRECISION + ) { + return false; + } + } while (--n); + return true; + } + + /** + * Returns a clone of the vector + * + * @returns {VectorType} + */ + clone() { + const V = new Vector(); + const l = this.elements.length; + for (let i = 0; i < l; i++) { + // Rule: all items within the vector must have a clone method. + V.elements.push(this.elements[i].clone()); + } + V.rowVector = this.rowVector; + return V; + } + + /** + * @param {ExpandOptions} [options] + * @returns {this} + */ + expand(options) { + this.elements = /** @type {NerdamerSymbolType[]} */ (this.elements.map(e => VectorDeps._.expand(e, options))); + return this; + } + + /** + * Maps the vector to another vector according to the given function + * + * @param {(element: NerdamerSymbolType, index: number) => NerdamerSymbolType} fn + * @returns {VectorType} + */ + map(fn) { + const elements = []; + this.each((x, i) => { + elements.push(fn(x, i)); + }); + + return new Vector(elements); + } + + /** + * Calls the iterator for each element of the vector in turn + * + * @param {Function} fn + */ + each(fn) { + let n = this.elements.length; + const k = n; + let i; + do { + i = k - n; + fn(this.elements[i], i + 1); + } while (--n); + } + + /** + * Returns a new vector created by normalizing the receiver + * + * @returns {VectorType} + */ + toUnitVector() { + return block( + 'SAFE', + () => { + const r = this.modulus(); + if (r.valueOf() === 0) { + return this.clone(); + } + return this.map(x => /** @type {NerdamerSymbolType} */ (VectorDeps._.divide(x, r))); + }, + undefined, + this + ); + } + + /** + * Returns the angle between the vector and the argument (also a vector) + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {NerdamerSymbolType | null} + */ + angleFrom(vector) { + return block( + 'SAFE', + () => { + const V = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + const n = this.elements.length; + if (n !== V.length) { + return null; + } + let dot = new VectorDeps.NerdamerSymbol(0); + let mod1 = new VectorDeps.NerdamerSymbol(0); + let mod2 = new VectorDeps.NerdamerSymbol(0); + // Work things out in parallel to save time + this.each((x, i) => { + dot = /** @type {NerdamerSymbolType} */ (VectorDeps._.add(dot, VectorDeps._.multiply(x, V[i - 1]))); + mod1 = /** @type {NerdamerSymbolType} */ (VectorDeps._.add(mod1, VectorDeps._.multiply(x, x))); // Will not conflict in safe block + mod2 = /** @type {NerdamerSymbolType} */ ( + VectorDeps._.add(mod2, VectorDeps._.multiply(V[i - 1], V[i - 1])) + ); // Will not conflict in safe block + }); + mod1 = /** @type {NerdamerSymbolType} */ (VectorDeps._.pow(mod1, new VectorDeps.NerdamerSymbol(0.5))); + mod2 = /** @type {NerdamerSymbolType} */ (VectorDeps._.pow(mod2, new VectorDeps.NerdamerSymbol(0.5))); + const product = /** @type {NerdamerSymbolType} */ (VectorDeps._.multiply(mod1, mod2)); + if (product.valueOf() === 0) { + return null; + } + /** @type {NerdamerSymbolType | number} */ + let theta = /** @type {NerdamerSymbolType} */ (VectorDeps._.divide(dot, product)); + const thetaVal = /** @type {number} */ (theta.valueOf()); + if (thetaVal < -1) { + theta = -1; + } + if (thetaVal > 1) { + theta = 1; + } + return new VectorDeps.NerdamerSymbol(Math.acos(/** @type {number} */ (theta))); + }, + undefined, + this + ); + } + + /** + * Returns true iff the vector is parallel to the argument + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {boolean | null} + */ + isParallelTo(vector) { + const angle = /** @type {number | null} */ (this.angleFrom(vector).valueOf()); + return angle === null ? null : angle <= VectorDeps.Settings.PRECISION; + } + + /** + * Returns true iff the vector is antiparallel to the argument + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {boolean | null} + */ + isAntiparallelTo(vector) { + const angle = /** @type {number | null} */ (this.angleFrom(vector).valueOf()); + return angle === null ? null : Math.abs(angle - Math.PI) <= VectorDeps.Settings.PRECISION; + } + + /** + * Returns true iff the vector is perpendicular to the argument + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {boolean | null} + */ + isPerpendicularTo(vector) { + const dot = this.dot(vector); + return dot === null + ? null + : Math.abs(/** @type {number} */ (/** @type {unknown} */ (dot))) <= VectorDeps.Settings.PRECISION; + } + + /** + * Returns the result of adding the argument to the vector + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {VectorType | null} + */ + add(vector) { + return block( + 'SAFE', + () => { + const V = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + if (this.elements.length !== V.length) { + return null; + } + return this.map((x, i) => /** @type {NerdamerSymbolType} */ (VectorDeps._.add(x, V[i - 1]))); + }, + undefined, + this + ); + } + + /** + * Returns the result of subtracting the argument from the vector + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {VectorType | null} + */ + subtract(vector) { + return block( + 'SAFE', + () => { + const V = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + if (this.elements.length !== V.length) { + return null; + } + return this.map((x, i) => /** @type {NerdamerSymbolType} */ (VectorDeps._.subtract(x, V[i - 1]))); + }, + undefined, + this + ); + } + + /** + * Returns the result of multiplying the elements of the vector by the argument + * + * @param {NerdamerSymbolType} k + * @returns {VectorType} + */ + multiply(k) { + return this.map(x => /** @type {NerdamerSymbolType} */ (VectorDeps._.multiply(x.clone(), k.clone()))); + } + + /** + * Alias for multiply + * + * @param {NerdamerSymbolType} k + * @returns {VectorType} + */ + x(k) { + return this.multiply(k); + } + + /** + * Returns the scalar product of the vector with the argument Both vectors must have equal dimensionality + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {NerdamerSymbolType | null} + */ + dot(vector) { + return block( + 'SAFE', + () => { + const V = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + let product = new VectorDeps.NerdamerSymbol(0); + let n = this.elements.length; + if (n !== V.length) { + return null; + } + do { + product = /** @type {NerdamerSymbolType} */ ( + VectorDeps._.add(product, VectorDeps._.multiply(this.elements[n - 1], V[n - 1])) + ); + } while (--n); + return product; + }, + undefined, + this + ); + } + + /** + * Returns the vector product of the vector with the argument Both vectors must have dimensionality 3 + * + * @param {VectorType | NerdamerSymbolType[]} vector + * @returns {VectorType | null} + */ + cross(vector) { + const B = /** @type {NerdamerSymbolType[]} */ (/** @type {VectorType} */ (vector).elements || vector); + if (this.elements.length !== 3 || B.length !== 3) { + return null; + } + const rowVector = this.rowVector && /** @type {VectorType} */ (vector).rowVector; + const A = this.elements; + return block( + 'SAFE', + () => { + const result = new Vector([ + /** @type {NerdamerSymbolType} */ ( + VectorDeps._.subtract(VectorDeps._.multiply(A[1], B[2]), VectorDeps._.multiply(A[2], B[1])) + ), + /** @type {NerdamerSymbolType} */ ( + VectorDeps._.subtract(VectorDeps._.multiply(A[2], B[0]), VectorDeps._.multiply(A[0], B[2])) + ), + /** @type {NerdamerSymbolType} */ ( + VectorDeps._.subtract(VectorDeps._.multiply(A[0], B[1]), VectorDeps._.multiply(A[1], B[0])) + ), + ]); + result.rowVector = rowVector; + return result; + }, + undefined, + this + ); + } + + /** @returns {this} */ + toUnitMultiplier() { + return this; + } + + /** + * Returns the (absolute) largest element of the vector + * + * @returns {NerdamerSymbolType | VectorType | MatrixType | number} + */ + max() { + /** @type {NerdamerSymbolType | VectorType | MatrixType | number} */ + let m = 0; + let n = this.elements.length; + const k = n; + let i; + do { + i = k - n; + const el = this.elements[i]; + const elVal = /** @type {number} */ (el.valueOf()); + const mVal = typeof m === 'number' ? m : /** @type {number} */ (m.valueOf()); + if (Math.abs(elVal) > Math.abs(mVal)) { + m = el; + } + } while (--n); + return m; + } + + /** @returns {NerdamerSymbolType} */ + magnitude() { + let magnitude = new VectorDeps.NerdamerSymbol(0); + this.each(e => { + magnitude = /** @type {NerdamerSymbolType} */ ( + VectorDeps._.add(magnitude, VectorDeps._.pow(e, new VectorDeps.NerdamerSymbol(2))) + ); + }); + return /** @type {NerdamerSymbolType} */ (VectorDeps._.sqrt(magnitude)); + } + + /** + * Returns the index of the first match found + * + * @param {NerdamerSymbolType | number} x + * @returns {number | null} + */ + indexOf(x) { + let index = null; + let n = this.elements.length; + const k = n; + let i; + do { + i = k - n; + if (index === null && this.elements[i].valueOf() === x.valueOf()) { + index = i + 1; + } + } while (--n); + return index; + } + + /** + * @param {unknown} x - Unused parameter + * @param {{ decimals?: boolean; decimalPlaces?: number }} [options] + * @returns {string} + */ + text_(x, options) { + const result = text( + /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (this)), + /** @type {string | undefined} */ (options) + ); + return (this.rowVector ? '[' : '') + result + (this.rowVector ? ']' : ''); + } + + /** + * @param {unknown} x - Unused parameter + * @param {{ decimals?: boolean; decimalPlaces?: number }} [options] + * @returns {string} + */ + text(x, options) { + const result = text( + /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (this)), + /** @type {string | undefined} */ (options) + ); + return (this.rowVector ? '[' : '') + result + (this.rowVector ? ']' : ''); + } + + /** @returns {string} */ + toString() { + return this.text(); + } + + /** + * @param {OutputType} [option] + * @returns {string} + */ + latex(option) { + const tex = []; + for (let i = 0; i < this.elements.length; i++) { + tex.push(VectorDeps.LaTeX.latex(this.elements[i], option)); + } + return `[${tex.join(', ')}]`; + } +} + +// Assign Vector to CoreDeps immediately +CoreDeps.classes.Vector = Vector; + +// Matrix Class ===================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Uses module-scope values directly. MatrixDeps only provides the parser (_) from the IIFE. + +/** + * Dependency container for Matrix class. Populated by the IIFE during initialization. + * + * @type {{ _: ParserType; LaTeX: LaTeXInterface; NerdamerSymbol: SymbolConstructor }} + */ +const MatrixDeps = { + get _() { + return CoreDeps.parser; + }, + get LaTeX() { + return CoreDeps.classes.LaTeX; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, +}; + +/** + * Matrix class - Ported from Sylvester.js + * + * @implements {MatrixType} + */ +class Matrix { + /** @type {FracType} */ + multiplier; + + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[][]} */ + elements; + + /** + * Custom marker for parser + * + * @type {true} + */ + custom = true; + + /** @param {...unknown} args */ + constructor(...args) { + this.multiplier = new Frac(1); + const m = args; + const l = m.length; + let i; + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[][]} */ + const el = []; + if (isMatrix(m)) { + // If it's a matrix then make a clone + for (i = 0; i < l; i++) { + el.push(/** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ (m[i]).slice(0)); + } + } else { + let row; + let lw; + let rl; + for (i = 0; i < l; i++) { + row = m[i]; + if (isVector(row)) { + row = row.elements; + } + if (!isArray(row)) { + row = [row]; + } + rl = row.length; + if (lw && lw !== rl) { + err('Unable to create Matrix. Row dimensions do not match!'); + } + el.push(/** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ (row)); + lw = rl; + } + } + this.elements = el; + } + + /** + * @param {number} n + * @returns {MatrixType} + */ + static identity(n) { + const m = new Matrix(); + for (let i = 0; i < n; i++) { + m.elements.push([]); + for (let j = 0; j < n; j++) { + m.set(i, j, i === j ? new MatrixDeps.NerdamerSymbol(1) : new MatrixDeps.NerdamerSymbol(0)); + } + } + return m; + } + + /** + * @param {unknown[]} arr + * @returns {MatrixType} + */ + static fromArray(arr) { + return new Matrix(...arr); + } + + /** + * @param {number} rows + * @param {number} cols + * @returns {MatrixType} + */ + static zeroMatrix(rows, cols) { + const m = new Matrix(); + for (let i = 0; i < rows; i++) { + m.elements.push( + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ ( + Vector.arrayPrefill(cols, new MatrixDeps.NerdamerSymbol(0)) + ) + ); + } + return m; + } + + /** + * @param {number} row + * @param {number} column + * @returns {NerdamerSymbolType | VectorType | MatrixType | undefined} + */ + get(row, column) { + if (!this.elements[row]) { + return undefined; + } + return this.elements[row][column]; + } + + /** + * @param {(element: NerdamerSymbolType) => NerdamerSymbolType} f + * @param {boolean} [rawValues] + * @returns {MatrixType} + */ + map(f, rawValues) { + const M = new Matrix(); + this.each((e, i, j) => { + M.set(i, j, f.call(M, e), rawValues); + }); + return M; + } + + /** + * @param {number} row + * @param {number} column + * @param {NerdamerSymbolType | VectorType | MatrixType | string | number} value + * @param {boolean} [raw] + */ + set(row, column, value, raw) { + this.elements[row] ||= []; + if (raw || isSymbol(value)) { + this.elements[row][column] = /** @type {NerdamerSymbolType | VectorType | MatrixType} */ (value); + } else { + this.elements[row][column] = new MatrixDeps.NerdamerSymbol( + /** @type {string | number | FracType} */ (value) + ); + } + } + + /** @returns {number} */ + cols() { + return this.elements[0].length; + } + + /** @returns {number} */ + rows() { + return this.elements.length; + } + + /** + * @param {number} n + * @returns {(NerdamerSymbolType | VectorType | MatrixType)[]} + */ + row(n) { + if (!n || n > this.cols()) { + return []; + } + return this.elements[n - 1]; + } + + /** + * @param {number} n + * @returns {(NerdamerSymbolType | VectorType | MatrixType)[]} + */ + col(n) { + const nr = this.rows(); + const col = []; + if (n > this.cols() || !n) { + return col; + } + for (let i = 0; i < nr; i++) { + col.push(this.elements[i][n - 1]); + } + return col; + } + + /** @param {Function} fn */ + eachElement(fn) { + const nr = this.rows(); + const nc = this.cols(); + let i; + let j; + for (i = 0; i < nr; i++) { + for (j = 0; j < nc; j++) { + fn.call(this, this.elements[i][j], i, j); + } + } + } + + /** + * Alias for eachElement + * + * @param {Function} fn + */ + each(fn) { + this.eachElement(fn); + } + + /** + * Ported from Sylvester.js + * + * @returns {NerdamerSymbolType | null} + */ + determinant() { + if (!this.isSquare()) { + return null; + } + const M = this.toRightTriangular(); + let det = /** @type {NerdamerSymbolType} */ (M.elements[0][0]); + let n = M.elements.length - 1; + const k = n; + let i; + do { + i = k - n + 1; + det = /** @type {NerdamerSymbolType} */ ( + MatrixDeps._.multiply(det, /** @type {NerdamerSymbolType} */ (M.elements[i][i])) + ); + } while (--n); + return det; + } + + /** @returns {boolean} */ + isSquare() { + return this.elements.length === this.elements[0].length; + } + + /** @returns {boolean} */ + isSingular() { + const det = this.determinant(); + return this.isSquare() && det !== null && det.multiplier.equals(0); + } + + /** + * @param {MatrixType} m + * @returns {this} + */ + augment(m) { + const r = this.rows(); + const rr = m.rows(); + if (r !== rr) { + err("Cannot augment matrix. Rows don't match."); + } + for (let i = 0; i < r; i++) { + this.elements[i] = this.elements[i].concat(m.elements[i]); + } + + return this; + } + + /** @returns {MatrixType} */ + clone() { + const r = this.rows(); + const c = this.cols(); + const m = new Matrix(); + for (let i = 0; i < r; i++) { + m.elements[i] = []; + for (let j = 0; j < c; j++) { + const symbol = this.elements[i][j]; + m.elements[i][j] = isSymbol(symbol) ? symbol.clone() : symbol; + } + } + return m; + } + + /** @returns {this} */ + toUnitMultiplier() { + return this; + } + + /** + * @param {ExpandOptions} [options] + * @returns {this} + */ + expand(options) { + this.eachElement(e => MatrixDeps._.expand(e, options)); + return this; + } + + /** + * @param {Record} [options] + * @returns {this} + */ + evaluate(options) { + this.eachElement(e => MatrixDeps._.evaluate(e, options)); + return this; + } + + /** + * Ported from Sylvester.js + * + * @returns {MatrixType} + */ + invert() { + if (!this.isSquare()) { + err('Matrix is not square!'); + } + return block( + 'SAFE', + () => { + let ni = this.elements.length; + const ki = ni; + let i; + let j; + const imatrix = Matrix.identity(ni); + const M = this.augment(imatrix).toRightTriangular(); + let np; + const kp = M.elements[0].length; + let p; + let els; + let divisor; + const inverseElements = []; + let newElement; + // Matrix is non-singular so there will be no zeros on the diagonal + // Cycle through rows from last to first + do { + i = ni - 1; + // First, normalise diagonal elements to 1 + els = []; + np = kp; + inverseElements[i] = []; + divisor = M.elements[i][i]; + do { + p = kp - np; + newElement = MatrixDeps._.divide(M.elements[i][p], divisor.clone()); + els.push(newElement); + // Shuffle of the current row of the right hand side into the results + // array as it will not be modified by later runs through this loop + if (p >= ki) { + inverseElements[i].push(newElement); + } + } while (--np); + M.elements[i] = els; + // Then, subtract this row from those above it to + // give the identity matrix on the left hand side + for (j = 0; j < i; j++) { + els = []; + np = kp; + do { + p = kp - np; + els.push( + MatrixDeps._.subtract( + M.elements[j][p].clone(), + MatrixDeps._.multiply(M.elements[i][p].clone(), M.elements[j][i].clone()) + ) + ); + } while (--np); + M.elements[j] = els; + } + } while (--ni); + return Matrix.fromArray(inverseElements); + }, + undefined, + this + ); + } + + /** + * Ported from Sylvester.js + * + * @returns {MatrixType} + */ + toRightTriangular() { + return block( + 'SAFE', + () => { + const M = this.clone(); + let els; + let fel; + let nel; + let n = this.elements.length; + const k = n; + let i; + let np; + const kp = this.elements[0].length; + let p; + do { + i = k - n; + fel = M.elements[i][i]; + if (fel.valueOf() === 0) { + for (let j = i + 1; j < k; j++) { + nel = M.elements[j][i]; + if (nel && nel.valueOf() !== 0) { + els = []; + np = kp; + do { + p = kp - np; + els.push(MatrixDeps._.add(M.elements[i][p].clone(), M.elements[j][p].clone())); + } while (--np); + M.elements[i] = els; + break; + } + } + } + fel = M.elements[i][i]; + if (fel.valueOf() !== 0) { + for (let j = i + 1; j < k; j++) { + const multiplier = MatrixDeps._.divide(M.elements[j][i].clone(), M.elements[i][i].clone()); + els = []; + np = kp; + do { + p = kp - np; + // Elements with column numbers up to an including the number + // of the row that we're subtracting can safely be set straight to + // zero, since that's the point of this routine and it avoids having + // to loop over and correct rounding errors later + els.push( + p <= i + ? new MatrixDeps.NerdamerSymbol(0) + : MatrixDeps._.subtract( + M.elements[j][p].clone(), + MatrixDeps._.multiply(M.elements[i][p].clone(), multiplier.clone()) + ) + ); + } while (--np); + M.elements[j] = els; + } + } + } while (--n); + + return M; + }, + undefined, + this + ); + } + + /** @returns {MatrixType} */ + transpose() { + const rows = this.elements.length; + const cols = this.elements[0].length; + const M = new Matrix(); + let ni = cols; + let i; + let nj; + let j; + + do { + i = cols - ni; + M.elements[i] = []; + nj = rows; + do { + j = rows - nj; + M.elements[i][j] = this.elements[j][i].clone(); + } while (--nj); + } while (--ni); + return M; + } + + /** + * Returns true if the matrix can multiply the argument from the left + * + * @param {MatrixType | unknown[]} matrix + * @returns {boolean} + */ + canMultiplyFromLeft(matrix) { + const l = isMatrix(matrix) ? matrix.elements.length : matrix.length; + // This.columns should equal matrix.rows + return this.elements[0].length === l; + } + + /** + * @param {MatrixType} matrix + * @returns {boolean} + */ + sameSize(matrix) { + return this.rows() === matrix.rows() && this.cols() === matrix.cols(); + } + + /** + * @param {MatrixType | unknown[][]} matrix + * @returns {MatrixType | null} + */ + multiply(matrix) { + return block( + 'SAFE', + () => { + const M = /** @type {MatrixType} */ (matrix).elements || /** @type {unknown[][]} */ (matrix); + if (!this.canMultiplyFromLeft(M)) { + const matrixTyped = /** @type {MatrixType} */ (matrix); + if (this.sameSize(matrixTyped)) { + const MM = new Matrix(); + const rows = this.rows(); + for (let i = 0; i < rows; i++) { + const e = MatrixDeps._.multiply( + new Vector(/** @type {NerdamerSymbolType[]} */ (this.elements[i])), + new Vector(/** @type {NerdamerSymbolType[]} */ (matrixTyped.elements[i])) + ); + MM.elements[i] = /** @type {VectorType} */ (e).elements; + } + return MM; + } + return null; + } + let ni = this.elements.length; + const ki = ni; + let i; + let nj; + const kj = M[0].length; + let j; + const cols = this.elements[0].length; + const elements = []; + let sum; + let nc; + let c; + do { + i = ki - ni; + elements[i] = []; + nj = kj; + do { + j = kj - nj; + sum = new MatrixDeps.NerdamerSymbol(0); + nc = cols; + do { + c = cols - nc; + sum = MatrixDeps._.add( + sum, + MatrixDeps._.multiply(this.elements[i][c], /** @type {NerdamerSymbolType} */ (M[c][j])) + ); + } while (--nc); + elements[i][j] = sum; + } while (--nj); + } while (--ni); + return Matrix.fromArray(elements); + }, + undefined, + this + ); + } + + /** + * @param {MatrixType} matrix + * @param {Function} [callback] + * @returns {MatrixType} + */ + add(matrix, callback) { + const M = new Matrix(); + if (this.sameSize(matrix)) { + this.eachElement((e, i, j) => { + let result = /** @type {NerdamerSymbolType} */ ( + MatrixDeps._.add(e.clone(), matrix.elements[i][j].clone()) + ); + if (callback) { + result = callback.call(M, result, e, matrix.elements[i][j]); + } + M.set(i, j, result); + }); + } + return M; + } + + /** + * @param {MatrixType} matrix + * @param {Function} [callback] + * @returns {MatrixType} + */ + subtract(matrix, callback) { + const M = new Matrix(); + if (this.sameSize(matrix)) { + this.eachElement((e, i, j) => { + let result = /** @type {NerdamerSymbolType} */ ( + MatrixDeps._.subtract(e.clone(), matrix.elements[i][j].clone()) + ); + if (callback) { + result = callback.call(M, result, e, matrix.elements[i][j]); + } + M.set(i, j, result); + }); + } + return M; + } + + /** @returns {this} */ + negate() { + this.each(e => e.negate()); + return this; + } + + /** @returns {VectorType | MatrixType} */ + toVector() { + if (this.rows() === 1 || this.cols() === 1) { + const v = new Vector(); + v.elements = /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ (this.elements.flat()); + return v; + } + return this; + } + + /** + * @param {string} [newline] + * @param {boolean} [toDecimal] + * @returns {string} + */ + toString(newline, toDecimal) { + const l = this.rows(); + const s = []; + newline = newline === undefined ? '\n' : newline; + for (let i = 0; i < l; i++) { + s.push( + `[${this.elements[i] + .map(x => { + const v = toDecimal ? x.multiplier.toDecimal() : x.toString(); + return x === undefined ? '' : v; + }) + .join(',')}]` + ); + } + return `matrix${inBrackets(s.join(','))}`; + } + + /** @returns {string} */ + text() { + return `matrix(${this.elements.map(row => `[${row.join(',')}]`)})`; + } + + /** + * @param {OutputType} [option] + * @returns {string} + */ + latex(option) { + const cols = this.cols(); + const { elements } = this; + return format('\\begin{vmatrix}{0}\\end{vmatrix}', () => { + const tex = []; + for (const row in elements) { + if (!Object.hasOwn(elements, row)) { + continue; + } + const rowTex = []; + for (let i = 0; i < cols; i++) { + rowTex.push(MatrixDeps.LaTeX.latex(elements[row][i], option)); + } + tex.push(rowTex.join(' & ')); + } + return tex.join(' \\cr '); + }); + } +} + +// Assign Matrix to CoreDeps immediately +CoreDeps.classes.Matrix = Matrix; + +// Build Object ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via BuildDeps which is set by the IIFE after initialization. + +/** + * Dependency container for Build object. Populated by the IIFE during initialization. Contains IIFE-local values and + * forward-referenced values. + * + * @type {{ + * _: ParserType; + * N: number; + * P: number; + * S: number; + * EX: number; + * FN: number; + * CB: number; + * Math2: Math2Interface; + * NerdamerSymbol: SymbolConstructor; + * }} + */ +const BuildDeps = { + get _() { + return CoreDeps.parser; + }, + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get Math2() { + return LateRefs.Math2; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, +}; + +/** Build object for compiling mathematical expressions to JavaScript functions. */ +const Build = { + /** @type {Record | Record>} */ + dependencies: {}, + /** + * @type {Record< + * string, + * ( + * symbol: NerdamerSymbolType, + * deps: [Record, string] + * ) => [string, [Record, string]] + * >} + */ + reformat: {}, + /** Initializes Build dependencies and reformat functions. Called once from IIFE after Math2 is available. */ + initDependencies() { + const { Math2 } = BuildDeps; + this.dependencies = { + _rename: { + 'Math2.factorial': 'factorial', + }, + factorial: { + 'Math2.gamma': Math2.gamma, + }, + gamma_incomplete: { + 'Math2.factorial': Math2.factorial, + }, + Li: { + 'Math2.Ei': Math2.Ei, + 'Math2.bigLog': Math2.bigLog, + Frac, + }, + Ci: { + 'Math2.factorial': Math2.factorial, + }, + Ei: { + 'Math2.factorial': Math2.factorial, + }, + Si: { + 'Math2.factorial': Math2.factorial, + }, + Shi: { + 'Math2.factorial': Math2.factorial, + }, + Chi: { + isInt, + nround, + 'Math2.num_integrate': Math2.num_integrate, + }, + factor: { + 'Math2.ifactor': Math2.ifactor, + NerdamerSymbol: BuildDeps.NerdamerSymbol, + }, + num_integrate: { + 'Math2.simpson': Math2.simpson, + nround, + }, + fib: { + even, + }, + }; + this.reformat = { + diff(symbol, deps) { + const v = symbol.args[1].toString(); + const f = `let f = ${Build.build(symbol.args[0].toString(), [v])};`; + let diffStr = Math2.diff.toString(); + if (!diffStr.startsWith('function') && !diffStr.startsWith('(') && !diffStr.startsWith('async')) { + diffStr = `function ${diffStr}`; + } + deps[1] += `let diff = ${diffStr};`; + deps[1] += f; + return [`diff(f)(${v})`, deps]; + }, + }; + }, + /** + * @param {string} f + * @returns {string} + */ + getProperName(f) { + const map = { + continuedFraction: 'continuedFraction', + }; + return map[f] || f; + }, + /** + * Assumes that dependencies are at max 2 levels + * + * @param {string} f + * @param {[Record, string]} [deps] + * @returns {[Record, string]} + */ + compileDependencies(f, deps) { + // Grab the predefined dependencies + const dependencies = Build.dependencies[f]; + + // The dependency string + let depString = deps && deps[1] ? deps[1] : ''; + + // The functions to be replaced + const replacements = deps && deps[0] ? deps[0] : {}; + + // Loop through them and add them to the list + for (const x in dependencies) { + if (typeof dependencies[x] === 'object') { + continue; + } // Skip object + const components = x.split('.'); // Math.f becomes f + // if the function isn't part of an object then reference the function itself + let depValue = dependencies[x]; + // If it's a function, convert method shorthand to function expression + if (typeof depValue === 'function') { + let fnStr = depValue.toString(); + // Handle ES6 method shorthand like "gamma(z) { ... }" -> "function gamma(z) { ... }" + if (!fnStr.startsWith('function') && !fnStr.startsWith('(') && !fnStr.startsWith('async')) { + fnStr = `function ${fnStr}`; + } + depValue = fnStr; + } + depString += `let ${components.length > 1 ? components[1] : components[0]}=${depValue};`; + replacements[x] = components.pop(); + } + + return [replacements, depString]; + }, + /** + * @param {NerdamerSymbolType} symbol + * @param {[Record, string]} [dependencies] + * @returns {[Record, string]} + */ + getArgsDeps(symbol, dependencies) { + const { args } = symbol; + let deps = dependencies; + const processFn = function (x) { + if (x.group === BuildDeps.FN) { + deps = Build.compileDependencies(x.fname, deps); + } + }; + for (let i = 0; i < args.length; i++) { + symbol.args[i].each(processFn); + } + return deps; + }, + /** + * @param {NerdamerSymbolType | string} symbol + * @param {string[]} [argArray] + * @returns {(...args: number[]) => number} + */ + build(symbol, argArray) { + // Module-scope values used directly: Math2, block, variables, inBrackets + // IIFE-local values from BuildDeps: + const { _, FN, N, S, P, EX, CB, NerdamerSymbol } = BuildDeps; + + symbol = block('PARSE2NUMBER', () => _.parse(symbol), true); + let args = variables(symbol); + const supplements = []; + /** @type {[Record, string]} */ + let dependencies = [{}, '']; + const ftext = function (sym, xports) { + // Fix for #545 - Parentheses confuse build. + if (sym.fname === '') { + sym = NerdamerSymbol.unwrapPARENS(sym); + } + xports ||= []; + const c = []; + const { group } = sym; + let prefix = ''; + + const ftextComplex = function (grp) { + const d = grp === CB ? '*' : '+'; + const cc = []; + + for (const x in sym.symbols) { + if (!Object.hasOwn(sym.symbols, x)) { + continue; + } + const s = sym.symbols[x]; + let ft = ftext(s, xports)[0]; + // Wrap it in brackets if it's group PL or CP + if (s.isComposite()) { + ft = inBrackets(ft); + } + cc.push(ft); + } + let retval = cc.join(d); + retval = retval && !sym.multiplier.equals(1) ? inBrackets(retval) : retval; + return retval; + }; + const ftextFunction = function (bn) { + let retval; + if (bn in Math) { + retval = `Math.${bn}`; + } else { + bn = Build.getProperName(bn); + if (supplements.indexOf(bn) === -1) { + // Make sure you're not adding the function twice + // Math2 functions aren't part of the standard javascript + // Math library and must be exported. + let fnStr = BuildDeps.Math2[bn].toString(); + // Handle ES6 method shorthand like "factorial(x) { ... }" -> "function factorial(x) { ... }" + if (!fnStr.startsWith('function') && !fnStr.startsWith('(') && !fnStr.startsWith('async')) { + fnStr = `function ${fnStr}`; + } + xports.push(`let ${bn} = ${fnStr}; `); + supplements.push(bn); + } + retval = bn; + } + retval += inBrackets(sym.args.map(x => ftext(x, xports)[0]).join(',')); + + return retval; + }; + + // The multiplier + if (group === N) { + c.push(sym.multiplier.toDecimal()); + } else if (sym.multiplier.equals(-1)) { + prefix = '-'; + } else if (!sym.multiplier.equals(1)) { + c.push(sym.multiplier.toDecimal()); + } + // The value + let value; + + if (group === S || group === P) { + value = sym.value; + } else if (group === FN) { + dependencies = Build.compileDependencies(sym.fname, dependencies); + dependencies = Build.getArgsDeps(sym, dependencies); + if (Build.reformat[sym.fname]) { + const components = Build.reformat[sym.fname](sym, dependencies); + dependencies = components[1]; + value = components[0]; + } else { + value = ftextFunction(sym.fname); + } + } else if (group === EX) { + const pg = sym.previousGroup; + if (pg === N || pg === S) { + value = sym.value; + } else if (pg === FN) { + value = ftextFunction(sym.fname); + dependencies = Build.compileDependencies(sym.fname, dependencies); + dependencies = Build.getArgsDeps(sym, dependencies); + } else { + value = ftextComplex(sym.previousGroup); + } + } else { + value = ftextComplex(sym.group); + } + + if (sym.group !== N && !sym.power.equals(1)) { + const pow = ftext(_.parse(sym.power)); + xports.push(pow[1]); + value = `Math.pow${inBrackets(`${value},${pow[0]}`)}`; + } + + if (value) { + c.push(prefix + value); + } + + return [c.join('*'), xports.join('').replace(/\n+\s+/gu, ' ')]; + }; + if (argArray) { + // Fix for issue #546 + // Disable argument checking since it's a bit presumptuous. + // Consider f(x) = 5; If I explicitely pass in an argument array contain x + // this check will fail and complain since the function doesn't contain x. + /* + for (let i = 0; i < args.length; i++) { + let arg = args[i]; + if (argArray.indexOf(arg) === -1) + err(arg + ' not found in argument array'); + } + */ + args = argArray; + } + + const fArray = ftext(symbol); + + // Make all the substitutions; + for (const x in dependencies[0]) { + if (!Object.hasOwn(dependencies[0], x)) { + continue; + } + const alias = dependencies[0][x]; + fArray[1] = fArray[1].replace(x, alias); + dependencies[1] = dependencies[1].replace(x, alias); + } + + const f = /** @type {(...args: number[]) => number} */ ( + // eslint-disable-next-line no-new-func + new Function(...args, `${(dependencies[1] || '') + fArray[1]} return ${fArray[0]};`) + ); + + return f; + }, +}; + +// Assign Build to CoreDeps immediately +CoreDeps.classes.Build = Build; + +// LaTeX Object ================================================================= +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via LaTeXDeps which is set by the IIFE after initialization. + +/** + * Dependency container for LaTeX object. Populated by the IIFE during initialization. Contains IIFE-local values and + * forward-referenced values. + * + * @type {{ + * _: ParserType; + * Settings: SettingsType; + * SQRT: string; + * ABS: string; + * PARENTHESIS: string; + * FACTORIAL: string; + * DOUBLEFACTORIAL: string; + * N: number; + * P: number; + * S: number; + * EX: number; + * FN: number; + * CB: number; + * CP: number; + * Parser: ParserConstructor | null; + * }} + */ +const LaTeXDeps = { + get _() { + return CoreDeps.parser; + }, + get Settings() { + return CoreDeps.settings; + }, + get SQRT() { + return CoreDeps.fnNames.SQRT; + }, + get ABS() { + return CoreDeps.fnNames.ABS; + }, + get PARENTHESIS() { + return CoreDeps.fnNames.PARENTHESIS; + }, + get FACTORIAL() { + return CoreDeps.fnNames.FACTORIAL; + }, + get DOUBLEFACTORIAL() { + return CoreDeps.fnNames.DOUBLEFACTORIAL; + }, + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get CP() { + return CoreDeps.groups.CP; + }, + get Parser() { + return CoreDeps.classes.Parser; + }, +}; + +/** LaTeX generator object for converting symbols to LaTeX notation. */ +const LaTeX = { + /** @type {ParserType | null} */ + parser: null, // Initialized inside IIFE after Parser is created + space: '~', + dot: ' \\cdot ', + + /** + * @param {NerdamerSymbolType | unknown[] | CollectionType} symbol + * @param {string} [option] + * @returns {string} + */ + latex(symbol, option) { + const { _: parser, P: GROUP_P, CB: GROUP_CB } = LaTeXDeps; + + // It might be an array + if (symbol && typeof symbol === 'object' && 'clone' in symbol && typeof symbol.clone === 'function') { + symbol = symbol.clone(); // Leave original as-is + } + if (symbol instanceof parser.classes.Collection) { + symbol = symbol.elements; + } + + if (isArray(symbol)) { + const LaTeXArray = []; + for (let i = 0; i < symbol.length; i++) { + let sym = symbol[i]; + // This way I can generate LaTeX on an array of strings. + if (!isSymbol(sym)) { + sym = parser.parse( + /** @type {string | number | NerdamerSymbolType | FracType | BigIntegerType} */ (sym) + ); + } + LaTeXArray.push(this.latex(/** @type {NerdamerSymbolType | Collection | unknown[]} */ (sym), option)); + } + return this.brackets(LaTeXArray.join(', '), 'square'); + } + if (isMatrix(symbol)) { + let TeX = '\\begin{pmatrix}\n'; + for (let i = 0; i < symbol.elements.length; i++) { + const rowTeX = []; + const e = symbol.elements[i]; + for (let j = 0; j < e.length; j++) { + rowTeX.push(this.latex(/** @type {NerdamerSymbolType | Collection | unknown[]} */ (e[j]), option)); + } + TeX += rowTeX.join(' & '); + if (i < symbol.elements.length - 1) { + TeX += '\\\\\n'; + } + } + TeX += '\\end{pmatrix}'; + return TeX; + } + if (isVector(symbol)) { + let TeX = '\\left['; + for (let i = 0; i < symbol.elements.length; i++) { + TeX += `${this.latex(symbol.elements[i], option)} ${i === symbol.elements.length - 1 ? '' : ',\\,'}`; + } + TeX += '\\right]'; + return TeX; + } + if (isSet(symbol)) { + let TeX = '\\{'; + for (let i = 0; i < symbol.elements.length; i++) { + TeX += `${this.latex(symbol.elements[i], option)} ${i === symbol.elements.length - 1 ? '' : ',\\,'}`; + } + TeX += '\\}'; + return TeX; + } + + symbol = symbol.clone(); + + const decimal = option === 'decimal' || option === 'decimals'; + const { power } = symbol; + const invert = isNegative(/** @type {NerdamerSymbolType | FracType} */ (power)); + const negative = symbol.multiplier.lessThan(0); + + if (symbol.group === GROUP_P && decimal) { + const base = Number(symbol.value); + const exp = Number(/** @type {{ toDecimal: () => string }} */ (symbol.power).toDecimal()); + const mult = Number(symbol.multiplier.toDecimal()); + return String(mult * base ** exp); + } + symbol.multiplier = symbol.multiplier.abs(); + + // If the user wants the result in decimal format then return it as such by placing it at the top part + let mArray; + + if (decimal) { + const m = String(symbol.multiplier.toDecimal()); + // If(String(m) === '1' && !decimal) m = ''; + mArray = [m, '']; + } else { + mArray = [symbol.multiplier.num, symbol.multiplier.den]; + } + // Get the value as a two part array + const vArray = this.value(symbol, invert, option, negative); + let p; + // Make it all positive since we know whether to push the power to the numerator or denominator already. + if (invert) { + power.negate(); + } + // The power is simple since it requires no additional formatting. We can get it to a + // string right away. pass in true to neglect unit powers + if (decimal) { + p = isSymbol(power) ? LaTeX.latex(power, option) : String(power.toDecimal()); + if (String(p) === '1') { + p = ''; + } + } + // Get the latex representation + else if (isSymbol(power)) { + p = this.latex(power, option); + } + // Get it as a fraction + else { + p = this.formatFrac(power, true); + } + // Use this array to specify if the power is getting attached to the top or the bottom + const pArray = ['', '']; + // Stick it to the top or the bottom. If it's negative then the power gets placed on the bottom + const index = invert ? 1 : 0; + pArray[index] = p; + + // Special case group P and decimal + const retval = (negative ? '-' : '') + this.set(mArray, vArray, pArray, symbol.group === GROUP_CB); + + return retval.replace(/\+-/giu, '-'); + }, + // Greek mapping + greek: { + alpha: '\\alpha', + beta: '\\beta', + gamma: '\\gamma', + delta: '\\delta', + epsilon: '\\epsilon', + zeta: '\\zeta', + eta: '\\eta', + theta: '\\theta', + iota: '\\iota', + kappa: '\\kappa', + lambda: '\\lambda', + mu: '\\mu', + nu: '\\nu', + xi: '\\xi', + omnikron: '\\omnikron', + pi: '\\pi', + rho: '\\rho', + sigma: '\\sigma', + tau: '\\tau', + upsilon: '\\upsilon', + phi: '\\phi', + chi: '\\chi', + psi: '\\psi', + omega: '\\omega', + Gamma: '\\Gamma', + Delta: '\\Delta', + Epsilon: '\\Epsilon', + Theta: '\\Theta', + Lambda: '\\Lambda', + Xi: '\\Xi', + Pi: '\\Pi', + Sigma: '\\Sigma', + Phi: '\\Phi', + Psi: '\\Psi', + Omega: '\\Omega', + }, + symbols: { + arccos: '\\arccos', + cos: '\\cos', + csc: '\\csc', + exp: '\\exp', + ker: '\\ker', + limsup: '\\limsup', + min: '\\min', + sinh: '\\sinh', + arcsin: '\\arcsin', + cosh: '\\cosh', + deg: '\\deg', + gcd: '\\gcd', + lg: '\\lg', + ln: '\\ln', + Pr: '\\Pr', + sqrt: '\\sqrt', + sup: '\\sup', + arctan: '\\arctan', + cot: '\\cot', + det: '\\det', + hom: '\\hom', + lim: '\\lim', + log: '\\log', + LN: '\\LN', + sec: '\\sec', + tan: '\\tan', + arg: '\\arg', + coth: '\\coth', + dim: '\\dim', + inf: '\\inf', + liminf: '\\liminf', + max: '\\max', + sin: '\\sin', + tanh: '\\tanh', + }, + /** + * Get the raw value of the symbol as an array + * + * @param {NerdamerSymbolType | unknown} symbol + * @param {boolean} inverted + * @param {string} [option] + * @param {boolean} [negative] + * @returns {string[]} + */ + value(symbol, inverted, option, negative) { + const { + SQRT, + ABS, + PARENTHESIS, + FACTORIAL, + DOUBLEFACTORIAL, + FN: GROUP_FN, + S: GROUP_S, + P: GROUP_P, + N: GROUP_N, + CB: GROUP_CB, + CP: GROUP_CP, + EX: GROUP_EX, + } = LaTeXDeps; + + const { group } = /** @type {NerdamerSymbolType} */ (symbol); + const { previousGroup } = /** @type {NerdamerSymbolType} */ (symbol); + const v = ['', '']; + const index = inverted ? 1 : 0; + /* If(group === N) // do nothing since we want to return top & bottom blank; */ + if (/** @type {NerdamerSymbolType} */ (symbol).isInfinity) { + v[index] = '\\infty'; + } else if ( + group === GROUP_S || + group === GROUP_P || + previousGroup === GROUP_S || + previousGroup === GROUP_P || + previousGroup === GROUP_N + ) { + let value = this.formatSubscripts(/** @type {NerdamerSymbolType} */ (symbol).value); + if (value.replace) { + value = value.replace(/(?.+)_$/u, '$1\\_'); + } + // Split it so we can check for instances of alpha as well as alpha_b + const tVarray = String(value).split('_'); + const greek = this.greek[tVarray[0]]; + if (greek) { + tVarray[0] = greek; + value = tVarray.join('_'); + } + const symbolEntry = this.symbols[tVarray[0]]; + if (symbolEntry) { + tVarray[0] = symbolEntry; + value = tVarray.join('_'); + } + v[index] = value; + } else if (group === GROUP_FN || previousGroup === GROUP_FN) { + const input = []; + const { fname } = /** @type {NerdamerSymbolType} */ (symbol); + // Collect the arguments + for (let i = 0; i < /** @type {NerdamerSymbolType} */ (symbol).args.length; i++) { + const arg = /** @type {NerdamerSymbolType} */ (symbol).args[i]; + let item; + if (typeof arg === 'string') { + item = arg; + } else { + item = this.latex(arg, option); + } + input.push(item); + } + + if (fname === SQRT) { + v[index] = `\\sqrt${this.braces(input.join(','))}`; + } else if (fname === ABS) { + v[index] = this.brackets(input.join(','), 'abs'); + } else if (fname === PARENTHESIS) { + v[index] = this.brackets(input.join(','), 'parens'); + } else if (fname === 'limit') { + v[index] = ` \\lim\\limits_{${input[1]} \\to ${input[2]}} ${input[0]}`; + } else if (fname === 'integrate') { + v[index] = `\\int${this.braces(input[0])}${this.braces(`d${input[1]}`)}`; + } else if (fname === 'defint') { + v[index] = `\\int\\limits_${this.braces(input[1])}^${this.braces(input[2])} ${input[0]} d${input[3]}`; + } else if (fname === FACTORIAL || fname === DOUBLEFACTORIAL) { + const arg = /** @type {NerdamerSymbolType} */ (symbol).args[0]; + if (arg.power.equals(1) && (arg.isComposite() || arg.isCombination())) { + input[0] = this.brackets(input[0]); + } + v[index] = input[0] + (fname === FACTORIAL ? '!' : '!!'); + } else if (fname === 'floor') { + v[index] = `\\left \\lfloor${this.braces(input[0])}\\right \\rfloor`; + } else if (fname === 'ceil') { + v[index] = `\\left \\lceil${this.braces(input[0])}\\right \\rceil`; + } + // Capture log(a, b) + else if (fname === LaTeXDeps.Settings.LOG && input.length > 1) { + v[index] = + `\\mathrm${this.braces(LaTeXDeps.Settings.LOG)}_${this.braces(input[1])}${this.brackets(input[0])}`; + } + // Capture log(a, b) + else if (fname === LaTeXDeps.Settings.LOG10) { + v[index] = + `\\mathrm${this.braces(LaTeXDeps.Settings.LOG)}_${this.braces('10')}${this.brackets(input[0])}`; + } else if (fname === LaTeXDeps.Settings.LOG2) { + v[index] = + `\\mathrm${this.braces(LaTeXDeps.Settings.LOG)}_${this.braces('2')}${this.brackets(input[0])}`; + } else if (fname === LaTeXDeps.Settings.LOG1P) { + v[index] = `\\ln${this.brackets(`1 + ${input[0]}`)}`; + } else if (fname === 'sum') { + const a = input[0]; + const b = input[1]; + const c = input[2]; + const d = input[3]; + v[index] = `\\sum\\limits_{${this.braces(b)}=${this.braces(c)}}^${this.braces(d)} ${this.braces(a)}`; + } else if (fname === 'product') { + const a = input[0]; + const b = input[1]; + const c = input[2]; + const d = input[3]; + v[index] = `\\prod\\limits_{${this.braces(b)}=${this.braces(c)}}^${this.braces(d)} ${this.braces(a)}`; + } else if (fname === 'nthroot') { + v[index] = `\\sqrt[${input[1]}]${this.braces(input[0])}`; + } else if (fname === 'mod') { + v[index] = `${input[0]} \\bmod ${input[1]}`; + } else if (fname === 'realpart') { + v[index] = `\\operatorname{Re}${this.brackets(input[0])}`; + } else if (fname === 'imagpart') { + v[index] = `\\operatorname{Im}${this.brackets(input[0])}`; + } else { + const name = fname === '' ? '' : `\\mathrm${this.braces(fname.replace(/_/gu, '\\_'))}`; + if (/** @type {NerdamerSymbolType} */ (symbol).isConversion) { + v[index] = name + this.brackets(input.join(''), 'parens'); + } else { + v[index] = name + this.brackets(input.join(','), 'parens'); + } + } + } else if (/** @type {NerdamerSymbolType} */ (symbol).isComposite()) { + const collected = /** @type {NerdamerSymbolType[]} */ ( + /** @type {NerdamerSymbolType} */ (symbol).collectSymbols() + ).sort( + group === GROUP_CP || previousGroup === GROUP_CP + ? (x, y) => y.group - x.group + : (x, y) => { + const px = isSymbol(x.power) ? -1 : Number(x.power); + const py = isSymbol(y.power) ? -1 : Number(y.power); + return py - px; + } + ); + const symbols = []; + const l = collected.length; + for (let i = 0; i < l; i++) { + symbols.push(LaTeX.latex(collected[i], option)); + } + const value = symbols.join('+'); + + const typedSymbol = /** @type {NerdamerSymbolType} */ (symbol); + v[index] = + !(typedSymbol.isLinear() && typedSymbol.multiplier.equals(1)) || negative + ? this.brackets(value, 'parens') + : value; + } else if (group === GROUP_CB || previousGroup === GROUP_EX || previousGroup === GROUP_CB) { + if (group === GROUP_CB) { + /** @type {NerdamerSymbolType} */ (symbol).distributeExponent(); + } + // This almost feels a little like cheating but I need to know if I should be wrapping the symbol + // in brackets or not. We'll do this by checking the value of the numerator and then comparing it + // to whether the symbol value is "simple" or not. + const denominator = []; + const numerator = []; + // Generate a profile + const denMap = []; + const numMap = []; + let numC = 0; + let denC = 0; + const setBrackets = function (container, map, counter) { + if (counter > 1 && map.length > 0) { + const l = map.length; + for (let idx = 0; idx < l; idx++) { + const mapIdx = map[idx]; + const containerItem = container[mapIdx]; + if ( + !( + /^\\left\(.+\\right\)\^\{.+\}$/gu.test(containerItem) || + /^\\left\(.+\\right\)$/gu.test(containerItem) + ) + ) { + container[mapIdx] = LaTeX.brackets(containerItem, 'parens'); + } + } + } + return container; + }; + + // Generate latex for each of them + /** @type {NerdamerSymbolType} */ (symbol).each(x => { + const isDenom = isNegative(x.power); + let laTex; + + if (isDenom) { + laTex = LaTeX.latex(x.invert(), option); + denC++; + if (x.isComposite()) { + if ( + !(/** @type {NerdamerSymbolType} */ (symbol).multiplier.den.equals(1)) && + Math.abs(Number(x.power)) === 1 + ) { + laTex = LaTeX.brackets(laTex, 'parens'); + } + denMap.push(denominator.length); // Make a note of where the composite was found + } + + denominator.push(laTex); + } else { + laTex = LaTeX.latex(x, option); + numC++; + if (x.isComposite()) { + if ( + !(/** @type {NerdamerSymbolType} */ (symbol).multiplier.num.equals(1)) && + Math.abs(Number(x.power)) === 1 + ) { + laTex = LaTeX.brackets(laTex, 'parens'); + } + numMap.push(numerator.length); // Make a note of where the composite was found + } + numerator.push(laTex); + } + }); + + // Apply brackets + setBrackets(numerator, numMap, numC); + v[0] = numerator.join(this.dot); // Collapse the numerator into one string + + setBrackets(denominator, denMap, denC); + v[1] = denominator.join(this.dot); + } + + return v; + }, + /** + * @param {unknown[]} m + * @param {string[]} v + * @param {string[]} p + * @param {boolean} combinePower + * @returns {string} + */ + set(m, v, p, combinePower) { + const isBracketed = function (str) { + return /^\\left\(.+\\right\)$/u.test(str); + }; + // Format the power if it exists + p &&= this.formatP(p); + // Group CB will have to be wrapped since the power applies to both it's numerator and denominator + let tp; + if (combinePower) { + // POSSIBLE BUG: If powers for group CB format wrong, investigate this since I might have overlooked something + // the assumption is that in every case the denonimator should be empty when dealing with CB. I can't think + // of a case where this isn't true + tp = p[0]; + p[0] = ''; // Temporarily make p blank + } + + // Merge v and p. Not that v MUST be first since the order matters + v = this.merge(v, p); + let mn = m[0]; + let md = m[1]; + const vn = v[0]; + const vd = v[1]; + // Filters + // if the top has a variable but the numerator is one drop it + if (vn && Number(mn) === 1) { + mn = ''; + } + // If denominator is 1 drop it always + if (Number(md) === 1) { + md = ''; + } + // Prepare the top portion but check that it's not already bracketed. If it is then leave out the cdot + const top = this.join( + /** @type {string} */ (mn), + /** @type {string} */ (vn), + isBracketed(/** @type {string} */ (vn)) ? '' : this.dot + ); + + // Prepare the bottom portion but check that it's not already bracketed. If it is then leave out the cdot + const bottom = this.join( + /** @type {string} */ (md), + /** @type {string} */ (vd), + isBracketed(/** @type {string} */ (vd)) ? '' : this.dot + ); + // Format the power if it exists + // make it a fraction if both top and bottom exists + if (top && bottom) { + let frac = this.frac(top, bottom); + if (combinePower && tp) { + frac = this.brackets(frac) + tp; + } + return frac; + } + // Otherwise only the top exists so return that + + return top; + }, + /** + * @param {string[]} a + * @param {string[]} b + * @returns {string[]} + */ + merge(a, b) { + const r = []; + for (let i = 0; i < 2; i++) { + r[i] = a[i] + b[i]; + } + return r; + }, + /** + * Joins together two strings if both exist + * + * @param {string} n + * @param {string} d + * @param {string} glue + * @returns {string} + */ + join(n, d, glue) { + if (!n && !d) { + return ''; + } + if (n && !d) { + return n; + } + if (d && !n) { + return d; + } + return n + glue + d; + }, + /** + * Places subscripts in braces for proper formatting + * + * @param {string} v + * @returns {string} + */ + formatSubscripts(v) { + // Split it at the underscore + const arr = v.toString().split('_'); + + let name = ''; + + // Loop over all entries except the first one + while (arr.length > 1) { + // Wrap all in braces except for the last one + if (arr.length > 0) { + name = `_${this.braces(arr.pop() + name)}`; + } + } + + return arr[0] + name; + }, + /** + * @param {string[]} pArray + * @returns {string[]} + */ + formatP(pArray) { + for (let i = 0; i < 2; i++) { + const p = pArray[i]; + if (p) { + pArray[i] = `^${this.braces(p)}`; + } + } + return pArray; + }, + /** + * Formats the fractions accordingly. + * + * @param {FracType} f + * @param {boolean} isPow + * @returns {string} + */ + formatFrac(f, isPow) { + const n = f.num.toString(); + const d = f.den.toString(); + // No need to have x^1 + if (isPow && n === '1' && d === '1') { + return ''; + } + // No need to have x/1 + if (d === '1') { + return n; + } + return this.frac(n, d); + }, + /** + * @param {string} n + * @param {string} d + * @returns {string} + */ + frac(n, d) { + return `\\frac${this.braces(n)}${this.braces(d)}`; + }, + /** + * @param {string} e + * @returns {string} + */ + braces(e) { + return `{${e}}`; + }, + /** + * @param {string} e + * @param {string} [typ] + * @returns {string} + */ + brackets(e, typ) { + typ ||= 'parens'; + const bracketTypes = { + parens: ['(', ')'], + square: ['[', ']'], + brace: ['{', '}'], + abs: ['|', '|'], + angle: ['\\langle', '\\rangle'], + }; + const bracket = bracketTypes[typ]; + return `\\left${bracket[0]}${e}\\right${bracket[1]}`; + }, + /** + * Removes extreneous tokens + * + * @param {LaTeXTokenType[]} tokens + * @returns {{ type: string; value: string }[] & { type?: string }} + */ + filterTokens(tokens) { + /** @type {{ type: string; value: string }[] & { type?: string }} */ + const filtered = /** @type {{ type: string; value: string }[] & { type?: string }} */ ([]); + + // Copy over the type of the scope + if (isArray(tokens)) { + filtered.type = /** @type {{ type?: string }} */ (tokens).type; + } + + // The items that need to be disposed + const d = ['\\', 'left', 'right', 'big', 'Big', 'large', 'Large']; + for (let i = 0, l = tokens.length; i < l; i++) { + const token = tokens[i]; + const nextToken = tokens[i + 1]; + if (token.value === '\\' && nextToken.value === '\\') { + filtered.push(token); + } else if (isArray(token)) { + filtered.push( + /** @type {{ type: string; value: string }} */ ( + /** @type {unknown} */ (LaTeX.filterTokens(/** @type {LaTeXTokenType[]} */ (token))) + ) + ); + } else if (d.indexOf(token.value) === -1) { + filtered.push(token); + } + } + return filtered; + }, + /** + * Parses tokens from LaTeX string. Does not do any error checking + * + * @param {unknown} rawTokens + * @returns {string} + */ + parse(rawTokens) { + const { SQRT } = LaTeXDeps; + + let i; + let l; + let retval = ''; + const tokens = this.filterTokens(/** @type {LaTeXTokenType[]} */ (/** @type {unknown} */ (rawTokens))); + const replace = { + cdot: '', + times: '', + infty: 'Infinity', + }; + // Get the next token + const next = function (n) { + return tokens[typeof n === 'undefined' ? ++i : (i += n)]; + }; + const parseNext = function () { + return LaTeX.parse(next()); + }; + const get = function (token) { + if (token in replace) { + return replace[token]; + } + // A quirk with implicit multiplication forces us to check for * + if (token === '*' && tokens[i + 1].value === '&') { + next(2); // Skip this and the & + return ','; + } + + if (token === '&') { + next(); + return ','; // Skip the * + } + // If it's the end of a row, return the row separator + if (token === '\\') { + return '],['; + } + return token; + }; + + // Start parsing the tokens + for (i = 0, l = tokens.length; i < l; i++) { + const token = tokens[i]; + // Fractions + if (token.value === 'frac') { + // Parse and wrap it in brackets + const n = parseNext(); + const d = parseNext(); + retval += `${n}/${d}`; + } else if (token.value in LaTeX.symbols) { + if (token.value === SQRT && tokens[i + 1].type === 'vector' && tokens[i + 2].type === 'NerdamerSet') { + const base = parseNext(); + const expr = parseNext(); + retval += `${expr}^${inBrackets(`1/${base}`)}`; + } else { + retval += token.value + parseNext(); + } + } else if (token.value === 'int') { + const f = parseNext(); + // Skip the comma + i++; + // Get the variable of integration + let dx = next().value; + dx = get(dx.substring(1, dx.length)); + retval += `integrate${inBrackets(`${f},${dx}`)}`; + } else if (token.value === 'int_') { + const lower = parseNext(); // Lower + i++; // Skip the ^ + let u = next().value; // Upper + // if it is in brackets + if (u === undefined) { + i--; + u = parseNext(); + } + const f = parseNext(); // Function + + // get the variable of integration + let dx = next().value; + // Skip the comma + if (dx === ',') { + dx = next().value; + } + // If 'd', skip + if (dx === 'differentialD') { + // Skip the * + i++; + dx = next().value; + } + if (dx === 'mathrm') { + // Skip the mathrm{d} + i++; + dx = next().value; + } + retval += `defint${inBrackets(`${f},${lower},${u},${dx}`)}`; + } else if (token.value && token.value.startsWith('int_')) { + // Var l = parseNext(); // lower + const intLower = token.value.replace('int_', ''); + i++; // Skip the ^ + let u = next().value; // Upper + // if it is in brackets + if (u === undefined) { + i--; + u = parseNext(); + } + const f = parseNext(); // Function + + // get the variable of integration + let dx = next().value; + // Skip the comma + if (dx === ',') { + dx = next().value; + } + // If 'd', skip + if (dx === 'differentialD') { + // Skip the * + i++; + dx = next().value; + } + if (dx === 'mathrm') { + // Skip the mathrm{d} + i++; + dx = next().value; + } + retval += `defint${inBrackets(`${f},${intLower},${u},${dx}`)}`; + } else if (token.value === 'mathrm') { + const f = tokens[++i][0].value; + retval += f + parseNext(); + } + // Sum and product + else if (token.value === 'sum_' || token.value === 'prod_') { + const fn = token.value === 'sum_' ? 'sum' : 'product'; + const nxt = next(); + i++; // Skip the caret + const end = parseNext(); + const f = parseNext(); + retval += fn + inBrackets([f, get(nxt[0]), get(nxt[2]), get(end)].join(',')); + } else if (token.value === 'lim_') { + const nxt = next(); + retval += `limit${inBrackets([parseNext(), get(nxt[0]), get(nxt[2])].join(','))}`; + } else if (token.value === 'begin') { + const nxt = next(); + if (Array.isArray(nxt)) { + const v = nxt[0].value; + if (v === 'matrix') { + // Start a matrix + retval += 'matrix(['; + } + } + } else if (token.value === 'end') { + const nxt = next(); + if (Array.isArray(nxt)) { + const v = nxt[0].value; + if (v === 'matrix') { + // End a matrix + retval += '])'; + } + } + } else if (Array.isArray(token)) { + retval += get(LaTeX.parse(token)); + } else { + retval += get(token.value.toString()); + } + } + + return inBrackets(retval); + }, + /** + * Initializes the LaTeX parser with custom operators for LaTeX parsing. Called once from the IIFE after Parser is + * available. + */ + initParser() { + const ParserClass = LaTeXDeps.Parser; + const keep = ['classes', 'setOperator', 'getOperators', 'getBrackets', 'tokenize', 'toRPN', 'tree', 'units']; + const parser = new ParserClass(); + for (const x in parser) { + if (keep.indexOf(x) === -1) { + delete parser[x]; + } + } + parser.setOperator({ + precedence: 8, + operator: '\\', + action: 'slash', + prefix: true, + postfix: false, + leftAssoc: true, + operation(e) { + return e; + }, + }); + parser.setOperator({ + precedence: 8, + operator: '\\,', + action: 'slash_comma', + prefix: true, + postfix: false, + leftAssoc: true, + operation(e) { + return e; + }, + }); + const brackets = parser.getBrackets(); + brackets['{'].maps_to = undefined; + this.parser = parser; + }, +}; + +// Assign LaTeX to CoreDeps immediately so VectorDeps/MatrixDeps getters work +CoreDeps.classes.LaTeX = LaTeX; + +// Settings Object ============================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via SettingsConstDeps which is set by the IIFE after initialization. + +/** + * Dependency container for Settings object constants. Populated by the IIFE during initialization. + * + * @type {{ + * LONG_PI: string; + * LONG_E: string; + * }} + */ +const SettingsConstDeps = { + get LONG_PI() { + return CoreDeps.ext.LONG_PI; + }, + get LONG_E() { + return CoreDeps.ext.LONG_E; + }, +}; + +/** Configuration settings for nerdamer. */ +const Settings = { + // Enables/Disables call peekers. False means callPeekers are disabled and true means callPeekers are enabled. + callPeekers: false, + + // The max number up to which to cache primes. Making this too high causes performance issues + init_primes: 1000, + + /** @type {string[]} */ + exclude: [], + // If you don't care about division by zero for example then this can be set to true. + // Has some nasty side effects so choose carefully. + suppress_errors: false, + // The global used to invoke the libary to parse to a number. Normally cos(9) for example returns + // cos(9) for convenience but parse to number will always try to return a number if set to true. + PARSE2NUMBER: false, + // This flag forces the a clone to be returned when add, subtract, etc... is called + SAFE: false, + // The symbol to use for imaginary symbols + IMAGINARY: 'i', + // The modules used to link numeric function holders + /** @type {(typeof Math | Record)[]} */ + FUNCTION_MODULES: [Math], + // Allow certain characters + ALLOW_CHARS: ['π'], + // Allow nerdamer to convert multi-character variables + USE_MULTICHARACTER_VARS: true, + // Allow changing of power operator + POWER_OPERATOR: '^', + // Function catch regex + FUNCTION_REGEX: /^\s*(?[a-z_][a-z0-9_]*)\((?[a-z0-9_,\s]*)\)\s*:?=\s*(?.+)\s*$/iu, + // The variable validation regex + // VALIDATION_REGEX: /^[a-z_][a-z\d\_]*$/i + VALIDATION_REGEX: + /^[a-z_αAβBγΓδΔϵEζZηHθΘιIκKλΛμMνNξΞoOπΠρPσΣτTυϒϕΦχXψΨωΩ∞][0-9a-z_αAβBγΓδΔϵEζZηHθΘιIκKλΛμMνNξΞoOπΠρPσΣτTυϒϕΦχXψΨωΩ]*$/iu, + // The regex used to determine which characters should be included in implied multiplication + IMPLIED_MULTIPLICATION_REGEX: + /(?[+\-/*]*[0-9]+)(?[a-z_αAβBγΓδΔϵEζZηHθΘιIκKλΛμMνNξΞoOπΠρPσΣτTυϒϕΦχXψΨωΩ]+[+\-/*]*)/giu, + // Aliases + ALIASES: { + π: 'pi', + '∞': 'Infinity', + }, + POSITIVE_MULTIPLIERS: false, + // Cached items + /** @type {{ roots?: Record }} */ + CACHE: {}, + // Print out warnings or not + SILENCE_WARNINGS: false, + // Precision + PRECISION: 21, + // The Expression defaults to this value for decimal places + EXPRESSION_DECP: 19, + // The text function defaults to this value for decimal places + DEFAULT_DECP: 16, + // Function mappings + VECTOR: 'vector', + PARENTHESIS: 'parens', + SQRT: 'sqrt', + ABS: 'abs', + FACTORIAL: 'factorial', + DOUBLEFACTORIAL: 'dfactorial', + // Reference pi and e - initialized via SettingsConstDeps inside IIFE + get LONG_PI() { + return SettingsConstDeps.LONG_PI; + }, + get LONG_E() { + return SettingsConstDeps.LONG_E; + }, + PI: Math.PI, + E: Math.E, + LOG: 'log', + LOG_LATEX: 'log', + LOG10: 'log10', + LOG10_LATEX: 'log_{10}', + LOG2: 'log2', + LOG2_LATEX: 'log_{2}', + LOG1P: 'log1p', + LOG1P_LATEX: 'ln\\left( 1 + {0} \\right)', + MAX_EXP: 200000, + // The number of scientific place to round to + SCIENTIFIC_MAX_DECIMAL_PLACES: 14, + // True if ints should not be converted to + SCIENTIFIC_IGNORE_ZERO_EXPONENTS: true, + // Exponent (absolute value) from which to switch from decimals to scientific in "decimals_or_scientific" mode + SCIENTIFIC_SWITCH_FROM_DECIMALS_MIN_EXPONENT: 7, + // No simplify() or solveFor() should take more ms than this + TIMEOUT: 800, + /** Initializes Settings.CACHE.roots with precomputed nth roots. Called once from IIFE during initialization. */ + initCache() { + this.CACHE.roots = {}; + const x = 40; + const y = 40; + for (let i = 2; i <= x; i++) { + for (let j = 2; j <= y; j++) { + const nthpow = nerdamerBigInt(i).pow(j); + this.CACHE.roots[`${nthpow}-${j}`] = i; + } + } + }, +}; + +// Set Settings.CONST_HASH at module scope (previously in IIFE) +Settings.CONST_HASH = CoreDeps.fnNames.CONST_HASH; + +// Initialize Settings.CACHE.roots at module scope +Settings.initCache(); + +// Populate LateRefs.Settings now that Settings is defined +LateRefs.Settings = Settings; + +// Math2 Object ================================================================== +// Extracted outside IIFE to enable proper TypeScript type inference. +// Dependencies are injected via Math2Deps which is set by the IIFE after initialization. + +/** + * Dependency container for Math2 object. Populated by the IIFE during initialization. + * + * Note: bigInt is typed as BigIntegerStaticType which doesn't expose constructor in TypeScript, but supports 'new' at + * runtime. Type assertions are used at call sites. + * + * @type {{ + * bigInt: BigIntegerStaticType; + * BIG_LOG_CACHE: string[]; + * PRIMES: number[]; + * NerdamerSymbol: SymbolConstructor; + * CB: number; + * P: number; + * }} + */ +const Math2Deps = { + get bigInt() { + return CoreDeps.ext.bigInt; + }, + get BIG_LOG_CACHE() { + return CoreDeps.ext.BIG_LOG_CACHE; + }, + get PRIMES() { + return CoreDeps.ext.PRIMES; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get P() { + return CoreDeps.groups.P; + }, +}; + +/** Math utility functions for nerdamer. */ +const Math2 = { + csc(x) { + return 1 / Math.sin(x); + }, + sec(x) { + return 1 / Math.cos(x); + }, + cot(x) { + return 1 / Math.tan(x); + }, + acsc(x) { + return Math.asin(1 / x); + }, + asec(x) { + return Math.acos(1 / x); + }, + acot(x) { + return Math.PI / 2 - Math.atan(x); + }, + // https://gist.github.com/jiggzson/df0e9ae8b3b06ff3d8dc2aa062853bd8 + erf(x) { + const t = 1 / (1 + 0.5 * Math.abs(x)); + const result = + 1 - + t * + Math.exp( + -x * x - + 1.26551223 + + t * + (1.00002368 + + t * + (0.37409196 + + t * + (0.09678418 + + t * + (-0.18628806 + + t * + (0.27886807 + + t * + (-1.13520398 + + t * + (1.48851587 + + t * + (-0.82215223 + + t * 0.17087277)))))))) + ); + return x >= 0 ? result : -result; + }, + diff(f) { + const h = 0.001; + + const derivative = function (x) { + return (f(x + h) - f(x - h)) / (2 * h); + }; + + return derivative; + }, + median(...values) { + values.sort((a, b) => a - b); + + const half = Math.floor(values.length / 2); + + if (values.length % 2) { + return values[half]; + } + + return (values[half - 1] + values[half]) / 2.0; + }, + /* + * Reverses continued fraction calculation + * @param {obj} contd + * @returns {number} + */ + fromContinued(contd) { + const arr = contd.fractions.slice(); + let e = 1 / arr.pop(); + for (let i = 0, l = arr.length; i < l; i++) { + e = 1 / (arr.pop() + e); + } + return contd.sign * (contd.whole + e); + }, + /* + * Calculates continued fractions + * @param {number} n + * @param {number} x The number of places + * @returns {number} + */ + continuedFraction(n, x) { + x ||= 20; + const sign = Math.sign(n); /* Store the sign*/ + const absn = Math.abs(n); /* Get the absolute value of the number*/ + const whole = Math.floor(absn); /* Get the whole*/ + let ni = absn - whole; /* Subtract the whole*/ + let c = 0; /* The counter to keep track of iterations*/ + let done = false; + const epsilon = 1e-14; + const max = 1e7; + let e; + let w; + const retval = { + whole, + sign, + fractions: [], + }; + /* Start calculating*/ + while (!done && ni !== 0) { + /* Invert and get the whole*/ + e = 1 / ni; + w = Math.floor(e); + if (w > max) { + /* This signals that we may have already gone too far*/ + const d = Math2.fromContinued(retval) - n; + if (d <= Number.EPSILON) { + break; + } + } + /* Add to result*/ + retval.fractions.push(w); + /* Move the ni to the decimal*/ + ni = e - w; + /* Ni should always be a decimal. If we have a whole number then we're in the rounding errors*/ + if (ni <= epsilon || c >= x - 1) { + done = true; + } + c++; + } + /* Cleanup 1/(n+1/1) = 1/(n+1) so just move the last digit one over if it's one*/ + let idx = retval.fractions.length - 1; + if (retval.fractions[idx] === 1) { + retval.fractions.pop(); + /* Increase the last one by one*/ + retval.fractions[--idx]++; + } + return retval; + }, + bigpow(n, p) { + if (!(n instanceof Frac)) { + n = Frac.create(n); + } + if (!(p instanceof Frac)) { + p = Frac.create(p); + } + const retval = new Frac(0); + if (p.isInteger()) { + retval.num = n.num.pow(p.toString()); + retval.den = n.den.pow(p.toString()); + } else { + const num = Frac.create(n.num ** p.num); + const den = Frac.create(n.den ** p.num); + + retval.num = Math2.nthroot(num, p.den.toString()); + retval.den = Math2.nthroot(den, p.den); + } + return retval; + }, + // http://stackoverflow.com/questions/15454183/how-to-make-a-function-that-computes-the-factorial-for-numbers-with-decimals + gamma(z) { + const g = 7; + const gammaCoeffs = [ + 0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, + 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7, + ]; + if (z < 0.5) { + return Math.PI / (Math.sin(Math.PI * z) * Math2.gamma(1 - z)); + } + z -= 1; + + let x = gammaCoeffs[0]; + for (let i = 1; i < g + 2; i++) { + x += gammaCoeffs[i] / (z + i); + } + + const t = z + g + 0.5; + return Math.sqrt(2 * Math.PI) * t ** (z + 0.5) * Math.exp(-t) * x; + }, + // Factorial + bigfactorial(x) { + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let retval = new Math2Deps.bigInt(1); + for (let i = 2; i <= x; i++) { + retval = retval.times(i); + } + return new Frac(retval); + }, + // https://en.wikipedia.org/wiki/Logarithm#Calculation + bigLog(x) { + const CACHE = Math2Deps.BIG_LOG_CACHE; + if (CACHE[x]) { + return Frac.quick.apply(null, CACHE[x].split('/')); + } + x = new Frac(x); + const n = 80; + let retval = new Frac(0); + const a = x.subtract(new Frac(1)); + const b = x.add(new Frac(1)); + for (let i = 0; i < n; i++) { + const t = new Frac(2 * i + 1); + const k = Math2.bigpow(a.divide(b), t); + const r = t.clone().invert().multiply(k); + retval = retval.add(r); + } + return retval.multiply(new Frac(2)); + }, + // The factorial function but using the big library instead + factorial(x) { + const isInteger = x % 1 === 0; + + /* Factorial for negative integers is complex infinity according to Wolfram Alpha*/ + if (isInteger && x < 0) { + return NaN; + } + + if (!isInteger) { + return Math2.gamma(x + 1); + } + + let retval = 1; + for (let i = 2; i <= x; i++) { + retval *= i; + } + return retval; + }, + // Double factorial + // http://mathworld.wolfram.com/DoubleFactorial.html + dfactorial(x) { + /* The return value*/ + /** @type {FracType | number} */ + let r = new Frac(1); + if (isInt(x)) { + const isEven = x % 2 === 0; + /* If x = isEven then n = x/2 else n = (x-1)/2*/ + const n = isEven ? x / 2 : (x + 1) / 2; + /* Start the loop*/ + if (isEven) { + for (let i = 1; i <= n; i++) { + r = /** @type {FracType} */ (r).multiply(new Frac(2).multiply(new Frac(i))); + } + } else { + for (let i = 1; i <= n; i++) { + r = /** @type {FracType} */ (r).multiply(new Frac(2).multiply(new Frac(i)).subtract(new Frac(1))); + } + } + } else { + /* Not yet extended to bigNum*/ + r = + 2 ** ((1 + 2 * x - Math.cos(Math.PI * x)) / 4) * + Math.PI ** ((Math.cos(Math.PI * x) - 1) / 4) * + Math2.gamma(1 + x / 2); + } + + /* Done*/ + return r; + }, + GCD(...rest) { + const args = arrayUnique(rest.map(x => Math.abs(x))).sort(); + let a = Math.abs(args.shift()); + let n = args.length; + + while (n-- > 0) { + let b = Math.abs(args.shift()); + while (true) { + a %= b; + if (a === 0) { + a = b; + break; + } + b %= a; + if (b === 0) { + break; + } + } + } + return a; + }, + QGCD(...args) { + let a = args[0]; + for (let i = 1; i < args.length; i++) { + const b = args[i]; + const sign = a.isNegative() && b.isNegative() ? -1 : 1; + a = b.gcd(a); + if (sign < 0) { + a.negate(); + } + } + return a; + }, + LCM(a, b) { + return (a * b) / Math2.GCD(a, b); + }, + // Pow but with the handling of negative numbers + // http://stackoverflow.com/questions/12810765/calculating-cubic-root-for-negative-number + pow(b, e) { + if (b < 0) { + if (Math.abs(e) < 1) { + /* Nth root of a negative number is imaginary when n is even*/ + if ((1 / e) % 2 === 0) { + return NaN; + } + return -(Math.abs(b) ** e); + } + } + return b ** e; + }, + factor(n) { + n = Number(n); + const sign = Math.sign(n); /* Store the sign*/ + /* move the number to absolute value*/ + n = Math.abs(n); + const ifactors = Math2.ifactor(n); + let factors = new Math2Deps.NerdamerSymbol(); + factors.symbols = {}; + factors.group = Math2Deps.CB; + for (const x in ifactors) { + if (!Object.hasOwn(ifactors, x)) { + continue; + } + const factor = new Math2Deps.NerdamerSymbol(1); + factor.group = Math2Deps.P; /* Cheat a little*/ + factor.value = x; + /** @type {NerdamerSymbolType} */ + const powerSym = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new Math2Deps.NerdamerSymbol(ifactors[x])) + ); + factor.power = powerSym; + factors.symbols[x] = factor; + } + factors.updateHash(); + + if (n === 1) { + factors = new Math2Deps.NerdamerSymbol(n); + } + + /* Put back the sign*/ + if (sign < 0) { + factors.negate(); + } + + return factors; + }, + /** + * Uses trial division + * + * @param {number} n - The number being factored + * @param {object} factors - The factors object + * @returns {object} + */ + sfactor(n, factors) { + factors ||= {}; + const r = Math.floor(Math.sqrt(n)); + const { PRIMES } = Math2Deps; + const lcprime = PRIMES[PRIMES.length - 1]; + /* A one-time cost... Hopefully ... And don't bother for more than a million*/ + /* takes too long*/ + if (r > lcprime && n < 1e6) { + generatePrimes(r); + } + const l = PRIMES.length; + for (let i = 0; i < l; i++) { + const prime = PRIMES[i]; + /* Trial division*/ + while (n % prime === 0) { + n /= prime; + factors[prime] = (factors[prime] || 0) + 1; + } + } + if (n > 1) { + factors[n] = 1; + } + return factors; + }, + /** + * Pollard's rho + * + * @param {number} num + * @returns {object} + */ + ifactor(num) { + const { bigInt } = Math2Deps; + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + const input = new bigInt(num); + // Convert to bigInt for safety + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let n = new bigInt(String(num)); + + if (n.equals(0)) { + return { 0: 1 }; + } + const sign = n.isNegative() ? -1 : 1; + n = n.abs(); + let factors = {}; /* Factor object being returned.*/ + if (n.lt('65536')) { + /* Less than 2^16 just use trial division*/ + factors = Math2.sfactor(n, factors); + } else { + const add = function (e) { + if (e.isPrime()) { + factors[e] = (factors[e] || 0) + 1; + } else { + factors = Math2.sfactor(e, factors); + } + }; + + try { + // NerdamerSet a safety + const max = 1e3; + const safetyCounter = { value: 0 }; + + const rho = function (c, currentN, safetyObj) { + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let xf = new bigInt(c); + let cz = 2; + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let x = new bigInt(c); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let factor = new bigInt(1); + + while (factor.equals(1)) { + for (let i = 0; i <= cz && factor.equals(1); i++) { + // Trigger the safety + if (safetyObj.value++ > max) { + throw new Error('stopping'); + } + + x = x.pow(2).add(1).mod(currentN); + factor = bigInt.gcd(x.minus(xf).abs(), currentN); + } + + cz *= 2; + xf = x; + } + if (factor.equals(currentN)) { + return rho(c + 1, currentN, safetyObj); + } + return factor; + }; + + while (!n.abs().equals(1)) { + if (n.isPrime()) { + add(n); + break; + } else { + const factor = rho(2, n, safetyCounter); + add(factor); + /* Divide out the factor*/ + n = n.divide(factor); + } + } + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + // Reset factors + factors = {}; + add(input); + } + } + + /* Put the sign back*/ + if (sign === -1) { + const sm = arrayMin(keys(factors).map(Number)); /*/ get the smallest number*/ + factors[`-${sm}`] = factors[sm]; + delete factors[sm]; + } + + return factors; + }, + // Factors a number into rectangular box. If sides are primes that this will be + // their prime factors. e.g. 21 -> (7)(3), 133 -> (7)(19) + /** + * @param {number} n + * @param {number} [max] + * @returns {[number, number] | [number, number, number]} + */ + boxfactor(n, max) { + max ||= 200; // Stop after this number of iterations + let c; + let r; + let d = Math.floor((5 / 12) * n); // The divisor + let i = 0; // Number of iterations + let safety = false; + while (true) { + c = Math.floor(n / d); + r = n % d; + if (r === 0) { + break; + } // We're done + if (safety) { + return /** @type {[number, number]} */ ([n, 1]); + } + d = Math.max(r, d - r); + i++; + safety = i > max; + } + return /** @type {[number, number, number]} */ ([c, d, i]); + }, + fib(n) { + let sign = Math.sign(n); + n = Math.abs(n); + sign = even(n) ? sign : Math.abs(sign); + let a = 0; + let b = 1; + let f = 1; + for (let i = 2; i <= n; i++) { + f = a + b; + a = b; + b = f; + } + return f * sign; + }, + mod(x, y) { + return x % y; + }, + // http://mathworld.wolfram.com/IntegerPart.html + integer_part(x) { + const sign = Math.sign(x); + return sign * Math.floor(Math.abs(x)); + }, + simpson(f, a, b, step) { + const getValue = function (fn, x, side) { + let v = fn(x); + const d = 0.000000000001; + if (isNaN(v)) { + v = fn(side === 1 ? x + d : x - d); + } + return v; + }; + + step ||= 0.0001; + // Calculate the number of intervals + let n = Math.abs(Math.floor((b - a) / step)); + // Simpson's rule requires an even number of intervals. If it's not then add 1 + if (n % 2 !== 0) { + n++; + } + // Get the interval size + const dx = (b - a) / n; + // Get x0 + let retval = getValue(f, a, 1); + + // Get the middle part 4x1+2x2+4x3 ... + // but first set a flag to see if it's even or odd. + // The first one is odd so we start there + let isEvenIteration = false; + // Get x1 + let xi = a + dx; + // The coefficient + let c; + let k; + // https://en.wikipedia.org/wiki/Simpson%27s_rule + for (let i = 1; i < n; i++) { + c = isEvenIteration ? 2 : 4; + k = c * getValue(f, xi, 1); + retval += k; + // Flip the even flag + isEvenIteration = !isEvenIteration; + // Increment xi + xi += dx; + } + + // Add xn + return (retval + getValue(f, xi, 2)) * (dx / 3); + }, + /** + * https://github.com/scijs/integrate-adaptive-simpson + * + * @param {Function} f - The function being integrated + * @param {number} a - Lower bound + * @param {number} b - Upper bound + * @param {number} tol - Step width + * @param {number} [maxdepth] + * @returns {number} + */ + num_integrate(f, a, b, tol, maxdepth) { + if (maxdepth < 0) { + throw new Error('max depth cannot be negative'); + } + + /* This algorithm adapted from pseudocode in:*/ + /* http://www.math.utk.edu/~ccollins/refs/Handouts/rich.pdf*/ + function adsimp(fn, lo, hi, fa, fm, fb, V0, tolerance, maxDepth, depth, state) { + if (state.nanEncountered) { + return NaN; + } + const h = hi - lo; + const f1 = fn(lo + h * 0.25); + const f2 = fn(hi - h * 0.25); + /* Simple check for NaN:*/ + if (isNaN(f1)) { + state.nanEncountered = true; + return undefined; + } + /* Simple check for NaN:*/ + if (isNaN(f2)) { + state.nanEncountered = true; + return undefined; + } + + const sl = (h * (fa + 4 * f1 + fm)) / 12; + const sr = (h * (fm + 4 * f2 + fb)) / 12; + const s2 = sl + sr; + const error = (s2 - V0) / 15; + + if (state.maxDepthCount > 1000 * maxDepth) { + return undefined; + } + + if (depth > maxDepth) { + state.maxDepthCount++; + return s2 + error; + } + if (Math.abs(error) < tolerance) { + return s2 + error; + } + const m = lo + h * 0.5; + const V1 = adsimp(fn, lo, m, fa, f1, fm, sl, tolerance * 0.5, maxDepth, depth + 1, state); + if (isNaN(V1)) { + state.nanEncountered = true; + return NaN; + } + const V2 = adsimp(fn, m, hi, fm, f2, fb, sr, tolerance * 0.5, maxDepth, depth + 1, state); + + if (isNaN(V2)) { + state.nanEncountered = true; + return NaN; + } + + return V1 + V2; + } + + function integrate(fn, lo, hi, tolerance, maxDepth) { + const state = { + maxDepthCount: 0, + nanEncountered: false, + }; + + if (tolerance === undefined) { + tolerance = 1e-9; + } + if (maxDepth === undefined) { + /* Issue #458 - This was lowered because of performance issues. */ + /* This was suspected from before but is now confirmed with this issue*/ + maxDepth = 45; + } + + const fa = fn(lo); + const fm = fn(0.5 * (lo + hi)); + const fb = fn(hi); + + const V0 = ((fa + 4 * fm + fb) * (hi - lo)) / 6; + + const result = adsimp(fn, lo, hi, fa, fm, fb, V0, tolerance, maxDepth, 1, state); + + if (state.maxDepthCount > 0) { + warn( + `integrate-adaptive-simpson: Warning: maximum recursion depth (${maxDepth}) reached ${ + state.maxDepthCount + } times` + ); + } + + if (state.nanEncountered) { + throw new Error('Function does not converge over interval!'); + } + + return result; + } + /** @type {number} */ + let retval; + + try { + retval = integrate(f, a, b, tol, maxdepth); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + /* Fallback to non-adaptive*/ + return Math2.simpson(f, a, b); + } + return /** @type {number} */ (nround(retval, 12)); + }, + // https://en.wikipedia.org/wiki/Trigonometric_integral + // CosineIntegral + Ci(x) { + const n = 20; + /* Roughly Euler–Mascheroni*/ + const g = 0.5772156649015329; + let sum = 0; + for (let i = 1; i < n; i++) { + /* Cache 2n*/ + const n2 = 2 * i; + sum += ((-1) ** i * x ** n2) / (n2 * Math2.factorial(n2)); + } + return Math.log(x) + g + sum; + }, + /* SineIntegral*/ + Si(x) { + const n = 20; + let sum = 0; + for (let i = 0; i < n; i++) { + const n2 = 2 * i; + sum += ((-1) ** i * x ** (n2 + 1)) / ((n2 + 1) * Math2.factorial(n2 + 1)); + } + return sum; + }, + /* ExponentialIntegral*/ + Ei(x) { + if (Number(x) === 0) { + return -Infinity; + } + const n = 30; + const g = 0.5772156649015329; /* Roughly Euler–Mascheroni*/ + let sum = 0; + for (let i = 1; i < n; i++) { + sum += x ** i / (i * Math2.factorial(i)); + } + return g + Math.abs(Math.log(x)) + sum; + }, + /* Hyperbolic Sine Integral*/ + /* http://mathworld.wolfram.com/Shi.html*/ + Shi(x) { + const n = 30; + let sum = 0; + let k; + let t; + for (let i = 0; i < n; i++) { + k = 2 * i; + t = k + 1; + sum += x ** t / (t * t * Math2.factorial(k)); + } + return sum; + }, + /* The cosine integral function*/ + Chi(x) { + const dx = 0.001; + const g = 0.5772156649015329; + const f = function (t) { + return (Math.cosh(t) - 1) / t; + }; + return ( + Math.log(/** @type {number} */ (x)) + + g + + /** @type {number} */ (Math2.num_integrate(f, 0.002, /** @type {number} */ (x), dx)) + ); + }, + /* The log integral*/ + Li(x) { + return Math2.Ei(Math2.bigLog(x)); + }, + /* The gamma incomplete function*/ + gamma_incomplete(n, xVal) { + const t = n - 1; + let sum = 0; + const x = xVal || 0; + for (let i = 0; i < t; i++) { + sum += x ** i / Math2.factorial(i); + } + return Math2.factorial(t) * Math.exp(-x) * sum; + }, + /* + * Heaviside step function - Moved from Special.js (originally contributed by Brosnan Yuen) + * Specification : http://mathworld.wolfram.com/HeavisideStepFunction.html + * if x > 0 then 1 + * if x == 0 then 1/2 + * if x < 0 then 0 + */ + step(x) { + if (x > 0) { + return 1; + } + if (x < 0) { + return 0; + } + return 0.5; + }, + /* + * Rectangle function - Moved from Special.js (originally contributed by Brosnan Yuen) + * Specification : http://mathworld.wolfram.com/RectangleFunction.html + * if |x| > 1/2 then 0 + * if |x| == 1/2 then 1/2 + * if |x| < 1/2 then 1 + */ + rect(x) { + const absX = Math.abs(x); + if (absX === 0.5) { + return absX; + } + if (absX > 0.5) { + return 0; + } + return 1; + }, + /* + * Sinc function - Moved from Special.js (originally contributed by Brosnan Yuen) + * Specification : http://mathworld.wolfram.com/SincFunction.html + * if x == 0 then 1 + * otherwise sin(x)/x + */ + sinc(x) { + if (x.equals(0)) { + return 1; + } + return Math.sin(x) / x; + }, + /* + * Triangle function - Moved from Special.js (originally contributed by Brosnan Yuen) + * Specification : http://mathworld.wolfram.com/TriangleFunction.html + * if |x| >= 1 then 0 + * if |x| < then 1-|x| + */ + tri(x) { + x = Math.abs(x); + if (x >= 1) { + return 0; + } + return 1 - x; + }, + // https://en.wikipedia.org/wiki/Nth_root_algorithm + nthroot(A, n) { + /* Make sure the input is of type Frac*/ + if (!(A instanceof Frac)) { + A = new Frac(A.toString()); + } + if (!(n instanceof Frac)) { + n = new Frac(n.toString()); + } + if (n.equals(1)) { + return A; + } + /* Begin algorithm*/ + let xk = A.divide(new Frac(2)); /* X0*/ + const e = new Frac(1e-15); + let dk; + let dk0; + let d0; + const a = n.clone().invert(); + const b = n.subtract(new Frac(1)); + do { + const powb = Math2.bigpow(xk, b); + let dkDec = a.multiply(A.divide(powb).subtract(xk)).toDecimal(25); + dk = Frac.create(dkDec); + if (d0) { + break; + } + + xk = xk.add(dk); + /* Check to see if there's no change from the last xk*/ + dkDec = dk.toDecimal(); + d0 = dk0 ? dk0 === dkDec : false; + dk0 = dkDec; + } while (dk.abs().gte(e)); + + return xk; + }, + /* https://gist.github.com/jiggzson/0c5b33cbcd7b52b36132b1e96573285f*/ + /* Just the square root function but big :)*/ + sqrt(n) { + if (!(n instanceof Frac)) { + n = new Frac(n); + } + let xn; + let d; + let ld; + let sameDelta; + let c = 0; /* Counter*/ + let done = false; + const delta = new Frac(1e-20); + xn = n.divide(new Frac(2)); + const safety = 1000; + do { + /* Break if we're not converging*/ + if (c > safety) { + throw new Error(`Unable to calculate square root for ${n}`); + } + xn = xn.add(n.divide(xn)).divide(new Frac(2)); + xn = new Frac(xn.decimal(30)); + /* Get the difference from the true square*/ + d = n.subtract(xn.multiply(xn)); + /* If the square of the calculated number is close enough to the number*/ + /* we're getting the square root or the last delta was the same as the new delta*/ + /* then we're done*/ + sameDelta = ld ? ld.equals(d) : false; + if (d.clone().abs().lessThan(delta) || sameDelta) { + done = true; + } + /* Store the calculated delta*/ + ld = d; + c++; /* Increase the counter*/ + } while (!done); + + return xn; + }, +}; + +// Register Math2 in Settings.FUNCTION_MODULES (before Parser instantiation) +Settings.FUNCTION_MODULES.push(Math2); +reserveNames(/** @type {object} */ (Math2)); + +// Populate LateRefs.Math2 now that Math2 is defined +LateRefs.Math2 = Math2; + +// IsSymbol Function ============================================================== +/** + * Checks to see if the object provided is a NerdamerSymbol + * + * @param {unknown} obj + * @returns {obj is NerdamerSymbolType} + */ +function isSymbol(obj) { + return obj instanceof CoreDeps.classes.NerdamerSymbol; +} + +// IsVector Function ============================================================== +/** + * Checks to see if the object provided is a Vector + * + * @param {object} obj + * @returns {obj is VectorType} + */ +function isVector(obj) { + return obj instanceof Vector; +} + +// IsMatrix Function ============================================================== +/** + * Checks to see if the object provided is a Matrix + * + * @param {object} obj + * @returns {obj is MatrixType} + */ +function isMatrix(obj) { + return obj instanceof Matrix; +} + +// IsExpression Function =========================================================== +/** + * Checks to see if the object provided is an Expression + * + * @param {object} obj + * @returns {obj is ExpressionType} + */ +function isExpression(obj) { + return obj instanceof Expression; +} + +// Variables Function ============================================================== +/** + * Dependency container for variables function. Initialized inside the IIFE. + * + * @type {{ + * EX: number; + * CP: number; + * CB: number; + * S: number; + * PL: number; + * FN: number; + * }} + */ +const VariablesDeps = { + get EX() { + return CoreDeps.groups.EX; + }, + get CP() { + return CoreDeps.groups.CP; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get S() { + return CoreDeps.groups.S; + }, + get PL() { + return CoreDeps.groups.PL; + }, + get FN() { + return CoreDeps.groups.FN; + }, +}; + +/** + * This method traverses the symbol structure and grabs all the variables in a symbol. The variable names are then + * returned in alphabetical order. + * + * @param {NerdamerSymbolType | FracType} obj + * @param {boolean} poly + * @param {object} vars - An object containing the variables. Do not pass this in as it generated automatically. In the + * future this will be a Collector object. + * @returns {string[]} - An array containing variable names + */ +function variables(obj, poly = null, vars = null) { + vars ||= { + c: [], + add(value) { + if (this.c.indexOf(value) === -1 && isNaN(value)) { + this.c.push(value); + } + }, + }; + + if (isSymbol(obj)) { + const { group } = obj; + const prevgroup = obj.previousGroup; + if (group === VariablesDeps.EX) { + variables(obj.power, poly, vars); + } + + if ( + group === VariablesDeps.CP || + group === VariablesDeps.CB || + prevgroup === VariablesDeps.CP || + prevgroup === VariablesDeps.CB + ) { + for (const x in obj.symbols) { + if (!Object.hasOwn(obj.symbols, x)) { + continue; + } + variables(obj.symbols[x], poly, vars); + } + } else if (group === VariablesDeps.S || prevgroup === VariablesDeps.S) { + // Very crude needs fixing. TODO + if (!(obj.value === 'e' || obj.value === 'pi' || obj.value === Settings.IMAGINARY)) { + vars.add(obj.value); + } + } else if (group === VariablesDeps.PL || prevgroup === VariablesDeps.PL) { + variables(/** @type {NerdamerSymbolType | FracType} */ (firstObject(obj.symbols)), poly, vars); + } else if (group === VariablesDeps.EX) { + if (!isNaN(Number(obj.value))) { + vars.add(obj.value); + } + variables(obj.power, poly, vars); + } else if (group === VariablesDeps.FN && !poly && obj.args) { + for (let i = 0; i < obj.args.length; i++) { + variables(obj.args[i], poly, vars); + } + } + } + + return vars.c.sort(); +} + +// GetCoeffs Function ============================================================== +// Uses ParserDeps._ for parser access. + +/** + * Returns the coefficients of a symbol given a variable. Given ax^2+b^x+c, it divides each nth term by x^n. + * + * @param {NerdamerSymbolType} symbol + * @param {NerdamerSymbolType} wrt + */ +function getCoeffs(symbol, wrt, _info) { + const coeffs = []; + // We loop through the symbols and stick them in their respective + // containers e.g. y*x^2 goes to index 2 + symbol.each(term => { + let coeff; + let p; + if (term.contains(wrt)) { + // We want only the coefficient which in this case will be everything but the variable + // e.g. a*b*x -> a*b if the variable to solve for is x + coeff = term.stripVar(wrt); + const x = /** @type {NerdamerSymbolType} */ (ParserDeps._.divide(term.clone(), coeff.clone())); + p = /** @type {FracType} */ (x.power).toDecimal(); + } else { + coeff = term; + p = 0; + } + const e = coeffs[p]; + // If it exists just add it to it + coeffs[p] = e ? ParserDeps._.add(e, coeff) : coeff; + }, true); + + for (let i = 0; i < coeffs.length; i++) { + coeffs[i] ||= new CoreDeps.classes.NerdamerSymbol(0); + } + // Fill the holes + return coeffs; +} + +// Nroots Function ================================================================= +/** + * Dependency container for nroots function. + * + * @type {{ + * _: ParserType; + * FN: number; + * P: number; + * N: number; + * NerdamerSymbol: SymbolConstructor; + * }} + */ +const NrootsDeps = { + get _() { + return CoreDeps.parser; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get P() { + return CoreDeps.groups.P; + }, + get N() { + return CoreDeps.groups.N; + }, + get NerdamerSymbol() { + return CoreDeps.classes.NerdamerSymbol; + }, +}; + +/** + * Gets nth roots of a number + * + * @param {NerdamerSymbolType} symbol + * @returns {VectorType} + */ +function nroots(symbol) { + let a; + let b; + let _roots; + + if (symbol.group === NrootsDeps.FN && symbol.fname === '') { + a = NrootsDeps.NerdamerSymbol.unwrapPARENS(NrootsDeps._.parse(symbol).toLinear()); + b = NrootsDeps._.parse(symbol.power); + } else if (symbol.group === NrootsDeps.P) { + a = NrootsDeps._.parse(symbol.value); + b = NrootsDeps._.parse(symbol.power); + } + + if (a && b && a.group === NrootsDeps.N && b.group === NrootsDeps.N && a.multiplier.isNegative()) { + _roots = []; + + const parts = NrootsDeps.NerdamerSymbol.toPolarFormArray(evaluate(symbol)); + const r = parts[0]; + + // Var r = _.parse(a).abs().toString(); + + // https://en.wikipedia.org/wiki/De_Moivre%27s_formula + const x = NrootsDeps._.arg(a); + const n = b.multiplier.den.toString(); + const p = b.multiplier.num.toString(); + + const formula = '(({0})^({1})*(cos({3})+({2})*sin({3})))^({4})'; + + for (let i = 0; i < Number(n); i++) { + const t = evaluate(NrootsDeps._.parse(format('(({0})+2*pi*({1}))/({2})', x, i, n))).multiplier.toDecimal(); + _roots.push(evaluate(NrootsDeps._.parse(format(formula, r, n, Settings.IMAGINARY, t, p)))); + } + return Vector.fromArray(_roots); + } + if (symbol.isConstant(true, true)) { + const sign = symbol.sign(); + const x = evaluate(symbol.abs()); + const root = NrootsDeps._.sqrt(x); + + _roots = [root.clone(), root.negate()]; + + if (sign < 0) { + _roots = /** @type {NerdamerSymbolType[]} */ ( + _roots.map(r => NrootsDeps._.multiply(r, NrootsDeps.NerdamerSymbol.imaginary())) + ); + } + } else { + _roots = [/** @type {NerdamerSymbolType} */ (NrootsDeps._.parse(symbol))]; + } + + return Vector.fromArray(_roots); +} + +// Compare Function ================================================================ +// Uses ParserDeps._ for parser access. + +/** + * Compares two symbols by evaluating them with random values for variables. This is useful for checking if two + * different representations are mathematically equivalent. + * + * @param {NerdamerSymbolType} sym1 + * @param {NerdamerSymbolType} sym2 + * @param {string[]} vars - An optional array of variables to use + * @returns {boolean} + */ +function compare(sym1, sym2, vars) { + const n = 5; // A random number between 1 and 5 is good enough + /** @type {Record} */ + const scope = {}; // Scope object with random numbers generated using vars + let comparison; + for (let i = 0; i < vars.length; i++) { + scope[vars[i]] = /** @type {NerdamerSymbolType} */ ( + new CoreDeps.classes.NerdamerSymbol(Math.floor(Math.random() * n) + 1) + ); + } + block('PARSE2NUMBER', () => { + comparison = ParserDeps._.parse(sym1, scope).equals(ParserDeps._.parse(sym2, scope)); + }); + return comparison; +} + +// IsFraction Function ============================================================= +/** + * Checks to see if a number or NerdamerSymbol is a fraction + * + * @param {number | string | NerdamerSymbolType} num + * @returns {boolean} + */ +function isFraction(num) { + if (isSymbol(num)) { + return isFraction(/** @type {NerdamerSymbolType} */ (num).multiplier.toDecimal()); + } + return Number(num) % 1 !== 0; +} + +// ArraySum Function =============================================================== +// Uses ParserDeps._ for parser access. + +/** + * Returns the sum of an array + * + * @param {Array} arr + * @param {boolean} toNumber + * @returns {NerdamerSymbolType | number} + */ +function arraySum(arr, toNumber) { + /** @type {NerdamerSymbolType} */ + let sum = /** @type {NerdamerSymbolType} */ (new CoreDeps.classes.NerdamerSymbol(0)); + for (let i = 0; i < arr.length; i++) { + const x = arr[i]; + // Convert to symbol if not + sum = /** @type {NerdamerSymbolType} */ (ParserDeps._.add(sum, isSymbol(x) ? x : ParserDeps._.parse(x))); + } + + return toNumber ? Number(sum) : sum; +} + +// AllConstants Function =========================================================== +/** + * Checks if all arguments aren't just all numbers but if they are constants as well e.g. pi, e. + * + * @param {object} args + * @returns {boolean} + */ +function allConstants(args) { + for (let i = 0; i < args.length; i++) { + if (args[i].isPi() || args[i].isE()) { + continue; + } + if (!args[i].isConstant(true)) { + return false; + } + } + return true; +} + +// FillHoles Function ============================================================== +/** + * Fills holes in an array with zero symbol or generates one with n zeroes + * + * @param {Array} arr + * @param {number} n + */ +function fillHoles(arr, n) { + n ||= arr.length; + for (let i = 0; i < n; i++) { + const sym = arr[i]; + if (!sym) { + arr[i] = new CoreDeps.classes.NerdamerSymbol(0); + } + } + return arr; +} + +// IsNegative Function ============================================================= +/** + * @param {number | NerdamerSymbolType | FracType} obj + * @returns {boolean} + */ +function isNegative(obj) { + if (isSymbol(obj)) { + return obj.multiplier.lessThan(0); + } + if (typeof obj === 'object' && 'lessThan' in obj) { + return /** @type {FracType} */ (obj).lessThan(0); + } + return /** @type {number} */ (obj) < 0; +} + +// Separate Function =============================================================== +/** + * Dependency container for separate function. + * + * @type {{ + * _: ParserType; + * S: number; + * FN: number; + * EX: number; + * ABS: string; + * }} + */ +const SeparateDeps = { + get _() { + return CoreDeps.parser; + }, + get S() { + return CoreDeps.groups.S; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get ABS() { + return CoreDeps.fnNames.ABS; + }, +}; + +/** + * Separates out the variables into terms of variables. e.g. x+y+x_y+sqrt(2)+pi returns {x: x, y: y, x y: x_y, + * constants: sqrt(2)+pi + * + * @param {NerdamerSymbolType} symbol + * @param {Record} [o] + * @returns {Record} + * @throws {Error} For exponentials + */ +function separate(symbol, o) { + symbol = /** @type {NerdamerSymbolType} */ (SeparateDeps._.expand(symbol)); + o ||= {}; + const insert = function (key, sym) { + o[key] ||= new CoreDeps.classes.NerdamerSymbol(0); + o[key] = /** @type {NerdamerSymbolType} */ (SeparateDeps._.add(o[key], sym.clone())); + }; + symbol.each(x => { + if (x.isConstant('all')) { + insert('constants', x); + } else if (x.group === SeparateDeps.S) { + insert(x.value, x); + } else if (x.group === SeparateDeps.FN && (x.fname === SeparateDeps.ABS || x.fname === '')) { + separate(x.args[0]); + } else if (x.group === SeparateDeps.EX || x.group === SeparateDeps.FN) { + // Todo: gm: this occurs with sqrt(a+1) + // Do nothing - skip EX and FN groups + } else { + insert(variables(x).join(' '), x); + } + }); + + return o; +} + +// DecomposeFn Function ============================================================ +/** + * Dependency container for decomposeFn function. + * + * @type {{ + * _: ParserType; + * CP: number; + * }} + */ +const DecomposeFnDeps = { + get _() { + return CoreDeps.parser; + }, + get CP() { + return CoreDeps.groups.CP; + }, +}; + +/** + * Breaks a function down into its parts wrt to a variable, mainly coefficients. Example: a*x^2+b wrt x + * + * @overload + * @param {NerdamerSymbolType} fn + * @param {string} wrt + * @param {true} asObj + * @returns {{ a: NerdamerSymbolType; x: NerdamerSymbolType; ax: NerdamerSymbolType; b: NerdamerSymbolType }} + */ +/** + * @overload + * @param {NerdamerSymbolType} fn + * @param {string} wrt + * @param {false} [asObj] + * @returns {NerdamerSymbolType[]} + */ +/** + * @param {NerdamerSymbolType} fn + * @param {string} wrt + * @param {boolean} [asObj] + */ +function decomposeFn(fn, wrt, asObj) { + wrt = String(wrt); // Convert to string + let ax; + let b; + if (fn.group === DecomposeFnDeps.CP) { + const t = /** @type {NerdamerSymbolType} */ (DecomposeFnDeps._.expand(fn.clone())).stripVar(wrt); + ax = DecomposeFnDeps._.subtract(fn.clone(), t.clone()); + b = t; + } else { + ax = fn.clone(); + } + const a = /** @type {NerdamerSymbolType} */ (ax).stripVar(wrt); + const x = DecomposeFnDeps._.divide(/** @type {NerdamerSymbolType} */ (ax).clone(), a.clone()); + b ||= new CoreDeps.classes.NerdamerSymbol(0); + if (asObj) { + return { + a, + x, + ax, + b, + }; + } + return [a, x, ax, b]; +} + +// Mix Function ==================================================================== +// Uses ParserDeps._ for parser access. + +/** + * Used to multiply two expressions in expanded form + * + * @param {NerdamerSymbolType} a + * @param {NerdamerSymbolType} b + */ +function mix(a, b, opt) { + // Flip them if b is a CP or PL and a is not + if ((b.isComposite() && !a.isComposite()) || (b.isLinear() && !a.isLinear())) { + [a, b] = [b, a]; + } + // A temporary variable to hold the expanded terms + let t = new CoreDeps.classes.NerdamerSymbol(0); + if (a.isLinear()) { + a.each(x => { + // If b is not a PL or a CP then simply multiply it + if (!b.isComposite()) { + const term = /** @type {NerdamerSymbolType} */ ( + ParserDeps._.multiply(ParserDeps._.parse(x), ParserDeps._.parse(b)) + ); + t = /** @type {NerdamerSymbolType} */ (ParserDeps._.add(t, ParserDeps._.expand(term, opt))); + } + // Otherwise multiply out each term. + else if (b.isLinear()) { + b.each(y => { + const term = /** @type {NerdamerSymbolType} */ ( + ParserDeps._.multiply(ParserDeps._.parse(x), ParserDeps._.parse(y)) + ); + const expanded = /** @type {NerdamerSymbolType} */ ( + ParserDeps._.expand(/** @type {NerdamerSymbolType} */ (ParserDeps._.parse(term)), opt) + ); + t = /** @type {NerdamerSymbolType} */ (ParserDeps._.add(t, expanded)); + }, true); + } else { + t = /** @type {NerdamerSymbolType} */ ( + ParserDeps._.add(t, ParserDeps._.multiply(x, ParserDeps._.parse(b))) + ); + } + }, true); + } else { + // Just multiply them together + t = /** @type {NerdamerSymbolType} */ (ParserDeps._.multiply(a, b)); + } + + // The expanded function is now t + return t; +} + +// ConvertToVector Function ======================================================== +// Uses ParserDeps._ for parser access. + +/** + * Converts an array to a vector. Consider moving this to Vector.fromArray + * + * @param {string[] | string | NerdamerSymbolType | number | number[]} x + */ +function convertToVector(x) { + if (isArray(x)) { + const vector = new Vector([]); + for (let i = 0; i < x.length; i++) { + vector.elements.push(convertToVector(x[i])); + } + return vector; + } + // Ensure that a nerdamer ready object is returned + if (!isSymbol(x)) { + return ParserDeps._.parse(x); + } + return x; +} + +// ArrayGetVariables Function ====================================================== +/** + * Gets all the variables in an array of Symbols + * + * @param {NerdamerSymbolType[]} arr + */ +function arrayGetVariables(arr) { + let vars = variables(arr[0], null, null); + + // Get all variables + for (let i = 1, l = arr.length; i < l; i++) { + vars = vars.concat(variables(arr[i])); + } + // Remove duplicates + vars = arrayUnique(vars).sort(); + + // Done + return vars; +} + +// GetU Function =================================================================== +// Uses ReservedDeps.RESERVED for u-substitution variable tracking. + +/** + * Is used for u-substitution. Gets a suitable u for substitution. If for instance a is used in the symbol then it keeps + * going down the line until one is found that's not in use. If all letters are taken then it starts appending numbers. + * IMPORTANT! It assumes that the substitution will be undone before the user gets to interact with the object again. + * + * @param {NerdamerSymbolType} symbol + */ +function getU(symbol) { + // Start with u + const u = 'u'; // Start with u + let v = u; // Init with u + let c = 0; // Postfix number + const vars = variables(symbol); + // Make sure this variable isn't reserved and isn't in the variable list + while (!(ReservedDeps.RESERVED.indexOf(v) === -1 && vars.indexOf(v) === -1)) { + v = u + c++; + } + // Get an empty slot. It seems easier to just push but the + // problem is that we may have some which are created by clearU + for ( + let i = 0, l = ReservedDeps.RESERVED.length; + i <= l; + i++ // Reserved cannot equals false or 0 so we can safely check for a falsy type + ) { + if (!ReservedDeps.RESERVED[i]) { + ReservedDeps.RESERVED[i] = v; // Reserve the variable + break; + } + } + return v; +} + +// _setFunction Function =========================================================== +/** + * Dependency container for _setFunction function. + * + * @type {{ + * _: ParserType; + * C: CoreType; + * USER_FUNCTIONS: string[]; + * }} + */ +const InternalSetFunctionDeps = { + get _() { + return CoreDeps.parser; + }, + get C() { + return CoreDeps.core; + }, + get USER_FUNCTIONS() { + return CoreDeps.state.USER_FUNCTIONS; + }, +}; + +/** + * Is used to set a user defined function using the function assign operator and also is used to set a user defined + * JavaScript function using the function assign operator + * + * @param {string | Function} fnName + * @param {string[]} [fnParams] + * @param {string} [fnBody] + * @returns {boolean} + */ +function _setFunction(fnName, fnParams, fnBody) { + if (!fnParams) { + const fnNameType = typeof fnName; + + // Option setFunction('f(x)=x^2+2'), setFunction('f(x):=x^2+2') + if (fnNameType === 'string') { + const fnNameStr = /** @type {string} */ (fnName); + if (!/:?=/u.test(fnNameStr)) { + return false; + } + + const match = Settings.FUNCTION_REGEX.exec(fnNameStr); + if (!match) { + return false; + } + const [, fName, fParams, fBody] = match; + fnName = fName; + fnParams = fParams.split(',').map(arg => arg.trim()); + fnBody = fBody; + } + + // Option setFunction(function fox(x) { return x^2; }) + else if (fnNameType === 'function') { + const jsFunction = /** @type {Function} */ (fnName); + const jsName = jsFunction.name; + validateName(jsName); + if (!isReserved(jsName)) { + InternalSetFunctionDeps.C.Math2[jsName] = jsFunction; + InternalSetFunctionDeps._.functions[jsName] = [undefined, jsFunction.length]; + + if (!InternalSetFunctionDeps.USER_FUNCTIONS.includes(jsName)) { + InternalSetFunctionDeps.USER_FUNCTIONS.push(jsName); + } + return true; + } + return false; + } else { + return false; + } + } + + fnName = /** @type {string} */ (fnName).trim(); + validateName(fnName); + + // Option setFunction('f(x)', ['x'], 'x^2+2') or setFunction('f(x)=x^2+2'), setFunction('f(x):=x^2+2') + if (!isReserved(fnName)) { + fnParams ||= variables(InternalSetFunctionDeps._.parse(fnBody)); + fnParams = fnParams.map(p => p.trim()); + // The function gets set to PARSER.mapped function which is just + // a generic function call. + InternalSetFunctionDeps._.functions[/** @type {string} */ (fnName)] = [ + InternalSetFunctionDeps._.mappedFunction, + fnParams.length, + { + name: fnName, + params: fnParams, + body: fnBody, + }, + ]; + + if (!InternalSetFunctionDeps.USER_FUNCTIONS.includes(fnName)) { + InternalSetFunctionDeps.USER_FUNCTIONS.push(fnName); + } + + return true; + } + return false; +} + +// _clearFunctions Function ======================================================== +/** + * Dependency container for _clearFunctions function. + * + * @type {{ + * _: ParserType; + * C: CoreType; + * USER_FUNCTIONS: string[]; + * }} + */ +const ClearFunctionsDeps = { + get _() { + return CoreDeps.parser; + }, + get C() { + return CoreDeps.core; + }, + get USER_FUNCTIONS() { + return CoreDeps.state.USER_FUNCTIONS; + }, +}; + +/** Clears all user defined functions */ +function _clearFunctions() { + for (const name of ClearFunctionsDeps.USER_FUNCTIONS) { + delete ClearFunctionsDeps.C.Math2[name]; + delete ClearFunctionsDeps._.functions[name]; + } +} + +// ImportFunctions Function ======================================================== +// Uses ParserDeps._ for parser access. + +/** + * Provide a mechanism for accessing functions directly. Not yet complete!!! Some functions will return undefined. This + * can maybe just remove the function object at some point when all functions are eventually housed in the global + * function object. Returns ALL parser available functions. Parser.functions may not contain all functions + * + * @returns {import('./index').NerdamerCore.MathFunctions} + */ +function importFunctions() { + /** @type {import('./index').NerdamerCore.MathFunctions} */ + const o = {}; + for (const x in ParserDeps._.functions) { + if (!Object.hasOwn(ParserDeps._.functions, x)) { + continue; + } + o[x] = /** @type {any} */ (ParserDeps._.functions[x][0]); + } + return o; +} + +// Text Function ================================================================== +/** + * Dependency container for text function. These are initialized later once they're available inside the IIFE. + * + * @type {{ + * bigInt: BigIntegerStaticType; + * isSymbol: Function; + * isVector: Function; + * N: number; + * P: number; + * S: number; + * FN: number; + * PL: number; + * CB: number; + * CP: number; + * EX: number; + * CUSTOM_OPERATORS: object; + * }} + */ +const TextDeps = { + get bigInt() { + return CoreDeps.ext.bigInt; + }, + get isSymbol() { + return CoreDeps.utils.isSymbol; + }, + get isVector() { + return CoreDeps.utils.isVector; + }, + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get PL() { + return CoreDeps.groups.PL; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get CP() { + return CoreDeps.groups.CP; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get CUSTOM_OPERATORS() { + return CoreDeps.state.CUSTOM_OPERATORS; + }, +}; + +/** + * Convert an object to its text representation. + * + * @param {NerdamerSymbolType} obj + * @param {string} [option] + * @param {number} [useGroup] + * @param {number} [decp] + * @returns {string} + */ +function text(obj, option = undefined, useGroup = undefined, decp = undefined) { + const asHash = option === 'hash'; + // Whether to wrap numbers in brackets + let wrapCondition; + const opt = asHash ? undefined : option; + const asDecimal = opt === 'decimal' || opt === 'decimals' || opt === 'decimals_or_scientific'; + + // Only set default decp for decimals_or_scientific mode, not for plain decimals. + // This preserves full valueOf() precision for internal operations. + // + // Background: When a NerdamerSymbol with a fractional multiplier (e.g., 1/3) is converted to + // a string via valueOf(), it becomes a decimal like "0.3333333333333333". If that + // decimal is then parsed back into a NerdamerSymbol, nerdamer uses a continued fractions + // algorithm (Fraction.fullConversion) to reconstruct the fraction. This algorithm + // finds the simplest fraction within epsilon (1e-30) of the decimal value. + // + // The precision matters: + // - 16 threes (0.3333333333333333): exactly equals JS's 1/3 in IEEE 754 → reconstructs to 1/3 + // - 15 threes (0.333333333333333): differs by ~3.3e-16 → becomes 321685687669321/965057063007964 + // + // Setting decp here would trigger toDecimal(16) which can truncate precision. + // By not setting decp for plain decimals mode, we preserve full valueOf() precision. + if (opt === 'decimals_or_scientific' && typeof decp === 'undefined') { + decp = Settings.DEFAULT_DECP; + } + + function toString(fracObj, decimalPlaces) { + switch (option) { + case 'decimals': + case 'decimal': + wrapCondition ||= function (_str) { + return false; + }; + if (decimalPlaces) { + return fracObj.toDecimal(decimalPlaces); + } + return fracObj.valueOf(); + case 'recurring': { + wrapCondition ||= function (s) { + return s.indexOf("'") !== -1; + }; + + const str = fracObj.toString(); + // Verify that the string is actually a fraction + const frac = /^-?\d+(?:\/\d+)?$/u.exec(str); + if (frac.length === 0) { + return str; + } + + // Split the fraction into the numerator and denominator + const parts = frac[0].split('/'); + let negative = false; + let m = Number(parts[0]); + if (m < 0) { + m = -m; + negative = true; + } + let n = Number(parts[1]); + n ||= 1; + + // https://softwareengineering.stackexchange.com/questions/192070/what-is-a-efficient-way-to-find-repeating-decimal#comment743574_192081 + /** @type {number | string} */ + let quotient = Math.floor(m / n); + let c = 10 * (m - quotient * n); + quotient = `${quotient.toString()}.`; + while (c && c < n) { + c *= 10; + quotient += '0'; + } + let digits = ''; + const passed = []; + let i = 0; + while (true) { + if (typeof passed[c] !== 'undefined') { + const prefix = digits.slice(0, passed[c]); + const cycle = digits.slice(passed[c]); + const result = `${quotient + prefix}'${cycle}'`; + return (negative ? '-' : '') + result.replace("'0'", '').replace(/\.$/u, ''); + } + const q = Math.floor(c / n); + const r = c - q * n; + passed[c] = i; + digits += q.toString(); + i += 1; + c = 10 * r; + } + } + case 'mixed': { + wrapCondition ||= function (s) { + return s.indexOf('/') !== -1; + }; + + const str = fracObj.toString(); + // Verify that the string is actually a fraction + const frac = /^-?\d+(?:\/\d+)?$/u.exec(str); + if (frac.length === 0) { + return str; + } + + // Split the fraction into the numerator and denominator + const parts = frac[0].split('/'); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + const numer = new TextDeps.bigInt(parts[0]); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + let denom = new TextDeps.bigInt(parts[1]); + if (denom.equals(0)) { + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + denom = new TextDeps.bigInt(1); + } + + // Return the quotient plus the remainder + const divmod = numer.divmod(denom); + const { quotient } = divmod; + const { remainder } = divmod; + const operator = parts[0][0] === '-' || quotient.equals(0) || remainder.equals(0) ? '' : '+'; + return ( + (quotient.equals(0) ? '' : quotient.toString()) + + operator + + (remainder.equals(0) ? '' : `${remainder.toString()}/${parts[1]}`) + ); + } + case 'scientific': + wrapCondition ||= function (_str) { + return false; + }; + return new Scientific(fracObj.valueOf()).toString(Settings.SCIENTIFIC_MAX_DECIMAL_PLACES); + case 'decimals_or_scientific': { + wrapCondition ||= function (_str) { + return false; + }; + const decimals = fracObj.valueOf(); + const scientific = new Scientific(decimals); + if (Math.abs(scientific.exponent) >= Settings.SCIENTIFIC_SWITCH_FROM_DECIMALS_MIN_EXPONENT) { + return scientific.toString(Settings.SCIENTIFIC_MAX_DECIMAL_PLACES); + } + if (decimalPlaces) { + return fracObj.toDecimal(decimalPlaces); + } + return decimals; + } + + default: + wrapCondition ||= function (s) { + return s.indexOf('/') !== -1; + }; + + return fracObj.toString(); + } + } + + // If the object is a symbol + if (TextDeps.isSymbol(obj)) { + /** @type {string | number} */ + let multiplier = ''; + let power = ''; + let sign = ''; + const group = obj.group || useGroup; + let { value } = obj; + + // If the value is to be used as a hash then the power and multiplier need to be suppressed + if (!asHash) { + // Get multiplier as string. Don't pass decp here to preserve precision + // for internal operations - decp is only applied in the TextDeps.N case below. + let om = toString(obj.multiplier); + if (String(om) === '-1' && String(obj.multiplier) === '-1') { + sign = '-'; + om = '1'; + } + // Only add the multiplier if it's not 1 + if (String(om) !== '1') { + multiplier = om; + } + // Use asDecimal to get the object back as a decimal + const p = obj.power ? toString(obj.power) : ''; + // Only add the multiplier + if (String(p) !== '1') { + // Is it a symbol + if (isSymbol(p)) { + power = text(p, opt); + } else { + power = p; + } + } + } + + switch (group) { + case TextDeps.N: { + multiplier = ''; + // Handle numeric output with appropriate precision: + // - decimals_or_scientific: use toString with decp to trigger Scientific formatting + // - decimals with explicit decp: round to requested decimal places + // - otherwise: use default toString which preserves full precision via valueOf() + let m; + if (opt === 'decimals_or_scientific') { + m = toString(obj.multiplier, decp); + } else if (decp && asDecimal) { + m = obj.multiplier.toDecimal(decp); + } else { + m = toString(obj.multiplier); + } + // If it's numerical then all we need is the multiplier + value = String(obj.multiplier) === '-1' ? '1' : m; + power = ''; + break; + } + case TextDeps.PL: + value = /** @type {NerdamerSymbolType[]} */ (obj.collectSymbols()) + .map(x => { + let txt = text(x, opt, useGroup, decp); + if (txt === '0') { + txt = ''; + } + return txt; + }) + .sort() + .join('+') + .replace(/\+-/gu, '-'); + break; + case TextDeps.CP: + value = /** @type {NerdamerSymbolType[]} */ (obj.collectSymbols()) + .map(x => { + let txt = text(x, opt, useGroup, decp); + if (txt === '0') { + txt = ''; + } + return txt; + }) + .sort() + .join('+') + .replace(/\+-/gu, '-'); + break; + case TextDeps.CB: + value = obj + .collectSymbols(symbol => { + const g = symbol.group; + // Both groups will already be in brackets if their power is greater than 1 + // so skip it. + if ( + (g === TextDeps.PL || g === TextDeps.CP) && + symbol.power.equals(1) && + symbol.multiplier.equals(1) + ) { + return inBrackets(text(symbol, opt)); + } + return text(symbol, opt); + }) + .join('*'); + break; + case TextDeps.EX: { + const pg = obj.previousGroup; + const pwg = /** @type {NerdamerSymbolType} */ (obj.power).group; + + // TextDeps.PL are the exception. It's simpler to just collect and set the value + if (pg === TextDeps.PL) { + value = obj.collectSymbols(text, opt).join('+').replace('+-', '-'); + } + if (!(pg === TextDeps.N || pg === TextDeps.S || pg === TextDeps.FN) && !asHash) { + value = inBrackets(value); + } + + if ( + (pwg === TextDeps.CP || + pwg === TextDeps.CB || + pwg === TextDeps.PL || + /** @type {NerdamerSymbolType} */ (obj.power).multiplier.toString() !== '1') && + power + ) { + power = inBrackets(power); + } + break; + } + } + + if (group === TextDeps.FN) { + value = obj.fname + inBrackets(obj.args.map(symbol => text(symbol, opt)).join(',')); + } + // TODO: Needs to be more efficient. Maybe. + if (group === TextDeps.FN && obj.fname in TextDeps.CUSTOM_OPERATORS) { + let a = text(obj.args[0]); + let b = text(obj.args[1]); + if (obj.args[0].isComposite()) // Preserve the brackets + { + a = inBrackets(a); + } + if (obj.args[1].isComposite()) // Preserve the brackets + { + b = inBrackets(b); + } + value = a + TextDeps.CUSTOM_OPERATORS[obj.fname] + b; + } + // Wrap the power since / is less than ^ + // TODO: introduce method call isSimple + const shouldWrapPower = + typeof wrapCondition === 'function' ? /** @type {Function} */ (wrapCondition)(power) : false; + if (power && group !== TextDeps.EX && shouldWrapPower) { + power = inBrackets(power); + } + + // The following groups are held together by plus or minus. They can be raised to a power or multiplied + // by a multiplier and have to be in brackets to preserve the order of precedence + if ( + ((group === TextDeps.CP || group === TextDeps.PL) && + ((multiplier && String(multiplier) !== '1') || sign === '-')) || + ((group === TextDeps.CB || group === TextDeps.CP || group === TextDeps.PL) && + power && + String(power) !== '1') || + (!asHash && group === TextDeps.P && String(value) === '-1') || + obj.fname === Settings.PARENTHESIS + ) { + value = inBrackets(value); + } + + if ( + decp && + (option === 'decimal' || ((option === 'decimals' || option === 'decimals_or_scientific') && multiplier)) + ) { + // Scientific notation? regular rounding would be the wrong decision here + if (multiplier.toString().includes('e')) { + if (option !== 'decimals_or_scientific') { + // ToPrecision can create extra digits, so we also + // convert it to string straight up and pick the shorter version + const numMult = Number(multiplier); + const m1 = numMult.toExponential(); + const m2 = numMult.toPrecision(decp); + /** @type {string | number} */ + multiplier = m1.length < m2.length ? m1 : m2; + } + } else { + multiplier = nround(Number(multiplier), decp); + } + } + + // Add the sign back + let c = sign + multiplier; + + const shouldWrapMult = + typeof wrapCondition === 'function' ? /** @type {Function} */ (wrapCondition)(multiplier) : false; + if (multiplier && shouldWrapMult) { + c = inBrackets(c); + } + + if (Number(power) < 0) { + power = inBrackets(power); + } + + // Add the multiplication back + if (multiplier) { + c = `${c}*`; + } + + if (power) { + if (value === 'e' && Settings.E_TO_EXP) { + return `${c}exp${inBrackets(power)}`; + } + power = Settings.POWER_OPERATOR + power; + } + + // This needs serious rethinking. Must fix + if (group === TextDeps.EX && value.charAt(0) === '-') { + value = inBrackets(value); + } + + let cv = c + value; + + if (obj.parens) { + cv = inBrackets(cv); + } + + return cv + power; + } + if (TextDeps.isVector(obj)) { + const l = obj.elements.length; + const c = []; + for (let i = 0; i < l; i++) { + c.push(obj.elements[i].text(option)); + } + return `[${c.join(',')}]`; + } + try { + return obj.toString(); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return ''; + } +} + +/** + * Dependency container for NerdamerSymbol class. These are initialized later once they're available inside the IIFE. + * + * @type {{ + * bigDec: DecimalStaticType; + * bigInt: BigIntegerStaticType; + * _: ParserType; + * N: number; + * P: number; + * S: number; + * FN: number; + * PL: number; + * CB: number; + * CP: number; + * EX: number; + * CONST_HASH: string; + * isSymbol: Function; + * text: Function; + * variables: Function; + * SQRT: string; + * PARENTHESIS: string; + * }} + */ +const NerdamerSymbolDeps = { + get bigDec() { + return CoreDeps.ext.bigDec; + }, + get bigInt() { + return CoreDeps.ext.bigInt; + }, + get _() { + return CoreDeps.parser; + }, + get N() { + return CoreDeps.groups.N; + }, + get P() { + return CoreDeps.groups.P; + }, + get S() { + return CoreDeps.groups.S; + }, + get FN() { + return CoreDeps.groups.FN; + }, + get PL() { + return CoreDeps.groups.PL; + }, + get CB() { + return CoreDeps.groups.CB; + }, + get CP() { + return CoreDeps.groups.CP; + }, + get EX() { + return CoreDeps.groups.EX; + }, + get CONST_HASH() { + return CoreDeps.fnNames.CONST_HASH; + }, + get isSymbol() { + return CoreDeps.utils.isSymbol; + }, + get text() { + return CoreDeps.utils.text; + }, + get variables() { + return CoreDeps.utils.variables; + }, + get SQRT() { + return CoreDeps.fnNames.SQRT; + }, + get PARENTHESIS() { + return CoreDeps.fnNames.PARENTHESIS; + }, +}; + +/** + * NerdamerSymbol class - The core symbol class for mathematical expressions + * + * @implements {NerdamerSymbolType} + */ +class NerdamerSymbol { + /** @type {number} */ + group; + + /** @type {string} */ + value; + + /** @type {FracType} */ + multiplier; + + /** @type {FracType | NerdamerSymbolType} */ + power; + + /** @type {NerdamerSymbolType[] | undefined} */ + args = undefined; + + /** @type {string | undefined} */ + fname = undefined; + + /** @type {boolean | undefined} */ + isImgSymbol = undefined; + + /** @type {boolean | undefined} */ + imaginary = undefined; + + /** @type {boolean | undefined} */ + isInfinity = undefined; + + /** @param {string | number | FracType | object} obj */ + constructor(obj) { + checkTimeout(); + + const isInfinity = obj === 'Infinity'; + // Convert big numbers to a string + if ( + typeof obj === 'object' && + obj !== null && + /** @type {DecimalType} */ (obj) instanceof NerdamerSymbolDeps.bigDec + ) { + obj = /** @type {DecimalType} */ (obj).toString(); + } + // Define numeric symbols + const objStr = String(obj); + if ( + /^(?-?\+?\d+)\.?\d*e?-?\+?\d*/iu.test(objStr) || + (typeof obj === 'object' && + obj !== null && + /** @type {DecimalType} */ (obj) instanceof NerdamerSymbolDeps.bigDec) + ) { + this.group = NerdamerSymbolDeps.N; + this.value = NerdamerSymbolDeps.CONST_HASH; + this.multiplier = new Frac(obj); + } + // Define symbolic symbols + else { + this.group = NerdamerSymbolDeps.S; + validateName(obj); + this.value = obj; + this.multiplier = new Frac(1); + this.imaginary = obj === Settings.IMAGINARY; + this.isInfinity = isInfinity; + } + + // As of 6.0.0 we switched to infinite precision so all objects have a power + // Although this is still redundant in constants, it simplifies the logic in + // other parts so we'll keep it + this.power = new Frac(1); + } + + /** + * Returns vanilla imaginary symbol + * + * @returns {NerdamerSymbolType} + */ + static imaginary() { + const s = new NerdamerSymbol(Settings.IMAGINARY); + s.imaginary = true; + return s; + } + + /** + * Return nerdamer's representation of Infinity + * + * @param {number} negative -1 to return negative infinity + * @returns {NerdamerSymbolType} + */ + static infinity(negative = undefined) { + const v = new NerdamerSymbol('Infinity'); + if (negative === -1) { + v.negate(); + } + return v; + } + + /** + * Creates a shell symbol for a given group + * + * @param {number} group + * @param {string | number} [value] + * @returns {NerdamerSymbolType} + */ + static shell(group, value) { + const symbol = new NerdamerSymbol(value); + symbol.group = group; + symbol.symbols = {}; + symbol.length = 0; + return symbol; + } + + /** + * Sqrt(x) -> x^(1/2) + * + * @param {NerdamerSymbolType} symbol + * @param {boolean} [all] + * @returns {NerdamerSymbolType} + */ + static unwrapSQRT(symbol, all) { + const p = symbol.power; + if (symbol.fname === Settings.SQRT && (symbol.isLinear() || all)) { + const t = symbol.args[0].clone(); + // Power is Frac here since we're in a function context (not EX group) + t.power = /** @type {FracType} */ (t.power).multiply(new Frac(1 / 2)); + t.multiplier = t.multiplier.multiply(symbol.multiplier); + symbol = t; + if (all) { + symbol.power = /** @type {FracType} */ (p).multiply(new Frac(1 / 2)); + } + } + + return symbol; + } + + /** + * @param {NerdamerSymbolType} [a] + * @param {NerdamerSymbolType} [b] + * @returns {NerdamerSymbolType} + */ + static hyp(a, b) { + a ||= new NerdamerSymbol(0); + b ||= new NerdamerSymbol(0); + const { _ } = NerdamerSymbolDeps; + return /** @type {NerdamerSymbolType} */ ( + _.sqrt( + /** @type {NerdamerSymbolType} */ ( + _.add( + _.pow(/** @type {NerdamerSymbolType} */ (a.clone()), new NerdamerSymbol(2)), + _.pow(/** @type {NerdamerSymbolType} */ (b.clone()), new NerdamerSymbol(2)) + ) + ) + ) + ); + } + + /** + * Converts to polar form array + * + * @param {NerdamerSymbolType} symbol + * @returns {[NerdamerSymbolType, NerdamerSymbolType]} + */ + static toPolarFormArray(symbol) { + const re = symbol.realpart(); + const im = symbol.imagpart(); + const r = NerdamerSymbol.hyp(re, im); + const theta = re.equals(0) + ? /** @type {NerdamerSymbolType} */ (NerdamerSymbolDeps._.parse('pi/2')) + : /** @type {NerdamerSymbolType} */ ( + NerdamerSymbolDeps._.trig.atan( + /** @type {NerdamerSymbolType} */ (NerdamerSymbolDeps._.divide(im, re)) + ) + ); + return [r, theta]; + } + + /** + * Removes parentheses + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + static unwrapPARENS(symbol) { + if (symbol.fname === '') { + const r = symbol.args[0]; + // Power.multiply: both powers should be Frac in parentheses context + r.power = /** @type {FracType} */ (r.power).multiply(/** @type {FracType} */ (symbol.power)); + r.multiplier = r.multiplier.multiply(symbol.multiplier); + if (symbol.fname === '') { + return NerdamerSymbol.unwrapPARENS(r); + } + return r; + } + return symbol; + } + + /** + * Quickly creates a NerdamerSymbol + * + * @param {string | number} value + * @param {number} [power] + * @returns {NerdamerSymbolType} + */ + static create(value, power) { + power = power === undefined ? 1 : power; + const { _ } = NerdamerSymbolDeps; + return _.parse(`(${value})^(${power})`); + } + /** @returns {NerdamerSymbolType} */ + pushMinus() { + const { _ } = NerdamerSymbolDeps; + /** @type {NerdamerSymbolType} */ + let retval = this; + if ( + (this.group === NerdamerSymbolDeps.CB || + this.group === NerdamerSymbolDeps.CP || + this.group === NerdamerSymbolDeps.PL) && + this.multiplier.lessThan(0) && + !even(this.power) + ) { + // Console.log(); + // console.log("replacing "+this.text("fractions")) + retval = this.clone(); + const m = retval.multiplier.clone(); + m.negate(); + // Console.log(" negated multiplier: "+m) + retval.toUnitMultiplier(); + + // Console.log(" unit main part: "+this) + for (const termkey in retval.symbols) { + if (!Object.hasOwn(retval.symbols, termkey)) { + continue; + } + retval.symbols[termkey] = retval.symbols[termkey].clone().negate(); + // Console.log(" negated term: "+this.symbols[termkey]) + if (retval.group === NerdamerSymbolDeps.CB) { + // Console.log(" is CB, breaking"); + break; + } + } + + // Console.log(" combined: "+retval.text("fractions")); + if (retval.length > 0) { + retval.each(c => c.pushMinus()); + // Console.log(" result: "+this.text("fractions")); + } + + // Console.log(" negated main part: "+retval) + retval = /** @type {NerdamerSymbolType} */ (_.parse(retval)); + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(/** @type {NerdamerSymbolType} */ (_.parse(m)), retval) + ); + } + return retval; + } + + /** + * Gets nth root accounting for rounding errors + * + * @param {number} n + * @returns {NerdamerSymbolType} + */ + getNth(n) { + const { _ } = NerdamerSymbolDeps; + // First calculate the root + const parsedN = /** @type {NerdamerSymbolType} */ (_.parse(String(n))); + const root = /** @type {NerdamerSymbolType} */ ( + evaluate( + /** @type {NerdamerSymbolType} */ ( + _.pow(/** @type {NerdamerSymbolType} */ (_.parse(this.multiplier)), parsedN.clone().invert()) + ) + ) + ); + // Round of any errors + const rounded = /** @type {NerdamerSymbolType} */ ( + _.parse(nround(/** @type {number} */ (/** @type {unknown} */ (root)))) + ); + // Reverse the root + const e = /** @type {NerdamerSymbolType} */ ( + evaluate(/** @type {NerdamerSymbolType} */ (_.pow(rounded, parsedN.clone()))) + ); + // If the rounded root equals the original number then we're good + if (e.equals(/** @type {NerdamerSymbolType} */ (_.parse(this.multiplier)))) { + return rounded; + } + // Otherwise return the unrounded version + return root; + } + + /** + * Checks if symbol is to the nth power + * + * @returns {boolean} + */ + isToNth(n) { + const { _ } = NerdamerSymbolDeps; + // Start by check in the multiplier for squareness + // First get the root but round it because currently we still depend + const root = this.getNth(n); + const nthMultiplier = isInt(root.multiplier.toDecimal()); + let nthPower; + + if (this.group === NerdamerSymbolDeps.CB) { + // Start by assuming that all will be square. + nthPower = true; + // All it takes is for one of the symbols to not have an even power + // e.g. x^n1*y^n2 requires that both n1 and n2 are even + this.each(x => { + const isNth = x.isToNth(n); + + if (!isNth) { + nthPower = false; + } + }); + } else { + // Check if the power is divisible by n if it's not a number. + nthPower = this.group === NerdamerSymbolDeps.N ? true : isInt(_.divide(_.parse(this.power), _.parse(n))); + } + + return nthMultiplier && nthPower; + } + + /** + * Checks if a symbol is square + * + * @returns {boolean} + */ + isSquare() { + return this.isToNth(2); + } + + /** + * Checks if a symbol is cube + * + * @returns {boolean} + */ + isCube() { + return this.isToNth(3); + } + + /** + * Checks if a symbol is a bare variable + * + * @returns {boolean} + */ + isSimple() { + return this.power.equals(1) && this.multiplier.equals(1); + } + + /** + * Simplifies the power of the symbol + * + * @returns {NerdamerSymbolType} A clone of the symbol + */ + powSimp() { + const { _ } = NerdamerSymbolDeps; + if (this.group === NerdamerSymbolDeps.CB) { + const powers = []; + const sign = this.multiplier.sign(); + this.each(x => { + const p = x.power; + // Why waste time if I can't do anything anyway + if (NerdamerSymbolDeps.isSymbol(p) || p.equals(1)) { + return; + } + powers.push(p); + }); + if (powers.length === 0) { + return this.clone(); + } + const min = new Frac(arrayMin(powers)); + + // Handle the coefficient + // handle the multiplier + // sign already declared above + const m = this.multiplier.clone().abs(); + const mfactors = Math2.ifactor(/** @type {number} */ (m.valueOf())); + // If we have a multiplier of 6750 and a min of 2 then the factors are 5^3*5^3*2 + // we can then reduce it to 2*3*5*(15)^2 + let out_ = new Frac(1); + let in_ = new Frac(1); + + for (const x in mfactors) { + if (!Object.hasOwn(mfactors, x)) { + continue; + } + let n = new Frac(mfactors[x]); + if (!n.lessThan(min)) { + n = n.divide(min).subtract(new Frac(1)); + in_ = in_.multiply(new Frac(x)); // Move the factor inside the bracket + } + + out_ = out_.multiply( + /** @type {NerdamerSymbolType} */ (_.parse(`${inBrackets(x)}^${inBrackets(n)}`)).multiplier + ); + } + /** @type {NerdamerSymbolType} */ + let t = new NerdamerSymbol(in_); + this.each(x => { + x = x.clone(); + x.power = x.power.divide(min); + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, /** @type {NerdamerSymbolType} */ (x))); + }); + + const xt = /** @type {NerdamerSymbolType} */ (_.symfunction(NerdamerSymbolDeps.PARENTHESIS, [t])); + xt.power = min; + xt.multiplier = sign < 0 ? out_.negate() : out_; + + return xt; + } + return this.clone(); + } + + /** + * Checks to see if two functions are of equal value + * + * @param {string | number | NerdamerSymbolType} symbol + * @returns {boolean} + */ + equals(symbol) { + /** @type {NerdamerSymbolType} */ + let sym; + if (NerdamerSymbolDeps.isSymbol(symbol)) { + sym = /** @type {NerdamerSymbolType} */ (symbol); + } else { + sym = new NerdamerSymbol(symbol); + } + return ( + this.value === sym.value && + /** @type {FracType} */ (this.power).equals(/** @type {FracType} */ (sym.power)) && + this.multiplier.equals(sym.multiplier) && + this.group === sym.group + ); + } + + /** @returns {NerdamerSymbolType} */ + abs() { + const e = this.clone(); + e.multiplier.abs(); + return e; + } + + /** + * Greater than + * + * @param {string | number | NerdamerSymbolType} symbol + * @returns {boolean} + */ + gt(symbol) { + if (!NerdamerSymbolDeps.isSymbol(symbol)) { + symbol = /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (new NerdamerSymbol(symbol))); + } + const sym = /** @type {NerdamerSymbolType} */ (symbol); + return this.isConstant() && sym.isConstant() && this.multiplier.greaterThan(sym.multiplier); + } + + /** + * Greater than or equal + * + * @param {string | number | NerdamerSymbolType} symbol + * @returns {boolean} + */ + gte(symbol) { + if (!NerdamerSymbolDeps.isSymbol(symbol)) { + symbol = new NerdamerSymbol(symbol); + } + const sym = /** @type {NerdamerSymbolType} */ (symbol); + return ( + this.equals(sym) || (this.isConstant() && sym.isConstant() && this.multiplier.greaterThan(sym.multiplier)) + ); + } + + /** + * Less than + * + * @param {string | number | NerdamerSymbolType} symbol + * @returns {boolean} + */ + lt(symbol) { + if (!NerdamerSymbolDeps.isSymbol(symbol)) { + symbol = new NerdamerSymbol(symbol); + } + const sym = /** @type {NerdamerSymbolType} */ (symbol); + return this.isConstant() && sym.isConstant() && this.multiplier.lessThan(sym.multiplier); + } + + /** + * Less than or equal + * + * @param {string | number | NerdamerSymbolType} symbol + * @returns {boolean} + */ + lte(symbol) { + if (!NerdamerSymbolDeps.isSymbol(symbol)) { + symbol = new NerdamerSymbol(symbol); + } + const sym = /** @type {NerdamerSymbolType} */ (symbol); + return this.equals(sym) || (this.isConstant() && sym.isConstant() && this.multiplier.lessThan(sym.multiplier)); + } + + /** + * Because nerdamer doesn't group symbols by polynomials but rather a custom grouping method, this has to be + * reinserted in order to make use of most algorithms. This function checks if the symbol meets the criteria of a + * polynomial. + * + * @param {boolean} [multivariate] + * @returns {boolean} + */ + isPoly(multivariate = false) { + const g = this.group; + const p = this.power; + // The power must be a integer so fail if it's not + if (!isInt(p) || Number(p) < 0) { + return false; + } + // Constants and first orders + if (g === NerdamerSymbolDeps.N || g === NerdamerSymbolDeps.S || this.isConstant(true)) { + return true; + } + const vars = NerdamerSymbolDeps.variables(this); + if (g === NerdamerSymbolDeps.CB && vars.length === 1) { + // The variable is assumed the only one that was found + const v = vars[0]; + // If no variable then guess what!?!? We're done!!! We have a polynomial. + if (!v) { + return true; + } + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const sym = this.symbols[x]; + // Sqrt(x) + if (sym.group === NerdamerSymbolDeps.FN && !sym.args[0].isConstant()) { + return false; + } + if (!sym.contains(v) && !sym.isConstant(true)) { + return false; + } + } + return true; + } + // PL groups. These only fail if a power is not an int + // this should handle cases such as x^2*t + if (this.isComposite() || (g === NerdamerSymbolDeps.CB && multivariate)) { + // Fail if we're not checking for multivariate polynomials + if (!multivariate && vars.length > 1) { + return false; + } + // Loop though the symbols and check if they qualify + for (const x in this.symbols) { + // We've already the symbols if we're not checking for multivariates at this point + // so we check the sub-symbols + if (!this.symbols[x].isPoly(multivariate)) { + return false; + } + } + return true; + } + return false; + + /* + //all tests must have passed so we must be dealing with a polynomial + return true; + */ + } + // Removes the requested variable from the symbol and returns the remainder + /** + * @param {string} x + * @param {boolean} [excludeX] + * @returns {NerdamerSymbolType} + */ + stripVar(x, excludeX = false) { + const { _ } = NerdamerSymbolDeps; + /** @type {NerdamerSymbolType} */ + let retval; + if ((this.group === NerdamerSymbolDeps.PL || this.group === NerdamerSymbolDeps.S) && this.value === x) { + retval = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(excludeX ? 0 : this.multiplier)) + ); + } else if (this.group === NerdamerSymbolDeps.CB && this.isLinear()) { + retval = new NerdamerSymbol(1); + this.each(s => { + if (!s.contains(x, true)) { + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(retval, /** @type {NerdamerSymbolType} */ (s.clone())) + ); + } + }); + retval.multiplier = retval.multiplier.multiply(this.multiplier); + } else if (this.group === NerdamerSymbolDeps.CP && !this.isLinear()) { + retval = new NerdamerSymbol(this.multiplier); + } else if (this.group === NerdamerSymbolDeps.CP && this.isLinear()) { + retval = new NerdamerSymbol(0); + this.each(s => { + if (!s.contains(x)) { + const t = s.clone(); + t.multiplier = t.multiplier.multiply(this.multiplier); + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, /** @type {NerdamerSymbolType} */ (t))); + } + }); + // BIG TODO!!! It doesn't make much sense + if (retval.equals(0)) { + retval = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(this.multiplier)) + ); + } + } else if ( + this.group === NerdamerSymbolDeps.EX && + /** @type {NerdamerSymbolType} */ (this.power).contains(x, true) + ) { + retval = new NerdamerSymbol(this.multiplier); + } else if (this.group === NerdamerSymbolDeps.FN && this.contains(x)) { + retval = new NerdamerSymbol(this.multiplier); + } else // Wth? This should technically be the multiplier. + // Unfortunately this method wasn't very well thought out :`(. + // should be: retval = new NerdamerSymbol(this.multiplier); + // use: ((1+x^2)*sqrt(-1+x^2))^(-1) for correction. + // this will break a bunch of unit tests so be ready to for the long haul + { + retval = this.clone(); + } + + return retval; + } + // Returns symbol in array form with x as base e.g. a*x^2+b*x+c = [c, b, a]. + toArray(v, arr) { + const { _ } = NerdamerSymbolDeps; + arr ||= { + arr: [], + add(x, idx) { + const e = this.arr[idx]; + this.arr[idx] = e ? _.add(e, x) : x; + }, + }; + const g = this.group; + + if (g === NerdamerSymbolDeps.S && this.contains(v)) { + arr.add(new NerdamerSymbol(this.multiplier), this.power); + } else if (g === NerdamerSymbolDeps.CB) { + const a = this.stripVar(v); + const x = /** @type {NerdamerSymbolType} */ ( + _.divide( + /** @type {NerdamerSymbolType} */ (this.clone()), + /** @type {NerdamerSymbolType} */ (a.clone()) + ) + ); + const p = x.isConstant() ? 0 : x.power; + arr.add(a, p); + } else if (g === NerdamerSymbolDeps.PL && this.value === v) { + this.each((x, p) => { + arr.add(x.stripVar(v), p); + }); + } else if (g === NerdamerSymbolDeps.CP) { + // The logic: they'll be broken into symbols so e.g. (x^2+x)+1 or (a*x^2+b*x+c) + // each case is handled above + this.each(x => { + x.toArray(v, arr); + }); + } else if (this.contains(v)) { + throw new NerdamerTypeError('Cannot convert to array! Exiting'); + } else { + arr.add(this.clone(), 0); // It's just a constant wrt to v + } + // Fill the holes + arr = arr.arr; // Keep only the array since we don't need the object anymore + for (let i = 0; i < arr.length; i++) { + arr[i] ||= new NerdamerSymbol(0); + } + return arr; + } + // Checks to see if a symbol contans a function + hasFunc(v) { + const fnGroup = this.group === NerdamerSymbolDeps.FN || this.group === NerdamerSymbolDeps.EX; + if ((fnGroup && !v) || (fnGroup && this.contains(v))) { + return true; + } + if (this.symbols) { + for (const x in this.symbols) { + if (this.symbols[x].hasFunc(v)) { + return true; + } + } + } + return false; + } + sub(a, b) { + const { _ } = NerdamerSymbolDeps; + a = NerdamerSymbolDeps.isSymbol(a) ? a.clone() : _.parse(a); + b = NerdamerSymbolDeps.isSymbol(b) ? b.clone() : _.parse(b); + if (a.group === NerdamerSymbolDeps.N || a.group === NerdamerSymbolDeps.P) { + err('Cannot substitute a number. Must be a variable'); + } + let samePow = false; + const aIsUnitMultiplier = a.multiplier.equals(1); + let m = this.multiplier.clone(); + let retval; + /* + * In order to make the substitution the bases have to first match take + * (x+1)^x -> (x+1)=y || x^2 -> x=y^6 + * In both cases the first condition is that the bases match so we begin there + * Either both are PL or both are not PL but we cannot have PL and a non-PL group match + */ + if ( + this.value === a.value && + ((this.group !== NerdamerSymbolDeps.PL && a.group !== NerdamerSymbolDeps.PL) || + (this.group === NerdamerSymbolDeps.PL && a.group === NerdamerSymbolDeps.PL)) + ) { + // We cleared the first hurdle but a subsitution may not be possible just yet + if (aIsUnitMultiplier || a.multiplier.equals(this.multiplier)) { + if (a.isLinear()) { + retval = b; + } else if (a.power.equals(this.power)) { + retval = b; + samePow = true; + } + if (a.multiplier.equals(this.multiplier)) { + m = new Frac(1); + } + } + } + // The next thing is to handle CB + else if (this.group === NerdamerSymbolDeps.CB || this.previousGroup === NerdamerSymbolDeps.CB) { + retval = new NerdamerSymbol(1); + this.each(x => { + const subbed = _.parse(x.sub(a, b)); // Parse it again for safety + retval = _.multiply(retval, subbed); + }); + } else if (this.isComposite()) { + const symbol = this.clone(); + + if (a.isComposite() && symbol.isComposite() && symbol.isLinear() && a.isLinear()) { + const find = function (stack, needle) { + for (const x in stack.symbols) { + if (!Object.hasOwn(stack.symbols, x)) { + continue; + } + const sym = stack.symbols[x]; + // If the symbol equals the needle or it's within the sub-symbols we're done + if ((sym.isComposite() && find(sym, needle)) || sym.equals(needle)) { + return true; + } + } + return false; + }; + // Go fish + for (const x in a.symbols) { + if (!Object.hasOwn(a.symbols, x)) { + continue; + } + if (!find(symbol, a.symbols[x])) { + return symbol.clone(); + } + } + retval = _.add(_.subtract(symbol.clone(), a), b); + } else { + retval = new NerdamerSymbol(0); + symbol.each(x => { + retval = _.add(retval, x.sub(a, b)); + }); + } + } else if (this.group === NerdamerSymbolDeps.EX) { + // The parsed value could be a function so parse and sub + retval = _.parse(this.value).sub(a, b); + } else if (this.group === NerdamerSymbolDeps.FN) { + const nargs = []; + for (let i = 0; i < this.args.length; i++) { + /** @type {NerdamerSymbolType} */ + let arg = this.args[i]; + if (!NerdamerSymbolDeps.isSymbol(arg)) { + arg = _.parse(arg); + } + nargs.push(arg.sub(a, b)); + } + retval = _.symfunction(this.fname, nargs); + } + // If we did manage a substitution + if (retval) { + if (!samePow) { + // Substitute the power + const p = + this.group === NerdamerSymbolDeps.EX + ? /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (this.power)).sub(a, b) + : _.parse(this.power); + // Now raise the symbol to that power + retval = _.pow(retval, p); + } + + // Transfer the multiplier + retval.multiplier = retval.multiplier.multiply(m); + + // Done + return retval; + } + // If all else fails + return this.clone(); + } + isMonomial() { + if (this.group === NerdamerSymbolDeps.S) { + return true; + } + if (this.group === NerdamerSymbolDeps.CB) { + for (const x in this.symbols) { + if (this.symbols[x].group !== NerdamerSymbolDeps.S) { + return false; + } + } + } else { + return false; + } + return true; + } + isPi() { + return this.group === NerdamerSymbolDeps.S && this.value === 'pi'; + } + sign() { + return this.multiplier.sign(); + } + isE() { + return this.value === 'e'; + } + isSQRT() { + return this.fname === NerdamerSymbolDeps.SQRT; + } + isConstant(checkAll, checkSymbols) { + if (checkSymbols && this.group === NerdamerSymbolDeps.CB) { + for (const x in this.symbols) { + if (this.symbols[x].isConstant(true)) { + return true; + } + } + } + + if (checkAll === 'functions' && this.isComposite()) { + let isConstant = true; + + this.each(x => { + if (!x.isConstant(checkAll, checkSymbols)) { + isConstant = false; + } + }, true); + + return isConstant; + } + + if (checkAll === 'all' && (this.isPi() || this.isE())) { + return true; + } + + if (checkAll && this.group === NerdamerSymbolDeps.FN) { + for (let i = 0; i < this.args.length; i++) { + if (!this.args[i].isConstant(checkAll)) { + return false; + } + } + return true; + } + + if (checkAll) { + return isNumericSymbol(this); + } + return this.value === NerdamerSymbolDeps.CONST_HASH; + } + // The symbols is imaginary if + // 1. n*i + // 2. a+b*i + // 3. a*i + isImaginary() { + if (this.imaginary) { + return true; + } + if (this.symbols) { + for (const x in this.symbols) { + if (this.symbols[x].isImaginary()) { + return true; + } + } + } + return false; + } + /** + * Returns the real part of a symbol + * + * @returns {NerdamerSymbolType} + */ + realpart() { + const { _ } = NerdamerSymbolDeps; + if (this.isConstant()) { + return this.clone(); + } + if (this.imaginary) { + return new NerdamerSymbol(0); + } + if (this.isComposite()) { + /** @type {NerdamerSymbolType} */ + let retval = new NerdamerSymbol(0); + this.each(x => { + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, x.realpart())); + }); + return retval; + } + if (this.isImaginary()) { + return new NerdamerSymbol(0); + } + return this.clone(); + } + /* + * Return imaginary part of a symbol + * @returns {NerdamerSymbolType} + */ + imagpart() { + const { _ } = NerdamerSymbolDeps; + if (this.group === NerdamerSymbolDeps.S && this.isImaginary()) { + /** @type {NerdamerSymbolType} */ + let x = this; + // In S group, power is always Frac + if (/** @type {FracType} */ (this.power).isNegative()) { + x = this.clone(); + x.power.negate(); + x.multiplier.negate(); + } + return new NerdamerSymbol(x.multiplier); + } + if (this.isComposite()) { + /** @type {NerdamerSymbolType} */ + let retval = new NerdamerSymbol(0); + this.each(x => { + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, x.imagpart())); + }); + return retval; + } + if (this.group === NerdamerSymbolDeps.CB) { + return this.stripVar(Settings.IMAGINARY); + } + return new NerdamerSymbol(0); + } + isInteger() { + return this.isConstant() && this.multiplier.isInteger(); + } + isLinear(wrt) { + if (wrt) { + if (this.isConstant()) { + return true; + } + // If this symbol doesn't contain the variable (including in exponents), it's constant with respect to it + if (!this.contains(wrt, true)) { + return true; + } + if (this.group === NerdamerSymbolDeps.S) { + if (this.value === wrt) { + return this.power.equals(1); + } + return true; + } + + if (this.isComposite() && this.power.equals(1)) { + for (const x in this.symbols) { + if (!this.symbols[x].isLinear(wrt)) { + return false; + } + } + return true; + } + + if (this.group === NerdamerSymbolDeps.CB) { + // If the variable doesn't exist in this term, it's constant wrt that variable, hence linear + if (!this.symbols[wrt]) { + return true; + } + return this.symbols[wrt].isLinear(wrt); + } + return false; + } + return this.power.equals(1); + } + /** + * Checks to see if a symbol has a function by a specified name or within a specified list + * + * @param {string | string[]} names + * @returns {boolean} + */ + containsFunction(names) { + if (typeof names === 'string') { + names = [names]; + } + if (this.group === NerdamerSymbolDeps.FN && names.indexOf(this.fname) !== -1) { + return true; + } + if (this.symbols) { + for (const x in this.symbols) { + if (this.symbols[x].containsFunction(names)) { + return true; + } + } + } + return false; + } + /** + * Multiplies the current power by the given power + * + * @param {NerdamerSymbolType | FracType} p2 + * @returns {NerdamerSymbolType} + */ + multiplyPower(p2) { + const { _ } = NerdamerSymbolDeps; + // Leave out 1 + if (this.group === NerdamerSymbolDeps.N && this.multiplier.equals(1)) { + return this; + } + + /** @type {FracType | NerdamerSymbolType} */ + let p1 = this.power; + + if ( + this.group !== NerdamerSymbolDeps.EX && + NerdamerSymbolDeps.isSymbol(p2) && + /** @type {NerdamerSymbolType} */ (p2).group === NerdamerSymbolDeps.N + ) { + const p = /** @type {NerdamerSymbolType} */ (p2).multiplier; + if (this.group === NerdamerSymbolDeps.N && !p.isInteger()) { + this.convert(NerdamerSymbolDeps.P); + } + + this.power = /** @type {FracType} */ (p1.equals(1) ? p.clone() : /** @type {FracType} */ (p1).multiply(p)); + + if (this.group === NerdamerSymbolDeps.P && isInt(this.power)) { + // Bring it back to an N + this.value = String(Number(this.value) ** Number(this.power)); + this.toLinear(); + this.convert(NerdamerSymbolDeps.N); + } + } else { + if (this.group !== NerdamerSymbolDeps.EX) { + p1 = /** @type {FracType | NerdamerSymbolType} */ (/** @type {unknown} */ (new NerdamerSymbol(p1))); + this.convert(NerdamerSymbolDeps.EX); + } + /** @type {FracType | NerdamerSymbolType} */ + const newPower = /** @type {FracType | NerdamerSymbolType} */ ( + _.multiply(/** @type {NerdamerSymbolType} */ (p1), /** @type {NerdamerSymbolType} */ (p2)) + ); + /** @type {FracType | NerdamerSymbolType} */ + this.power = newPower; + } + + return this; + } + setPower(p, retainSign = false) { + // Leave out 1 + if (this.group === NerdamerSymbolDeps.N && this.multiplier.equals(1)) { + return this; + } + if (this.group === NerdamerSymbolDeps.EX && !NerdamerSymbolDeps.isSymbol(p)) { + this.group = this.previousGroup; + delete this.previousGroup; + if (this.group === NerdamerSymbolDeps.N) { + this.multiplier = new Frac(this.value); + this.value = NerdamerSymbolDeps.CONST_HASH; + } else { + this.power = p; + } + } else { + let isSymbolic = false; + if (NerdamerSymbolDeps.isSymbol(p)) { + if (p.group === NerdamerSymbolDeps.N) { + // P should be the multiplier instead + p = p.multiplier; + } else { + isSymbolic = true; + } + } + const group = isSymbolic ? NerdamerSymbolDeps.EX : NerdamerSymbolDeps.P; + this.power = p; + if (this.group === NerdamerSymbolDeps.N && group) { + this.convert(group, retainSign); + } + } + + return this; + } + /** + * Checks to see if symbol is located in the denominator + * + * @returns {boolean} + */ + isInverse() { + if (this.group === NerdamerSymbolDeps.EX) { + return /** @type {NerdamerSymbolType} */ (this.power).multiplier.lessThan(0); + } + return Number(this.power) < 0; + } + /** + * Make a duplicate of a symbol by copying a predefined list of items. The name 'copy' would probably be a more + * appropriate name. to a new symbol + * + * @param {NerdamerSymbolType} [c] + * @returns {NerdamerSymbolType} + */ + clone(c = undefined) { + /** @type {NerdamerSymbolType} */ + const self = this; + const clone = c || new NerdamerSymbol(0); + // List of properties excluding power as this may be a symbol and would also need to be a clone. + const properties = [ + 'value', + 'group', + 'length', + 'previousGroup', + 'imaginary', + 'fname', + 'args', + 'isInfinity', + 'scientific', + ]; + const l = properties.length; + let i; + if (self.symbols) { + clone.symbols = {}; + for (const x in self.symbols) { + if (!Object.hasOwn(self.symbols, x)) { + continue; + } + clone.symbols[x] = self.symbols[x].clone(); + } + } + + for (i = 0; i < l; i++) { + if (self[properties[i]] !== undefined) { + clone[properties[i]] = self[properties[i]]; + } + } + + clone.power = self.power.clone(); + clone.multiplier = self.multiplier.clone(); + // Add back the flag to track if this symbol is a conversion symbol + // These properties may be added by external modules (like units) + // Use type assertion to access dynamically added properties + const selfAny = /** @type {Record} */ (/** @type {unknown} */ (self)); + const cloneAny = /** @type {Record} */ (/** @type {unknown} */ (clone)); + if (selfAny.isConversion) { + cloneAny.isConversion = selfAny.isConversion; + } + + if (selfAny.isUnit) { + cloneAny.isUnit = selfAny.isUnit; + } + + return clone; + } + /** + * Converts a symbol multiplier to one. + * + * @param {boolean} [keepSign] Keep the multiplier as negative if the multiplier is negative and keepSign is true + */ + toUnitMultiplier(keepSign = false) { + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + this.multiplier.num = new NerdamerSymbolDeps.bigInt(this.multiplier.num.isNegative() && keepSign ? -1 : 1); + // @ts-expect-error - bigInt supports constructor at runtime but not in TypeScript types + this.multiplier.den = new NerdamerSymbolDeps.bigInt(1); + return this; + } + /** Converts a NerdamerSymbol's power to one. */ + toLinear() { + // Do nothing if it's already linear + if (this.power.equals(1)) { + return this; + } + this.setPower(new Frac(1)); + return this; + } + /** + * Iterates over all the sub-symbols. If no sub-symbols exist then it's called on itself + * + * @param {Function} fn + * @param {boolean} [deep] If true it will itterate over the sub-symbols their symbols as well + */ + each(fn, deep = false) { + if (this.symbols) { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const sym = this.symbols[x]; + if (sym.group === NerdamerSymbolDeps.PL && deep) { + for (const y in sym.symbols) { + if (!Object.hasOwn(sym.symbols, y)) { + continue; + } + fn.call(x, sym.symbols[y], y); + } + } else { + fn.call(this, sym, x); + } + } + } else { + fn.call(this, this, this.value); + } + } + /** + * A numeric value to be returned for Javascript. It will try to return a number as far a possible but in case of a + * pure symbolic symbol it will just return its text representation. When Settings.USE_BIG is true, may return a + * Decimal instance for numeric symbols. + * + * @returns {string | number | DecimalType} + */ + valueOf() { + if (this.group === NerdamerSymbolDeps.N) { + return this.multiplier.valueOf(); + } + if (this.power.equals(0)) { + return 1; + } + if (this.multiplier.equals(0)) { + return 0; + } + return NerdamerSymbolDeps.text(this, 'decimals'); + } + /** + * Checks to see if a symbols has a particular variable within it. Pass in true as second argument to include the + * power of exponentials which aren't check by default. + * + * @example + * let s = _.parse('x+y+z'); + * s.contains('y'); + * //returns true + * + * @param {string | NerdamerSymbolType} variable + * @param {boolean} [all] + * @returns {boolean} + */ + contains(variable, all = false) { + // Contains expects a string + variable = String(variable); + const g = this.group; + if (this.value === variable) { + return true; + } + if (this.symbols) { + for (const x in this.symbols) { + if (this.symbols[x].contains(variable, all)) { + return true; + } + } + } + if (g === NerdamerSymbolDeps.FN || this.previousGroup === NerdamerSymbolDeps.FN) { + for (let i = 0; i < this.args.length; i++) { + if (this.args[i].contains(variable, all)) { + return true; + } + } + } + + if (g === NerdamerSymbolDeps.EX) { + // Exit only if it does + if (all && /** @type {NerdamerSymbolType} */ (this.power).contains(variable, all)) { + return true; + } + if (this.value === variable) { + return true; + } + } + + return this.value === variable; + } + /** Negates a symbols */ + negate() { + this.multiplier.negate(); + if (this.group === NerdamerSymbolDeps.CP || this.group === NerdamerSymbolDeps.PL) { + this.distributeMultiplier(); + } + return this; + } + /** + * Inverts a symbol + * + * @param {boolean} [powerOnly] + * @param {boolean} [all] + */ + invert(powerOnly = false, all = false) { + // Invert the multiplier + if (!powerOnly) { + this.multiplier = this.multiplier.invert(); + } + // Invert the rest + if (NerdamerSymbolDeps.isSymbol(this.power)) { + this.power.negate(); + } else if (this.group === NerdamerSymbolDeps.CB && all) { + this.each(x => x.invert()); + } else if (this.power && this.group !== NerdamerSymbolDeps.N) { + this.power.negate(); + } + return this; + } + /** + * Symbols of group CP or PL may have the multiplier being carried by the top level symbol at any given time e.g. + * 2*(x+y+z). This is convenient in many cases, however in some cases the multiplier needs to be carried + * individually e.g. 2_x+2_y+2*z. This method distributes the multiplier over the entire symbol + * + * @param {boolean} [all] + */ + distributeMultiplier(all = false) { + // In CP/PL groups (not EX), power is Frac + const isOne = all ? /** @type {FracType} */ (this.power).absEquals(1) : this.power.equals(1); + if (this.symbols && isOne && this.group !== NerdamerSymbolDeps.CB && !this.multiplier.equals(1)) { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const s = this.symbols[x]; + s.multiplier = s.multiplier.multiply(this.multiplier); + s.distributeMultiplier(); + } + this.toUnitMultiplier(); + } + + return this; + } + /** This method expands the exponent over the entire symbol just like distributeMultiplier */ + distributeExponent() { + const { _ } = NerdamerSymbolDeps; + if (!this.power.equals(1)) { + const p = this.power; + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const s = this.symbols[x]; + if (s.group === NerdamerSymbolDeps.EX) { + s.power = _.multiply(s.power, new NerdamerSymbol(p)); + } else if (NerdamerSymbolDeps.isSymbol(this.symbols[x].power)) { + this.symbols[x].power = _.multiply(this.symbols[x].power, new NerdamerSymbol(p)); + } else { + this.symbols[x].power = this.symbols[x].power.multiply(p); + } + } + this.toLinear(); + } + return this; + } + /** + * This method will attempt to up-convert or down-convert one symbol from one group to another. Not all symbols are + * convertible from one group to another however. In that case the symbol will remain unchanged. + * + * @param {number} group + * @param {boolean} [imaginary] + */ + convert(group, imaginary = undefined) { + if (group > NerdamerSymbolDeps.FN) { + // Make a clone of this symbol; + const cp = this.clone(); + + // Attach a symbols object and upgrade the group + this.symbols = {}; + + if (group === NerdamerSymbolDeps.CB) { + // Symbol of group CB hold symbols bound together through multiplication + // because of commutativity this multiplier can technically be anywhere within the group + // to keep track of it however it's easier to always have the top level carry it + cp.toUnitMultiplier(); + } else { + // Reset the symbol + this.toUnitMultiplier(); + } + + if (this.group === NerdamerSymbolDeps.FN) { + cp.args = this.args; + delete this.args; + delete this.fname; + } + + // The symbol may originate from the symbol i but this property no longer holds true + // after copying + if (this.isImgSymbol) { + delete this.isImgSymbol; + } + + this.toLinear(); + // Attach a clone of this symbol to the symbols object using its proper key + this.symbols[cp.keyForGroup(group)] = cp; + this.group = group; + // Objects by default don't have a length property. However, in order to keep track of the number + // of sub-symbols we have to impliment our own. + this.length = 1; + } else if (group === NerdamerSymbolDeps.EX) { + // 1^x is just one so check and make sure + if (!(this.group === NerdamerSymbolDeps.N && this.multiplier.equals(1))) { + if (this.group !== NerdamerSymbolDeps.EX) { + this.previousGroup = this.group; + } + if (this.group === NerdamerSymbolDeps.N) { + this.value = this.multiplier.num.toString(); + this.toUnitMultiplier(); + } + // Update the hash to reflect the accurate hash + else { + this.value = NerdamerSymbolDeps.text(this, 'hash'); + } + + this.group = NerdamerSymbolDeps.EX; + } + } else if (group === NerdamerSymbolDeps.N) { + const m = this.multiplier.toDecimal(); + this.symbols &&= undefined; + new NerdamerSymbol( + this.group === NerdamerSymbolDeps.P ? Number(m) * Number(this.value) ** Number(this.power) : m + ).clone(this); + } else if (group === NerdamerSymbolDeps.P && this.group === NerdamerSymbolDeps.N) { + this.value = imaginary + ? this.multiplier.num.toString() + : String(Math.abs(Number(this.multiplier.num.toString()))); + this.toUnitMultiplier(!imaginary); + this.group = NerdamerSymbolDeps.P; + } + return this; + } + /** + * This method is one of the principal methods to make it all possible. It performs cleanup and prep operations + * whenever a symbols is inserted. If the symbols results in a 1 in a CB (multiplication) group for instance it will + * remove the redundant symbol. Similarly in a symbol of group PL or CP (symbols glued by multiplication) it will + * remove any dangling zeroes from the symbol. It will also up-convert or down-convert a symbol if it detects that + * it's incorrectly grouped. It should be noted that this method is not called directly but rather by the 'attach' + * method for addition groups and the 'combine' method for multiplication groups. + * + * @param {NerdamerSymbolType} symbol + * @param {string} action + */ + insert(symbol, action) { + const { _ } = NerdamerSymbolDeps; + // This check can be removed but saves a lot of aggravation when trying to hunt down + // a bug. If left, you will instantly know that the error can only be between 2 symbols. + if (!NerdamerSymbolDeps.isSymbol(symbol)) { + err(`Object ${symbol} is not of type NerdamerSymbol!`); + } + if (this.symbols) { + const { group } = this; + if (group > NerdamerSymbolDeps.FN) { + const key = symbol.keyForGroup(group); + const existing = key in this.symbols ? this.symbols[key] : false; // Check if there's already a symbol there + if (action === 'add') { + const hash = key; + if (existing) { + // Add them together using the parser + this.symbols[hash] = _.add(existing, symbol); + // If the addition resulted in a zero multiplier remove it + if (this.symbols[hash].multiplier.equals(0)) { + delete this.symbols[hash]; + this.length--; + + if (this.length === 0) { + this.convert(NerdamerSymbolDeps.N); + this.multiplier = new Frac(0); + } + } + } else { + this.symbols[key] = symbol; + this.length++; + } + } else { + // Check if this is of group P and unwrap before inserting + if (symbol.group === NerdamerSymbolDeps.P && isInt(symbol.power)) { + symbol.convert(NerdamerSymbolDeps.N); + } + + // Transfer the multiplier to the upper symbol but only if the symbol numeric + if (symbol.group === NerdamerSymbolDeps.EX) { + symbol.parens = symbol.multiplier.lessThan(0); + this.multiplier = this.multiplier.multiply(symbol.multiplier.clone().abs()); + symbol.toUnitMultiplier(true); + } else { + this.multiplier = this.multiplier.multiply(symbol.multiplier); + symbol.toUnitMultiplier(); + } + + if (existing) { + // Remove because the symbol may have changed + symbol = /** @type {NerdamerSymbolType} */ ( + _.multiply(/** @type {NerdamerSymbolType} */ (remove(this.symbols, key)), symbol) + ); + if (symbol.isConstant()) { + this.multiplier = this.multiplier.multiply(symbol.multiplier); + symbol = new NerdamerSymbol(1); // The dirty work gets done down the line when it detects 1 + } + + this.length--; + // Clean up + } + + // Don't insert the symbol if it's 1 + if (!symbol.isOne(true)) { + this.symbols[key] = symbol; + this.length++; + } else if (symbol.multiplier.lessThan(0)) { + this.negate(); // Put back the sign + } + } + + // Clean up + if (this.length === 0) { + this.convert(NerdamerSymbolDeps.N); + } + // Update the hash + if (this.group === NerdamerSymbolDeps.CP || this.group === NerdamerSymbolDeps.CB) { + this.updateHash(); + } + } + } + + return this; + } + /** The insert method for addition */ + attach(symbol) { + if (isArray(symbol)) { + for (let i = 0; i < symbol.length; i++) { + this.insert(/** @type {NerdamerSymbolType} */ (symbol[i]), 'add'); + } + return this; + } + return this.insert(symbol, 'add'); + } + /** The insert method for multiplication */ + combine(symbol) { + if (isArray(symbol)) { + for (let i = 0; i < symbol.length; i++) { + this.insert(/** @type {NerdamerSymbolType} */ (symbol[i]), 'multiply'); + } + return this; + } + return this.insert(symbol, 'multiply'); + } + /** + * This method should be called after any major "surgery" on a symbol. It updates the hash of the symbol for example + * if the fname of a function has changed it will update the hash of the symbol. + */ + updateHash() { + if (this.group === NerdamerSymbolDeps.N) { + return; + } + + if (this.group === NerdamerSymbolDeps.FN) { + let contents = ''; + const { args } = this; + const isParens = this.fname === NerdamerSymbolDeps.PARENTHESIS; + for (let i = 0; i < args.length; i++) { + contents += (i === 0 ? '' : ',') + NerdamerSymbolDeps.text(args[i]); + } + const fnName = isParens ? '' : this.fname; + this.value = fnName + (isParens ? contents : inBrackets(contents)); + } else if (!(this.group === NerdamerSymbolDeps.S || this.group === NerdamerSymbolDeps.PL)) { + this.value = NerdamerSymbolDeps.text(this, 'hash'); + } + } + /** + * This function defines how every group in stored within a group of higher order think of it as the switchboard for + * the library. It defines the hashes for symbols. + * + * @param {number} group + */ + keyForGroup(group) { + const g = this.group; + let key; + + if (g === NerdamerSymbolDeps.N) { + key = this.value; + } else if (g === NerdamerSymbolDeps.S || g === NerdamerSymbolDeps.P) { + if (group === NerdamerSymbolDeps.PL) { + // In S/P groups, power is Frac + key = /** @type {FracType} */ (this.power).toDecimal(); + } else { + key = this.value; + } + } else if (g === NerdamerSymbolDeps.FN) { + if (group === NerdamerSymbolDeps.PL) { + // In FN group, power is Frac + key = /** @type {FracType} */ (this.power).toDecimal(); + } else { + key = NerdamerSymbolDeps.text(this, 'hash'); + } + } else if (g === NerdamerSymbolDeps.PL) { + // If the order is reversed then we'll assume multiplication + // TODO: possible future dilemma + if (group === NerdamerSymbolDeps.CB) { + key = NerdamerSymbolDeps.text(this, 'hash'); + } else if (group === NerdamerSymbolDeps.CP) { + if (this.power.equals(1)) { + key = this.value; + } else { + key = + inBrackets(NerdamerSymbolDeps.text(this, 'hash')) + + Settings.POWER_OPERATOR + + // In PL group, power is Frac + /** @type {FracType} */ (this.power).toDecimal(); + } + } else if (group === NerdamerSymbolDeps.PL) { + key = this.power.toString(); + } else { + key = this.value; + } + return key; + } else if (g === NerdamerSymbolDeps.CP) { + if (group === NerdamerSymbolDeps.CP) { + key = NerdamerSymbolDeps.text(this, 'hash'); + } + if (group === NerdamerSymbolDeps.PL) { + // In CP group, power is Frac + key = /** @type {FracType} */ (this.power).toDecimal(); + } else { + key = this.value; + } + } else if (g === NerdamerSymbolDeps.CB) { + if (group === NerdamerSymbolDeps.PL) { + // In CB group, power is Frac + key = /** @type {FracType} */ (this.power).toDecimal(); + } else { + key = NerdamerSymbolDeps.text(this, 'hash'); + } + } else if (g === NerdamerSymbolDeps.EX) { + if (group === NerdamerSymbolDeps.PL) { + // In EX group, power is NerdamerSymbol, use text() + key = NerdamerSymbolDeps.text(this.power); + } else { + key = NerdamerSymbolDeps.text(this, 'hash'); + } + } + + return key; + } + /** + * Symbols are typically stored in an object which works fine for most cases but presents a problem when the order + * of the symbols makes a difference. This function simply collects all the symbols and returns them as an array. If + * a function is supplied then that function is called on every symbol contained within the object. + * + * @param {(symbol: NerdamerSymbolType, opt?: string) => unknown} [fn] + * @param {string} [opt] + * @param {SortFn} [sortFn] + * @param {boolean} [expandSymbol] + * @returns {Array} + */ + collectSymbols(fn, opt, sortFn, expandSymbol) { + let collected = []; + if (this.symbols) { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const symbol = this.symbols[x]; + if ( + expandSymbol && + (symbol.group === NerdamerSymbolDeps.PL || symbol.group === NerdamerSymbolDeps.CP) + ) { + collected = collected.concat(symbol.collectSymbols()); + } else { + collected.push(fn ? fn(symbol, opt) : symbol); + } + } + } else { + collected.push(this); + } + if (sortFn === null) { + sortFn = undefined; + } // WTF Firefox? Seriously? + + return collected.sort(sortFn); // Sort hopefully gives us some sort of consistency + } + + /** + * CollectSymbols but only for summands + * + * @param {(symbol: NerdamerSymbolType, opt?: string) => unknown} [fn] + * @param {string} [opt] + * @param {SortFn} [sortFn] + * @param {boolean} [expandSymbol] + * @returns {Array} + */ + collectSummandSymbols(fn, opt, sortFn, expandSymbol) { + let collected = []; + if (!this.symbols || this.group === NerdamerSymbolDeps.CB) { + collected.push(this); + } else { + for (const x in this.symbols) { + if (!Object.hasOwn(this.symbols, x)) { + continue; + } + const symbol = this.symbols[x]; + if ( + expandSymbol && + (symbol.group === NerdamerSymbolDeps.PL || symbol.group === NerdamerSymbolDeps.CP) + ) { + collected = collected.concat(symbol.collectSymbols()); + } else { + collected.push(fn ? fn(symbol, opt) : symbol); + } + } + } + if (sortFn === null) { + sortFn = undefined; + } // WTF Firefox? Seriously? + + return collected.sort(sortFn); // Sort hopefully gives us some sort of consistency + } + /** + * Returns the latex representation of the symbol + * + * @param {string} option + * @returns {string} + */ + latex(option) { + return LaTeX.latex(this, option); + } + /** + * Returns the text representation of a symbol + * + * @param {string} [option] + * @returns {string} + */ + text(option = undefined) { + return NerdamerSymbolDeps.text(this, option); + } + /** + * Checks if the function evaluates to 1. e.g. x^0 or 1 :) + * + * @param {boolean} [abs] Compares the absolute value + */ + isOne(abs = false) { + const f = abs ? 'absEquals' : 'equals'; + if (this.group === NerdamerSymbolDeps.N) { + return this.multiplier[f](1); + } + return this.power.equals(0); + } + isComposite() { + const g = this.group; + const pg = this.previousGroup; + return ( + g === NerdamerSymbolDeps.CP || + g === NerdamerSymbolDeps.PL || + pg === NerdamerSymbolDeps.PL || + pg === NerdamerSymbolDeps.CP + ); + } + isCombination() { + const g = this.group; + const pg = this.previousGroup; + return g === NerdamerSymbolDeps.CB || pg === NerdamerSymbolDeps.CB; + } + lessThan(n) { + return this.multiplier.lessThan(n); + } + greaterThan(n) { + if (!NerdamerSymbolDeps.isSymbol(n)) { + n = new NerdamerSymbol(n); + } + + // We can't tell for sure if a is greater than be if they're not both numbers + if (!this.isConstant(true) || !n.isConstant(true)) { + return false; + } + + return this.multiplier.greaterThan(n.multiplier); + } + /** + * Get's the denominator of the symbol if the symbol is of class CB (multiplication) with other classes the symbol + * is either the denominator or not. Take x^-1+x^-2. If the symbol was to be mixed such as x+x^-2 then the symbol + * doesn't have have an exclusive denominator and has to be found by looking at the actual symbols themselves. + * + * @returns {NerdamerSymbolType} + */ + getDenom() { + const { _ } = NerdamerSymbolDeps; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let retval; + /** @type {NerdamerSymbolType} */ + let symbol; + symbol = /** @type {NerdamerSymbolType} */ (this.clone()); + // E.g. 1/(x*(x+1)) + if (this.group === NerdamerSymbolDeps.CB && this.power.lessThan(0)) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + + // If the symbol already is the denominator... DONE!!! + if ( + symbol.power.lessThan(0) || + (symbol.group === NerdamerSymbolDeps.EX && + /** @type {NerdamerSymbolType} */ (symbol.power).multiplier.lessThan(0)) + ) { + const d = _.parse(symbol.multiplier.den); + retval = symbol.toUnitMultiplier(); + retval.power.negate(); + retval = _.multiply(d, retval); // Put back the coeff + } else if (symbol.group === NerdamerSymbolDeps.CB) { + retval = _.parse(symbol.multiplier.den); + for (const x in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, x)) { + continue; + } + const s = symbol.symbols[x]; + if ( + Number(s.power) < 0 || + (s.group === NerdamerSymbolDeps.EX && + /** @type {NerdamerSymbolType} */ (s.power).multiplier.lessThan(0)) + ) { + retval = _.multiply( + /** @type {NerdamerSymbolType} */ (retval), + /** @type {NerdamerSymbolType} */ (symbol.symbols[x].clone().invert()) + ); + } + } + } else { + retval = _.parse(symbol.multiplier.den); + } + return /** @type {NerdamerSymbolType} */ (retval); + } + /** @returns {NerdamerSymbolType} */ + getNum() { + const { _ } = NerdamerSymbolDeps; + /** @type {NerdamerSymbolType | VectorType | MatrixType} */ + let retval; + /** @type {NerdamerSymbolType} */ + let symbol; + symbol = /** @type {NerdamerSymbolType} */ (this.clone()); + // E.g. 1/(x*(x+1)) + if (symbol.group === NerdamerSymbolDeps.CB && symbol.power.lessThan(0)) { + symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol)); + } + // If the symbol already is the denominator... DONE!!! + if ( + (symbol.power.greaterThan(0) && symbol.group !== NerdamerSymbolDeps.CB) || + (symbol.group === NerdamerSymbolDeps.EX && + /** @type {NerdamerSymbolType} */ (symbol.power).multiplier.greaterThan(0)) + ) { + retval = _.multiply(_.parse(symbol.multiplier.num), symbol.toUnitMultiplier()); + } else if (symbol.group === NerdamerSymbolDeps.CB) { + retval = _.parse(symbol.multiplier.num); + symbol.each(x => { + if ( + Number(x.power) > 0 || + (x.group === NerdamerSymbolDeps.EX && + /** @type {NerdamerSymbolType} */ (x.power).multiplier.greaterThan(0)) + ) { + retval = _.multiply( + /** @type {NerdamerSymbolType} */ (retval), + /** @type {NerdamerSymbolType} */ (x.clone()) + ); + } + }); + } + // Else if(symbol.group === NerdamerSymbolDeps.EX && this.previousGroup === NerdamerSymbolDeps.S) { + // retval = _.multiply(_.parse(symbol.multiplier.num), symbol.toUnitMultiplier()); + // } + else { + retval = _.parse(symbol.multiplier.num); + } + return /** @type {NerdamerSymbolType} */ (retval); + } + toString() { + return this.text(); + } +} + +// Assign NerdamerSymbol to CoreDeps immediately +CoreDeps.classes.NerdamerSymbol = NerdamerSymbol; + +// Parser Class ===================================================================== +// The Parser is the core mathematical expression parser for nerdamer. It uses a +// modified Shunting-yard algorithm (http://en.wikipedia.org/wiki/Shunting-yard_algorithm). +// +// DEPENDENCY INJECTION: +// The Parser relies on values that are only available inside the IIFE. These are +// injected via ParserDeps, which the IIFE populates before Parser instantiation: +// +// 1. Symbol Group Constants (N, P, S, EX, FN, PL, CB, CP) - Symbol type classification +// 2. Function Name Constants (SQRT, ABS, FACTORIAL, DOUBLEFACTORIAL, PARENTHESIS) +// 3. bigDec - BigDecimal library for high-precision calculations +// 4. PRIMES - Array of prime numbers for factorization +// 5. VARS - Object storing user-defined variables + +/** + * The Parser class - core mathematical expression parser for nerdamer. + * + * This class is defined at module scope but instantiated inside the IIFE. The Parser destructures its dependencies from + * ParserDeps at construction time, which the IIFE has already populated with the correct values. + * + * @implements {ParserType} + */ +class Parser { + constructor() { + // Destructure dependencies from ParserDeps (populated by IIFE before instantiation) + const { N, P, S, EX, FN, PL, CB, CP } = ParserDeps; + const { SQRT, ABS, FACTORIAL, DOUBLEFACTORIAL, PARENTHESIS } = ParserDeps; + const { bigDec, PRIMES, VARS } = ParserDeps; + + // Local reference to this parser instance for use in nested functions + /** @type {ParserType} */ + const _parser = this; + const _ = _parser; + const bin = {}; + const preprocessors = { names: [], actions: [] }; + + // Parser.classes =============================================================== + /** Slice class for representing array slices */ + class Slice { + /** @type {NerdamerSymbolType | number} */ + upper; + /** @type {NerdamerSymbolType | number} */ + lower; + + /** + * @param {NerdamerSymbolType | number} upper - Start of slice + * @param {NerdamerSymbolType | number} lower - End of slice + */ + constructor(upper, lower) { + this.upper = upper; + this.lower = lower; + } + + isConstant() { + const u = /** @type {NerdamerSymbolType} */ (this.upper); + const l = /** @type {NerdamerSymbolType} */ (this.lower); + return u.isConstant() && l.isConstant(); + } + + // Using 'getText' to avoid shadowing the outer 'text' function + text() { + return `${text(/** @type {NerdamerSymbolType} */ (this.upper))}:${text(/** @type {NerdamerSymbolType} */ (this.lower))}`; + } + } + + /** Token class for representing parser tokens */ + class Token { + static OPERATOR = 'OPERATOR'; + static VARIABLE_OR_LITERAL = 'VARIABLE_OR_LITERAL'; + static FUNCTION = 'FUNCTION'; + static UNIT = 'UNIT'; + static KEYWORD = 'KEYWORD'; + static MAX_PRECEDENCE = 999; + + /** + * @param {string} node - Token value + * @param {string} nodeType - Token type + * @param {number} [column] - Column position + */ + constructor(node, nodeType, column) { + this.type = nodeType; + this.value = node; + if (column !== undefined) { + this.column = column + 1; + } + if (nodeType === Token.OPERATOR) { + // Copy everything over from the operator + // eslint-disable-next-line no-use-before-define -- operators is defined later but this function is only called after + const operator = operators[node]; + for (const x in operator) { + if (!Object.hasOwn(operator, x)) { + continue; + } + this[x] = operator[x]; + } + } else if (nodeType === Token.FUNCTION) { + this.precedence = Token.MAX_PRECEDENCE; // Leave enough room + this.leftAssoc = false; + } + } + + /** @this {TokenType} */ + toString() { + if (this.is_prefix) { + return `\`${this.value}`; + } + return this.value; + } + } + + // Create link to classes + this.classes = { + Collection, + Slice, + Token, + }; + // Parser.modules =============================================================== + // object for functions which handle complex number + const complex = { + prec: undefined, + cos(r, i) { + const re = _.parse(String(Math.cos(r) * Math.cosh(i))); + const im = _.parse(String(Math.sin(r) * Math.sinh(i))); + return _.subtract(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + sin(r, i) { + const re = _.parse(String(Math.sin(r) * Math.cosh(i))); + const im = _.parse(String(Math.cos(r) * Math.sinh(i))); + return _.subtract(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + tan(r, i) { + const re = _.parse(String(Math.sin(2 * r) / (Math.cos(2 * r) + Math.cosh(2 * i)))); + const im = _.parse(String(Math.sinh(2 * i) / (Math.cos(2 * r) + Math.cosh(2 * i)))); + return _.add(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + sec(r, i) { + const t = this.removeDen(this.cos(r, i)); + return _.subtract(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + csc(r, i) { + const t = this.removeDen(this.sin(r, i)); + return _.add(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + cot(r, i) { + const t = this.removeDen(this.tan(r, i)); + return _.subtract(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + acos(r, i) { + const symbol = this.fromArray([r, i]); + const squared = _.pow(symbol.clone(), new NerdamerSymbol(2)); + const sq = _.expand(squared); // Z*z + const a = _.multiply(sqrt(_.subtract(new NerdamerSymbol(1), sq)), NerdamerSymbol.imaginary()); + const b = _.expand(_.add(symbol.clone(), a)); + const c = log(b); + return _.expand(_.multiply(NerdamerSymbol.imaginary().negate(), c)); + }, + asin(r, i) { + return _.subtract(_.parse('pi/2'), this.acos(r, i)); + }, + atan(r, i) { + // Handle i and -i + if (r.equals(0) && (i.equals(1) || i.equals(-1))) { + // Just copy Wolfram Alpha for now. The parenthesis + return _.parse(`${NerdamerSymbol.infinity()}*${Settings.IMAGINARY}*${i}`); + } + const symbol = complex.fromArray([r, i]); + const a = _.expand(_.multiply(NerdamerSymbol.imaginary(), symbol.clone())); + const b = log(_.expand(_.subtract(new NerdamerSymbol(1), a.clone()))); + const c = log(_.expand(_.add(new NerdamerSymbol(1), a.clone()))); + return _.expand( + _.multiply(_.divide(NerdamerSymbol.imaginary(), new NerdamerSymbol(2)), _.subtract(b, c)) + ); + }, + asec(r, i) { + const d = this.removeDen([r, i]); + d[1].negate(); + return this.acos(...d); + }, + acsc(r, i) { + const d = this.removeDen([r, i]); + d[1].negate(); + return this.asin(...d); + }, + acot(r, i) { + const d = this.removeDen([r, i]); + d[1].negate(); + return this.atan(...d); + }, + // Hyperbolic trig + cosh(r, i) { + const re = _.parse(String(Math.cosh(r) * Math.cos(i))); + const im = _.parse(String(Math.sinh(r) * Math.sin(i))); + return _.add(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + sinh(r, i) { + const re = _.parse(String(Math.sinh(r) * Math.cos(i))); + const im = _.parse(String(Math.cosh(r) * Math.sin(i))); + return _.add(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + tanh(r, i) { + const re = _.parse(String(Math.sinh(2 * r) / (Math.cos(2 * i) + Math.cosh(2 * r)))); + const im = _.parse(String(Math.sin(2 * i) / (Math.cos(2 * i) + Math.cosh(2 * r)))); + return _.subtract(re, _.multiply(im, NerdamerSymbol.imaginary())); + }, + sech(r, i) { + const t = this.removeDen(this.cosh(r, i)); + return _.subtract(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + csch(r, i) { + const t = this.removeDen(this.sinh(r, i)); + return _.subtract(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + coth(r, i) { + const t = this.removeDen(this.tanh(r, i)); + return _.add(t[0], _.multiply(t[1], NerdamerSymbol.imaginary())); + }, + acosh(r, i) { + const z = this.fromArray([r, i]); + const a = sqrt(_.add(z.clone(), new NerdamerSymbol(1))); + const b = sqrt(_.subtract(z.clone(), new NerdamerSymbol(1))); + return _.expand(log(_.add(z, _.expand(_.multiply(a, b))))); + }, + asinh(r, i) { + const z = this.fromArray([r, i]); + const a = sqrt(_.add(new NerdamerSymbol(1), _.expand(_.pow(z.clone(), new NerdamerSymbol(2))))); + return _.expand(log(_.add(z, a))); + }, + atanh(r, i) { + const z = this.fromArray([r, i]); + const a = log(_.add(z.clone(), new NerdamerSymbol(1))); + const b = log(_.subtract(new NerdamerSymbol(1), z)); + return _.expand(_.divide(_.subtract(a, b), new NerdamerSymbol(2))); + }, + asech(r, i) { + const t = this.removeDen([r, i]); + t[1].negate(); + return this.acosh(...t); + }, + acsch(r, i) { + const t = this.removeDen([r, i]); + t[1].negate(); + return this.asinh(...t); + }, + acoth(r, i) { + const t = this.removeDen([r, i]); + t[1].negate(); + return this.atanh(...t); + }, + sqrt(symbol) { + const re = symbol.realpart(); + const im = symbol.imagpart(); + const h = NerdamerSymbol.hyp(re, im); + const a = _.add(re.clone(), h); + const d = sqrt(_.multiply(new NerdamerSymbol(2), a.clone())); + return _.add(_.divide(a.clone(), d.clone()), _.multiply(_.divide(im, d), NerdamerSymbol.imaginary())); + }, + log(r, i) { + const re = log(NerdamerSymbol.hyp(r, i)); + const phi = Settings.USE_BIG + ? new NerdamerSymbol(bigDec.atan2(i.multiplier.toDecimal(), r.multiplier.toDecimal())) + : Math.atan2(i, r); + const im = _.parse(phi); + return _.add(re, _.multiply(NerdamerSymbol.imaginary(), im)); + }, + erf(symbol, _n) { + // Do nothing for now. Revisit this in the future. + return _.symfunction('erf', [symbol]); + + // N = n || 30; + + // let f = function (R, I) { + // return block('PARSE2NUMBER', function () { + // let retval = new NerdamerSymbol(0); + // for(let i = 0; i < n; i++) { + // let a, b; + // a = _.parse(bigDec.exp(bigDec(i).toPower(2).neg().dividedBy(bigDec(n).pow(2).plus(bigDec(R).toPower(2).times(4))))); + // b = _.parse(format('2*({1})-e^(-(2*{0}*{1}*{2}))*(2*{1}*cosh({2}*{3})-{0}*{3}*sinh({3}*{2}))', Settings.IMAGINARY, R, I, i)); + // retval = _.add(retval, _.multiply(a, b)); + // } + // return _.multiply(retval, new NerdamerSymbol(2)); + // }, true); + // }; + // let re, im, a, b, c, k; + // re = symbol.realpart(); + // im = symbol.imagpart(); + + // k = _.parse(format('(e^(-{0}^2))/pi', re)); + // a = _.parse(format('(1-e^(-(2*{0}*{1}*{2})))/(2*{1})', Settings.IMAGINARY, re, im)); + // b = f(re.toString(), im.toString()); + + // return _.add(_.parse(Math2.erf(re.toString())), _.multiply(k, _.add(a, b))); + }, + removeDen(symbol) { + let r; + let i; + if (isArray(symbol)) { + r = symbol[0]; + i = symbol[1]; + } else { + r = symbol.realpart(); + i = symbol.imagpart(); + } + + const den = r ** 2 + i ** 2; + const re = _.parse(String(r / den)); + const im = _.parse(String(i / den)); + return [re, im]; + }, + fromArray(arr) { + return _.add(arr[0], _.multiply(NerdamerSymbol.imaginary(), arr[1])); + }, + evaluate(symbol, f) { + let re; + let im; + + const signVal = symbol.power.sign(); + // Remove it from under the denominator + symbol.power = symbol.power.abs(); + // Expand + if (symbol.power.greaterThan(1)) { + symbol = _.expand(symbol); + } + // Remove the denominator + if (signVal < 0) { + const d = this.removeDen(symbol); + re = d[0]; + im = d[1]; + } else { + re = symbol.realpart(); + im = symbol.imagpart(); + } + + if (re.isConstant('all') && im.isConstant('all')) { + return this[f](re, im); + } + + return _.symfunction(f, [symbol]); + }, + }; + // Object for functions which handle trig + const trig = (this.trig = { + // Container for trigonometric function + cos(symbol) { + if (symbol.equals('pi') && symbol.multiplier.den.equals(2)) { + return new NerdamerSymbol(0); + } + + if (Settings.PARSE2NUMBER) { + if (symbol.equals(new NerdamerSymbol(Settings.PI / 2))) { + return new NerdamerSymbol(0); + } + if (symbol.isConstant()) { + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.cos(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.cos(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'cos'); + } + } + if (symbol.equals(0)) { + return new NerdamerSymbol(1); + } + + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const m = symbol.multiplier.abs(); + symbol.multiplier = m; + + if (symbol.isPi() && symbol.isLinear()) { + // Return for 1 or -1 for multiples of pi + if (isInt(m)) { + retval = new NerdamerSymbol(even(m) ? 1 : -1); + } else { + const _n = Number(m.num); + const d = Number(m.den); + if (d === 2) { + retval = new NerdamerSymbol(0); + } else if (d === 3) { + retval = _.parse('1/2'); + c = true; + } else if (d === 4) { + retval = _.parse('1/sqrt(2)'); + c = true; + } else if (d === 6) { + retval = _.parse('sqrt(3)/2'); + c = true; + } else { + retval = _.symfunction('cos', [symbol]); + } + } + } + + if (c && (q === 2 || q === 3)) { + retval.negate(); + } + + retval ||= _.symfunction('cos', [symbol]); + + return retval; + }, + sin(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + if (Number(symbol.multiplier.toDecimal()) % Math.PI === 0) { + return new NerdamerSymbol(0); + } + + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.sin(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.sin(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'sin'); + } + } + + if (symbol.equals(0)) { + return new NerdamerSymbol(0); + } + + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const signVal = symbol.multiplier.sign(); + const m = symbol.multiplier.abs(); + symbol.multiplier = m; + if (symbol.equals('pi')) { + retval = new NerdamerSymbol(0); + } else if (symbol.isPi() && symbol.isLinear()) { + // Return for 0 for multiples of pi + if (isInt(m)) { + retval = new NerdamerSymbol(0); + } else { + const _n = m.num; + const d = m.den; + if (d.equals(2)) { + retval = new NerdamerSymbol(1); + c = true; + } else if (d.equals(3)) { + retval = _.parse('sqrt(3)/2'); + c = true; + } else if (d.equals(4)) { + retval = _.parse('1/sqrt(2)'); + c = true; + } else if (d.equals(6)) { + retval = _.parse('1/2'); + c = true; + } else { + retval = _.multiply(new NerdamerSymbol(signVal), _.symfunction('sin', [symbol])); + } + } + } + + retval ||= _.multiply(new NerdamerSymbol(signVal), _.symfunction('sin', [symbol])); + + if (c && (q === 3 || q === 4)) { + /** @type {NerdamerSymbolType} */ (retval).negate(); + } + + return retval; + }, + tan(symbol) { + if (Settings.PARSE2NUMBER) { + if (Number(symbol.multiplier.toDecimal()) % Math.PI === 0 && symbol.isLinear()) { + return new NerdamerSymbol(0); + } + if (symbol.isConstant()) { + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.tan(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.tan(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'tan'); + } + } + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const m = symbol.multiplier; + + symbol.multiplier = m; + + if (symbol.isPi() && symbol.isLinear()) { + // Return 0 for all multiples of pi + if (isInt(m)) { + retval = new NerdamerSymbol(0); + } else { + const _n = m.num; + const d = m.den; + if (d.equals(2)) { + throw new UndefinedError(`tan is undefined for ${symbol.toString()}`); + } else if (d.equals(3)) { + retval = _.parse('sqrt(3)'); + c = true; + } else if (d.equals(4)) { + retval = new NerdamerSymbol(1); + c = true; + } else if (d.equals(6)) { + retval = _.parse('1/sqrt(3)'); + c = true; + } else { + retval = _.symfunction('tan', [symbol]); + } + } + } + + retval ||= _.symfunction('tan', [symbol]); + + if (c && (q === 2 || q === 4)) { + retval.negate(); + } + + return retval; + }, + sec(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + if (Settings.USE_BIG) { + return new NerdamerSymbol( + new bigDec(1).dividedBy(bigDec.cos(symbol.multiplier.toDecimal())) + ); + } + + return new NerdamerSymbol(Math2.sec(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'sec'); + } + return _.parse(format('1/cos({0})', symbol)); + } + + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const m = symbol.multiplier.abs(); + symbol.multiplier = m; + + if (symbol.isPi() && symbol.isLinear()) { + // Return for 1 or -1 for multiples of pi + if (isInt(m)) { + retval = new NerdamerSymbol(even(m) ? 1 : -1); + } else { + const _n = m.num; + const d = m.den; + if (d.equals(2)) { + throw new UndefinedError(`sec is undefined for ${symbol.toString()}`); + } else if (d.equals(3)) { + retval = new NerdamerSymbol(2); + c = true; + } else if (d.equals(4)) { + retval = _.parse('sqrt(2)'); + c = true; + } else if (d.equals(6)) { + retval = _.parse('2/sqrt(3)'); + c = true; + } else { + retval = _.symfunction('sec', [symbol]); + } + } + } + + if (c && (q === 2 || q === 3)) { + retval.negate(); + } + + retval ||= _.symfunction('sec', [symbol]); + + return retval; + }, + csc(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + if (Settings.USE_BIG) { + return new NerdamerSymbol( + new bigDec(1).dividedBy(bigDec.sin(symbol.multiplier.toDecimal())) + ); + } + + return new NerdamerSymbol(Math2.csc(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'csc'); + } + return _.parse(format('1/sin({0})', symbol)); + } + + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const signVal = symbol.multiplier.sign(); + const m = symbol.multiplier.abs(); + + symbol.multiplier = m; + + if (symbol.isPi() && symbol.isLinear()) { + // Return for 0 for multiples of pi + if (isInt(m)) { + throw new UndefinedError(`csc is undefined for ${symbol.toString()}`); + } else { + const _n = m.num; + const d = m.den; + if (d.equals(2)) { + retval = new NerdamerSymbol(1); + c = true; + } else if (d.equals(3)) { + retval = _.parse('2/sqrt(3)'); + c = true; + } else if (d.equals(4)) { + retval = _.parse('sqrt(2)'); + c = true; + } else if (d.equals(6)) { + retval = new NerdamerSymbol(2); + c = true; + } else { + retval = _.multiply(new NerdamerSymbol(signVal), _.symfunction('csc', [symbol])); + } + } + } + + retval ||= _.multiply(new NerdamerSymbol(signVal), _.symfunction('csc', [symbol])); + + if (c && (q === 3 || q === 4)) { + /** @type {NerdamerSymbolType} */ (retval).negate(); + } + + return retval; + }, + cot(symbol) { + if (Settings.PARSE2NUMBER) { + if (Number(symbol.multiplier.toDecimal()) % (Math.PI / 2) === 0) { + return new NerdamerSymbol(0); + } + if (symbol.isConstant()) { + if (Settings.USE_BIG) { + return new NerdamerSymbol( + new bigDec(1).dividedBy(bigDec.tan(symbol.multiplier.toDecimal())) + ); + } + + return new NerdamerSymbol(Math2.cot(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'cot'); + } + return _.parse(format('1/tan({0})', symbol)); + } + let retval; + let c = false; + const q = getQuadrant(symbol.multiplier.toDecimal()); + const m = symbol.multiplier; + + symbol.multiplier = m; + + if (symbol.isPi() && symbol.isLinear()) { + // Return 0 for all multiples of pi + if (isInt(m)) { + throw new UndefinedError(`cot is undefined for ${symbol.toString()}`); + } else { + const _n = m.num; + const d = m.den; + if (d.equals(2)) { + retval = new NerdamerSymbol(0); + } else if (d.equals(3)) { + retval = _.parse('1/sqrt(3)'); + c = true; + } else if (d.equals(4)) { + retval = new NerdamerSymbol(1); + c = true; + } else if (d.equals(6)) { + retval = _.parse('sqrt(3)'); + c = true; + } else { + retval = _.symfunction('cot', [symbol]); + } + } + } + + retval ||= _.symfunction('cot', [symbol]); + + if (c && (q === 2 || q === 4)) { + retval.negate(); + } + + return retval; + }, + acos(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + // Handle values in the complex domain + if (symbol.gt(1) || symbol.lt(-1)) { + const x = symbol.toString(); + return expand(evaluate(`pi/2-asin(${x})`)); + } + // Handle big numbers + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.acos(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.acos(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'acos'); + } + } + return _.symfunction('acos', [symbol]); + }, + asin(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + // Handle values in the complex domain + if (symbol.gt(1) || symbol.lt(-1)) { + const i = Settings.IMAGINARY; + const x = symbol.multiplier.toDecimal(); + return expand(evaluate(`${i}*log(sqrt(1-${x}^2)-${i}*${x})`)); + } + // Handle big numbers + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.asin(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.asin(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'asin'); + } + } + return _.symfunction('asin', [symbol]); + }, + atan(symbol) { + let retval; + if (symbol.equals(0)) { + retval = new NerdamerSymbol(0); + } else if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + // Handle big numbers + if (Settings.USE_BIG) { + return new NerdamerSymbol(bigDec.atan(symbol.multiplier.toDecimal())); + } + + return new NerdamerSymbol(Math.atan(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'atan'); + } + return _.symfunction('atan', [symbol]); + } else if (symbol.equals(-1)) { + retval = _.parse('-pi/4'); + } else { + retval = _.symfunction('atan', [symbol]); + } + return retval; + }, + asec(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.equals(0)) { + throw new OutOfFunctionDomainError('Input is out of the domain of sec!'); + } + if (symbol.isConstant()) { + return trig.acos(symbol.invert()); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'asec'); + } + } + return _.symfunction('asec', [symbol]); + }, + acsc(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return trig.asin(symbol.invert()); + } + + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'acsc'); + } + } + return _.symfunction('acsc', [symbol]); + }, + acot(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return _.add(_.parse('pi/2'), trig.atan(symbol).negate()); + } + + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'acot'); + } + } + return _.symfunction('acot', [symbol]); + }, + atan2(a, b) { + if (a.equals(0) && b.equals(0)) { + throw new UndefinedError('atan2 is undefined for 0, 0'); + } + + if (Settings.PARSE2NUMBER && a.isConstant() && b.isConstant()) { + return new NerdamerSymbol(Math.atan2(a, b)); + } + return _.symfunction('atan2', [a, b]); + }, + }); + // Object for functions which handle hyperbolic trig + const trigh = (this.trigh = { + // Container for hyperbolic trig function + cosh(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.cosh(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'cosh'); + } + } + + return _.symfunction('cosh', [symbol]); + }, + sinh(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.sinh(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'sinh'); + } + } + + return _.symfunction('sinh', [symbol]); + }, + tanh(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.tanh(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'tanh'); + } + } + + return _.symfunction('tanh', [symbol]); + }, + sech(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.sech(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'sech'); + } + return _.parse(format('1/cosh({0})', symbol)); + } + + return _.symfunction('sech', [symbol]); + }, + csch(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.csch(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'csch'); + } + return _.parse(format('1/sinh({0})', symbol)); + } + + return _.symfunction('csch', [symbol]); + }, + coth(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math.coth(symbol.valueOf())); + } + if (symbol.isImaginary()) { + return complex.evaluate(symbol, 'coth'); + } + return _.parse(format('1/tanh({0})', symbol)); + } + + return _.symfunction('coth', [symbol]); + }, + acosh(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'acosh'); + } else if (Settings.PARSE2NUMBER) { + retval = evaluate(_.parse(format(`${Settings.LOG}(({0})+sqrt(({0})^2-1))`, symbol.toString()))); + } else { + retval = _.symfunction('acosh', [symbol]); + } + return retval; + }, + asinh(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'asinh'); + } else if (Settings.PARSE2NUMBER) { + retval = evaluate(_.parse(format(`${Settings.LOG}(({0})+sqrt(({0})^2+1))`, symbol.toString()))); + } else { + retval = _.symfunction('asinh', [symbol]); + } + return retval; + }, + atanh(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'atanh'); + } else if (Settings.PARSE2NUMBER) { + retval = evaluate(_.parse(format(`(1/2)*${Settings.LOG}((1+({0}))/(1-({0})))`, symbol.toString()))); + } else { + retval = _.symfunction('atanh', [symbol]); + } + return retval; + }, + asech(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'asech'); + } else if (Settings.PARSE2NUMBER) { + retval = evaluate( + log( + _.add( + symbol.clone().invert(), + sqrt(_.subtract(_.pow(symbol, new NerdamerSymbol(-2)), new NerdamerSymbol(1))) + ) + ) + ); + } else { + retval = _.symfunction('asech', [symbol]); + } + return retval; + }, + acsch(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'acsch'); + } else if (Settings.PARSE2NUMBER) { + retval = evaluate(_.parse(format(`${Settings.LOG}((1+sqrt(1+({0})^2))/({0}))`, symbol.toString()))); + } else { + retval = _.symfunction('acsch', [symbol]); + } + return retval; + }, + acoth(symbol) { + let retval; + if (Settings.PARSE2NUMBER && symbol.isImaginary()) { + retval = complex.evaluate(symbol, 'acoth'); + } else if (Settings.PARSE2NUMBER) { + if (symbol.equals(1)) { + retval = NerdamerSymbol.infinity(); + } else { + /** @type {NerdamerSymbolType} */ + const logResult = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ ( + log( + _.divide( + _.add(symbol.clone(), new NerdamerSymbol(1)), + _.subtract(symbol.clone(), new NerdamerSymbol(1)) + ) + ) + ) + ); + retval = evaluate( + /** @type {NerdamerSymbolType} */ (_.divide(logResult, new NerdamerSymbol(2))) + ); + } + } else { + retval = _.symfunction('acoth', [symbol]); + } + return retval; + }, + }); + // List of supported units + this.units = {}; + // List all the supported operators + const operators = { + '\\': { + precedence: 8, + operator: '\\', + action: 'slash', + prefix: true, + postfix: false, + leftAssoc: true, + operation(e) { + return e; // Bypass the slash + }, + }, + '!!': { + precedence: 7, + operator: '!!', + action: 'dfactorial', + prefix: false, + postfix: true, + leftAssoc: true, + operation(e) { + return _.symfunction(Settings.DOUBLEFACTORIAL, [e]); // Wrap it in a factorial function + }, + }, + '!': { + precedence: 7, + operator: '!', + action: 'factorial', + prefix: false, + postfix: true, + leftAssoc: true, + operation(e) { + return _factorial(e); // Wrap it in a factorial function + }, + }, + '^': { + precedence: 6, + operator: '^', + action: 'pow', + prefix: false, + postfix: false, + leftAssoc: true, + }, + '**': { + precedence: 6, + operator: '**', + action: 'pow', + prefix: false, + postfix: false, + leftAssoc: true, + }, + '%': { + precedence: 4, + operator: '%', + action: 'percent', + prefix: false, + postfix: true, + leftAssoc: true, + overloaded: true, + overloadAction: 'mod', + overloadLeftAssoc: false, + operation(x) { + return _.divide(x, new NerdamerSymbol(100)); + }, + }, + '*': { + precedence: 4, + operator: '*', + action: 'multiply', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '/': { + precedence: 4, + operator: '/', + action: 'divide', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '+': { + precedence: 3, + operator: '+', + action: 'add', + prefix: true, + postfix: false, + leftAssoc: false, + operation(x) { + return x; + }, + }, + plus: { + precedence: 3, + operator: 'plus', + action: 'add', + prefix: true, + postfix: false, + leftAssoc: false, + operation(x) { + return x; + }, + }, + '-': { + precedence: 3, + operator: '-', + action: 'subtract', + prefix: true, + postfix: false, + leftAssoc: false, + operation(x) { + return x.negate(); + }, + }, + '=': { + precedence: 2, + operator: '=', + action: 'equals', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '==': { + precedence: 1, + operator: '==', + action: 'eq', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '<': { + precedence: 1, + operator: '<', + action: 'lt', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '<=': { + precedence: 1, + operator: '<=', + action: 'lte', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '>': { + precedence: 1, + operator: '>', + action: 'gt', + prefix: false, + postfix: false, + leftAssoc: false, + }, + '=>': { + precedence: 1, + operator: '=>', + action: 'gte', + prefix: false, + postfix: false, + leftAssoc: false, + }, + ',': { + precedence: 0, + operator: ',', + action: 'comma', + prefix: false, + postfix: false, + leftAssoc: false, + }, + ':': { + precedence: 0, + operator: ',', + action: 'assign', + prefix: false, + postfix: false, + leftAssoc: false, + vectorFn: 'slice', + }, + ':=': { + precedence: 0, + operator: ',', + action: 'functionAssign', + prefix: false, + postfix: false, + leftAssoc: true, + }, + }; + // Brackets + const brackets = { + '(': { + type: 'round', + id: 1, + is_open: true, + is_close: false, + }, + ')': { + type: 'round', + id: 2, + is_open: false, + is_close: true, + }, + '[': { + type: 'square', + id: 3, + is_open: true, + is_close: false, + maps_to: 'vector', + }, + ']': { + type: 'square', + id: 4, + is_open: false, + is_close: true, + }, + '{': { + type: 'curly', + id: 5, + is_open: true, + is_close: false, + maps_to: 'NerdamerSet', + }, + '}': { + type: 'curly', + id: 6, + is_open: false, + is_close: true, + }, + }; + // Supported functions. + // Format: function_name: [mappedFunction, number_of_parameters] + /** @type {FunctionMapType} */ + const functions = (this.functions = { + cos: [trig.cos, 1], + sin: [trig.sin, 1], + tan: [trig.tan, 1], + sec: [trig.sec, 1], + csc: [trig.csc, 1], + cot: [trig.cot, 1], + acos: [trig.acos, 1], + asin: [trig.asin, 1], + atan: [trig.atan, 1], + arccos: [trig.acos, 1], + arcsin: [trig.asin, 1], + arctan: [trig.atan, 1], + asec: [trig.asec, 1], + acsc: [trig.acsc, 1], + acot: [trig.acot, 1], + atan2: [trig.atan2, 2], + acoth: [trigh.acoth, 1], + asech: [trigh.asech, 1], + acsch: [trigh.acsch, 1], + sinh: [trigh.sinh, 1], + cosh: [trigh.cosh, 1], + tanh: [trigh.tanh, 1], + asinh: [trigh.asinh, 1], + sech: [trigh.sech, 1], + csch: [trigh.csch, 1], + coth: [trigh.coth, 1], + acosh: [trigh.acosh, 1], + atanh: [trigh.atanh, 1], + log10: [undefined, 1], + log2: [undefined, 1], + log1p: [undefined, 1], + exp: [exp, 1], + radians: [radians, 1], + degrees: [degrees, 1], + min: [min, -1], + max: [max, -1], + erf: [undefined, 1], + floor: [undefined, 1], + ceil: [undefined, 1], + trunc: [undefined, 1], + Si: [undefined, 1], + step: [undefined, 1], + rect: [undefined, 1], + sinc: [sinc, 1], + tri: [undefined, 1], + sign: [sign, 1], + Ci: [undefined, 1], + Ei: [undefined, 1], + Shi: [undefined, 1], + Chi: [undefined, 1], + Li: [undefined, 1], + fib: [undefined, 1], + fact: [_factorial, 1], + factorial: [_factorial, 1], + continuedFraction: [continuedFraction, [1, 2]], + dfactorial: [undefined, 1], + gamma_incomplete: [undefined, [1, 2]], + round: [round, [1, 2]], + scientific: [scientific, [1, 2]], + mod: [_mod, 2], + pfactor: [pfactor, 1], + vector: [vector, -1], + matrix: [matrix, -1], + NerdamerSet: [set, -1], + imatrix: [imatrix, -1], + parens: [parens, -1], + sqrt: [sqrt, 1], + cbrt: [cbrt, 1], + nthroot: [nthroot, 2], + log: [log, [1, 2]], + expand: [expandall, 1], + abs: [abs, 1], + invert: [invert, 1], + determinant: [determinant, 1], + size: [size, 1], + transpose: [transpose, 1], + dot: [dot, 2], + cross: [cross, 2], + vecget: [vecget, 2], + vecset: [vecset, 3], + vectrim: [vectrim, [1, 2]], + matget: [matget, 3], + matset: [matset, 4], + matgetrow: [matgetrow, 2], + matsetrow: [matsetrow, 3], + matgetcol: [matgetcol, 2], + matsetcol: [matsetcol, 3], + rationalize: [rationalize, 1], + IF: [IF, 3], + isIn: [isIn, 2], + // Imaginary support + realpart: [realpart, 1], + imagpart: [imagpart, 1], + conjugate: [conjugate, 1], + arg: [arg, 1], + polarform: [polarform, 1], + rectform: [rectform, 1], + sort: [sort, [1, 2]], + integer_part: [undefined, 1], + union: [union, 2], + contains: [contains, 2], + intersection: [intersection, 2], + difference: [difference, 2], + intersects: [intersects, 2], + isSubset: [isSubset, 2], + primes: [primes, 2], + // System support + print: [print, -1], + }); + + // Error handler + this.error = err; + // This function is used to comb through the function modules and find a function given its name + const findFunction = function (fname) { + const fmodules = Settings.FUNCTION_MODULES; + const l = fmodules.length; + for (let i = 0; i < l; i++) { + const fmodule = fmodules[i]; + if (fname in fmodule) { + return fmodule[fname]; + } + } + return err(`The function ${fname} is undefined!`); + }; + + /** + * This method gives the ability to override operators with new methods. + * + * @param {string} which + * @param {Function} withWhat + */ + this.override = function override(which, withWhat) { + bin[which] ||= []; + bin[which].push(this[which]); + this[which] = withWhat; + }; + + /** + * Restores a previously overridden operator + * + * @param {string} what + */ + this.restore = function restore(what) { + this[what] &&= bin[what].pop(); + }; + + /** + * This method is supposed to behave similarly to the override method but it does not override the existing + * function rather it only extends it + * + * @param {string} what + * @param {Function} withWhat + * @param {boolean} forceCall + */ + this.extend = function extend(what, withWhat, forceCall) { + const self = this; + const extended = this[what]; + if (typeof extended === 'function' && typeof withWhat === 'function') { + const f = this[what]; + this[what] = function extendedOp(a, b) { + if (isSymbol(a) && isSymbol(b) && !forceCall) { + return f.call(self, a, b); + } + return withWhat.call(self, a, b, f); + }; + } + }; + + /** + * Generates library's representation of a function. It's a fancy way of saying a symbol with a few extras. The + * most important thing is that that it gives a fname and an args property to the symbols in addition to + * changing its group to FN + * + * @param {string} fnName + * @param {Array} params + * @returns {NerdamerSymbolType} + */ + this.symfunction = function symfunction(fnName, params) { + // Call the proper function and return the result; + const f = new NerdamerSymbol(fnName); + f.group = FN; + if (typeof params === 'object') { + params = [].slice.call(params); + } // Ensure an array + f.args = params; + f.fname = fnName === PARENTHESIS ? '' : fnName; + f.updateHash(); + return f; + }; + + /** + * An internal function call for the Parser. This will either trigger a real function call if it can do so or + * just return a symbolic representation of the function using symfunction. + * + * @param {string} fnName + * @param {Array} args + * @param {number} [allowedArgs] + * @returns {NerdamerSymbolType} + */ + this.callfunction = function callfunction(fnName, args, allowedArgs = undefined) { + const fnSettings = functions[fnName]; + + if (!fnSettings) { + err(`Nerdamer currently does not support the function ${fnName}`); + } + + const numAllowedArgs = fnSettings[1] || allowedArgs; // Get the number of allowed arguments + let fn = fnSettings[0]; // Get the mapped function + let retval; + // We want to be able to call apply on the arguments or create a symfunction. Both require + // an array so make sure to wrap the argument in an array. + if (!(args instanceof Array)) { + args = args === undefined ? [] : [args]; + } + + if (numAllowedArgs !== -1) { + const isArrayType = isArray(numAllowedArgs); + const minArgs = isArrayType ? numAllowedArgs[0] : numAllowedArgs; + const maxArgs = isArrayType ? numAllowedArgs[1] : numAllowedArgs; + const numArgs = args.length; + + const errorMsg = `${fnName} requires a {0} of {1} arguments. {2} provided!`; + + if (numArgs < minArgs) { + err(format(errorMsg, 'minimum', minArgs, numArgs)); + } + if (numArgs > maxArgs) { + err(format(errorMsg, 'maximum', maxArgs, numArgs)); + } + } + + /* + * The following are very important to the how nerdamer constructs functions! + * Assumption 1 - if fn is undefined then handling of the function is purely numeric. This + * enables us to reuse Math, Math2, ..., any function from Settings.FUNCTIONS_MODULES entry + * Assumption 2 - if fn is defined then that function takes care of EVERYTHING including symbolics + * Assumption 3 - if the user calls symbolics on a function that returns a numeric value then + * they are expecting a symbolic output. + */ + // check if arguments are all numers + const numericArgs = allNumbers(args); + // Big number support. Check if Big number is requested and the arguments are all numeric and, not imaginary + // if (Settings.USE_BIG && numericArgs) { + // retval = Big[fnName].apply(undefined, args); + // } + // else { + if (fn) { + // Call nerdamer function + // Remember assumption 2. The function is defined so it MUST handle all aspects including numeric values + retval = fn.apply(fnSettings[2], args); + } else { + // Call JS function + // Remember assumption 1. No function defined so it MUST be numeric in nature + fn = findFunction(fnName); + if (Settings.PARSE2NUMBER && numericArgs) { + retval = bigConvert(fn.apply(fn, args)); + } else { + retval = _.symfunction(fnName, args); + } + } + // } + + return retval; + }; + /** + * Build a regex based on the operators currently loaded. These operators are to be ignored when substituting + * spaces for multiplication + */ + this.operator_filter_regex = (function buildOperatorFilterRegex() { + // We only want the operators which are singular since those are the ones + // that nerdamer uses anyway + const ostr = `^\\${Object.keys(operators) + .filter(x => x.length === 1) + .join('\\')}`; + // Create a regex which captures all spaces between characters except those + // have an operator on one end + // Note: Cannot use 'u' flag because operator escapes like \! are invalid in Unicode mode + // eslint-disable-next-line require-unicode-regexp -- Dynamic regex with operator chars that have invalid Unicode escapes + return new RegExp(`([${ostr}])\\s+([${ostr}])`); + })(); + + /** + * Replaces nerdamer.setOperator + * + * @param {object} operator + * @param {Function} [action] + * @param {'over' | 'under'} [shift] + */ + // eslint-disable-next-line no-shadow -- intentionally shadows outer setOperator for Parser method + this.setOperator = function setOperator(operator, action = undefined, shift = undefined) { + const name = operator.operator; // Take the name to be the symbol + operators[name] = operator; + if (action) { + this[operator.action] = action; + } + // Make the parser aware of the operator + _parser[name] = operator.operation; + // Make the action available to the parser if infix + if (!operator.action && !(operator.prefix || operator.postif)) { + operator.action = name; + } + // If this operator is exclusive then all successive operators should be shifted + if (shift === 'over' || shift === 'under') { + const { precedence } = operator; + + for (const x in operators) { + if (!Object.hasOwn(operators, x)) { + continue; + } + const o = operators[x]; + const condition = shift === 'over' ? o.precedence >= precedence : o.precedence > precedence; + if (condition) { + o.precedence++; + } + } + } + }; + + /** + * Gets an opererator by its symbol + * + * @param {string} operator + * @returns {object} + */ + // eslint-disable-next-line no-shadow -- intentionally shadows outer getOperator for Parser method + this.getOperator = function getOperator(operator) { + return operators[operator]; + }; + + // eslint-disable-next-line no-shadow -- intentionally shadows outer aliasOperator for Parser method + this.aliasOperator = function aliasOperator(o, n) { + const t = {}; + const operator = operators[o]; + // Copy everything over to the new operator + for (const x in operator) { + if (!Object.hasOwn(operator, x)) { + continue; + } + t[x] = operator[x]; + } + // Update the symbol + t.operator = n; + + this.setOperator(t); + }; + + /** + * Returns the list of operators. Caution! Can break parser! + * + * @returns {object} + */ + this.getOperators = function getOperators() { + // Will replace this with some cloning action in the future + return operators; + }; + + this.getBrackets = function getBrackets() { + return brackets; + }; + /* + * Preforms preprocessing on the string. Useful for making early modification before + * sending to the parser + * @param {string} e + * @param {ParserType} parser - The parser instance to use as context + */ + const prepareExpression = function prepareExpression(e, parser) { + /* + * Since variables cannot start with a number, the assumption is made that when this occurs the + * user intents for this to be a coefficient. The multiplication symbol in then added. The same goes for + * a side-by-side close and open parenthesis + */ + e = String(e); + // Apply preprocessors + for (let i = 0; i < preprocessors.actions.length; i++) { + e = preprocessors.actions[i].call(parser, e); + } + + // E = e.split(' ').join('');//strip empty spaces + // replace multiple spaces with one space + e = e.replace(/\s+/gu, ' '); + + // Only even bother to check if the string contains e. This regex is painfully slow and might need a better solution. e.g. hangs on (0.06/3650))^(365) + if (/e/giu.test(e)) { + // Negative numbers + e = e.replace(/-+\d+\.?\d*e\+?-?\d+/giu, x => scientificToDecimal(x)); + // Positive numbers that are not part of an identifier + e = e.replace(/(? scientificToDecimal(x)); + } + // Replace scientific numbers + + // allow omission of multiplication after coefficients + e = + e + .replace(Settings.IMPLIED_MULTIPLICATION_REGEX, (match, group1, group2, start, str) => { + const first = str.charAt(start); + let before = ''; + let d = '*'; + if (!first.match(/[+\-/*]/u)) { + before = str.charAt(start - 1); + } + if (before.match(/[a-z]/iu)) { + d = ''; + } + return group1 + d + group2; + }) + .replace(/(?[a-z0-9_]+)/giu, (match, a) => { + if (Settings.USE_MULTICHARACTER_VARS === false && !(a in functions)) { + if (!isNaN(a)) { + return a; + } + return a.split('').join('*'); + } + return a; + }) + // Allow omission of multiplication sign between brackets + .replace(/\)\(/gu, ')*(') || '0'; + // Replace x(x+a) with x*(x+a) + while (true) { + const eOrg = e; // Store the original + e = e.replace( + /(?[a-z0-9_]+)(?\()|(?\))(?[a-z0-9]+)/giu, + (match, a, b, c, d) => { + const g1 = a || c; + const g2 = b || d; + if (g1 in functions) // Create a passthrough for functions + { + return g1 + g2; + } + return `${g1}*${g2}`; + } + ); + // If the original equals the replace we're done + if (eOrg === e) { + break; + } + } + return e; + }; + // Delay setting of constants until Settings is ready + this.initConstants = function initConstants() { + this.CONSTANTS = { + E: new NerdamerSymbol(Settings.E), + PI: new NerdamerSymbol(Settings.PI), + }; + }; + /* + * Debugging method used to better visualize vector and arrays + * @param {object | ScopeArrayType} o + * @returns {string} + */ + this.prettyPrint = function prettyPrint(o) { + if (Array.isArray(o)) { + const arr = /** @type {ScopeArrayType} */ (o); + const s = arr.map(x => _.prettyPrint(x)).join(', '); + if (arr.type === 'vector') { + return `vector<${s}>`; + } + return `(${s})`; + } + return o.toString(); + }; + this.peekers = { + pre_operator: [], + post_operator: [], + pre_function: [], + post_function: [], + }; + + this.callPeekers = function callPeekers(name, ...rest) { + if (Settings.callPeekers) { + const peekers = this.peekers[name]; + // Remove the first items and stringify + const args = rest.map(stringify); + // Call each one of the peekers + for (let i = 0; i < peekers.length; i++) { + peekers[i].apply(null, args); + } + } + }; + /* + * Tokenizes the string + * @param {string} e + * @returns {Token[]} + */ + this.tokenize = function tokenize(e) { + // Cast to String + e = String(e); + // Remove multiple white spaces and spaces at beginning and end of string + e = e.trim().replace(/\s+/gu, ' '); + // Remove spaces before and after brackets + for (const x in brackets) { + if (!Object.hasOwn(brackets, x)) { + continue; + } + const regex = new RegExp(brackets[x].is_close ? `\\s+\\${x}` : `\\${x}\\s+`, 'gu'); + e = e.replace(regex, x); + } + + let col = 0; // The column position + const L = e.length; // Expression length + let lpos = 0; // Marks beginning of next token + const tokens = []; // The tokens container + const scopes = [tokens]; // Initiate with the tokens as the highest scope + let target = scopes[0]; // The target to which the tokens are added. This can swing up or down + let depth = 0; + const openBrackets = []; + let hasSpace = false; // Marks if an open space character was found + let operatorStr; // Current operator string being processed + const SPACE = ' '; + const EMPTY_STRING = ''; + const COMMA = ','; + const MINUS = '-'; + const MULT = '*'; + // Possible source of bug. Review + /* + //gets the next space + let next_space = function(from) { + for(let i=from; i operator.precedence || + (!operator.leftAssoc && last.precedence === operator.precedence) + ) + ) { + break; + } + output.push(stack.pop()); + } + + // Change the behavior of the operator if it's a vector and we've been asked to do so + if ((fn === 'vector' || fn === 'set') && 'vectorFn' in operator) { + operator.action = operator.vectorFn; + } + + // If the operator is a postfix operator then we're ready to go since it belongs + // to the preceding token. However the output cannot be empty. It must have either + // an operator or a variable/literal + if (operator.postfix) { + const previous = tokens[i - 1]; + if (!previous) { + throw new OperatorError(`Unexpected prefix operator '${e.value}'! at ${e.column}`); + } else if (previous.type === Token.OPERATOR) { + // A postfix can only be followed by a postfix + if (!previous.postfix) { + throw new OperatorError( + `Unexpected prefix operator '${previous.value}'! at ${previous.column}` + ); + } + } + } else { + // We must be at an infix so point the operator this + let nextIsOperator; + do { + // The first one is an infix operator all others have to be prefix operators so jump to the end + const next = tokens[i + 1]; // Take a look ahead + nextIsOperator = next ? next.type === Token.OPERATOR : false; // Check if it's an operator + if (nextIsOperator) { + // If it's not a prefix operator then it not in the right place + if (!next.prefix) { + throw new OperatorError(`A prefix operator was expected at ${next.column}`); + } + // Mark it as a confirmed prefix + next.is_prefix = true; + // Add it to the prefixes + prefixes.push(next); + i++; + } + } while (nextIsOperator); + } + + // If it's a prefix it should be on a special stack called prefixes + // we do this to hold on to prefixes because of left associative operators. + // they belong to the variable/literal but if placed on either the stack + // or output there's no way of knowing this. I might be wrong so I welcome + // any discussion about this. + + if (operator.is_prefix) // ADD ALL EXCEPTIONS FOR ADDING TO PREFIX STACK HERE. !!! + { + prefixes.push(operator); + } else { + stack.push(operator); + } + // Move the prefixes to the stack + while (prefixes.length) { + if ( + operator.leftAssoc || + (!operator.leftAssoc && prefixes[prefixes.length - 1].precedence >= operator.precedence) + ) // Revisit for commas + { + stack.push(prefixes.pop()); + } else { + break; + } + } + } else if (e.type === Token.VARIABLE_OR_LITERAL) { + // Move prefixes to stack at beginning of scope + if (output.length === 0) { + collapse(prefixes, stack); + } + // Done with token + output.push(e); + const lastOnStack = stack[stack.length - 1]; + // Then move all the prefixes to the output + if (!lastOnStack || !lastOnStack.leftAssoc) { + collapse(prefixes, output); + } + } else if (e.type === Token.FUNCTION) { + stack.push(e); + } else if (e.type === Token.UNIT) { + // If it's a unit it belongs on the stack since it's tied to the previous token + output.push(e); + } + // If it's an additonal scope then put that into RPN form + if (Array.isArray(e)) { + const scopeArr = /** @type {ScopeArrayType} */ (e); + output.push(this.toRPN(e)); + if (scopeArr.type) { + output.push(new Token(scopeArr.type, Token.FUNCTION, scopeArr.column)); + } // Since it's hidden it needs no column + } + } + // Collapse the remainder of the stack and prefixes to output + collapse(stack, output); + collapse(prefixes, output); + + return output; + }; + /* + * Parses the tokens + * @param {Tokens[]} rpn + * @param {object} substitutions + * @returns {NerdamerSymbolType} + */ + // eslint-disable-next-line no-shadow -- rpn parameter name matches expected API + this.parseRPN = function parseRPN(rpn, substitutions) { + try { + // Default substitutions + substitutions ||= {}; + // Prepare the substitutions. + // we first parse them out as-is + for (const x in substitutions) { + if (!Object.hasOwn(substitutions, x)) { + continue; + } + substitutions[x] = _.parse(substitutions[x], {}); + } + + // Although technically constants, + // pi and e are only available when evaluating the expression so add to the subs. + // Doing this avoids rounding errors + // link e and pi + if (Settings.PARSE2NUMBER) { + // Use the value provided if the individual for some strange reason prefers this. + // one reason could be to sub e but not pi or vice versa + if (!('e' in substitutions)) { + substitutions.e = new NerdamerSymbol(Settings.E); + } + if (!('pi' in substitutions)) { + substitutions.pi = new NerdamerSymbol(Settings.PI); + } + } + + const Q = []; + let e; // Current RPN token being processed - also used for error reporting + for (let i = 0, l = rpn.length; i < l; i++) { + e = rpn[i]; + + // Arrays indicate a new scope so parse that out + if (Array.isArray(e)) { + e = this.parseRPN(e, substitutions); + } + + if (e) { + if (e.type === Token.OPERATOR) { + if (e.is_prefix || e.postfix) // Resolve the operation assocated with the prefix + { + Q.push(e.operation(Q.pop())); + } else { + let b = Q.pop(); + let a = Q.pop(); + // Throw an error if the RH value is empty. This cannot be a postfix since we already checked + if (typeof a === 'undefined') { + throw new OperatorError(`${e} is not a valid postfix operator at ${e.column}`); + } + + const isComma = e.action === 'comma'; + // Convert Sets to Vectors on all operations at this point. Sets are only recognized functions or individually + if (a instanceof NerdamerSet && !isComma) { + a = Vector.fromSet(/** @type {SetType} */ (a)); + } + + if (b instanceof NerdamerSet && !isComma) { + b = Vector.fromSet(/** @type {SetType} */ (b)); + } + + // Call all the pre-operators + this.callPeekers('pre_operator', a, b, e); + + const ans = _[e.action](a, b); + + // Call all the pre-operators + this.callPeekers('post_operator', ans, a, b, e); + + Q.push(ans); + } + } else if (e.type === Token.FUNCTION) { + let args = Q.pop(); + const { parent } = args; // Make a note of the parent + if (!(args instanceof Collection)) { + args = Collection.create(args); + } + // The return value may be a vector. If it is then we check + // Q to see if there's another vector on the stack. If it is then + // we check if has elements. If it does then we know that we're dealing + // with an "getter" object and return the requested values + + // call the function. This is the _.callfunction method in nerdamer + const fnName = e.value; + const fnArgs = args.getItems(); + + // Call the pre-function peekers + this.callPeekers('pre_function', fnName, fnArgs); + + const ret = _.callfunction(fnName, fnArgs); + + // Call the post-function peekers + this.callPeekers('post_function', ret, fnName, fnArgs); + + const _last = Q[Q.length - 1]; + const next = rpn[i + 1]; + const _next_is_comma = next && next.type === Token.OPERATOR && next.value === ','; + + // If(!next_is_comma && ret instanceof Vector && last && last.elements && !(last instanceof Collection)) { + // //remove the item from the queue + // let item = Q.pop(); + + // let getter = ret.elements[0]; + // //check if it's symbolic. If so put it back and add the item to the stack + // if(!getter.isConstant()) { + // item.getter = getter; + // Q.push(item); + // Q.push(ret); + // } + // else if(getter instanceof Slice) { + // //if it's a Slice return the slice + // Q.push(Vector.fromArray(item.elements.slice(getter.upper, getter.lower))); + // } + // else { + // let index = Number(getter); + // let il = item.elements.length; + // //support for negative indices + // if(index < 0) + // index = il + index; + // //it it's still out of bounds + // if(index < 0 || index >= il) //index should no longer be negative since it's been reset above + // //range error + // throw new OutOfRangeError('Index out of range ' + (e.column + 1)); + + // let element = item.elements[index]; + // //cyclic but we need to mark this for future reference + // item.getter = index; + // element.parent = item; + + // Q.push(element); + // } + // } + // else { + // extend the parent reference + if (parent) { + ret.parent = parent; + } + Q.push(ret); + // } + } else { + let subbed; + const v = e.value; + + if (v in Settings.ALIASES) { + e = _.parse(Settings.ALIASES[e]); + } + // Wrap it in a symbol if need be + else if (e.type === Token.VARIABLE_OR_LITERAL) { + e = new NerdamerSymbol(v); + } else if (e.type === Token.UNIT) { + /** @type {NerdamerSymbolType} */ + const unitSymbol = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(v)) + ); + unitSymbol.isUnit = true; + e = unitSymbol; + } + + // Make substitutions + // Always constants first. This avoids the being overridden + if (v in _.CONSTANTS) { + subbed = e; + e = new NerdamerSymbol(_.CONSTANTS[v]); + } + // Next substitutions. This allows declared variable to be overridden + // check if the values match to avoid erasing the multiplier. + // Example:/e = 3*a. substutiting a for a will wipe out the multiplier. + else if (v in substitutions && v !== substitutions[v].toString()) { + subbed = e; + e = substitutions[v].clone(); + } + // Next declare variables + else if (v in VARS) { + subbed = e; + e = VARS[v].clone(); + } + // Make notation of what it was before + if (subbed) { + e.subbed = subbed; + } + + Q.push(e); + } + } + } + + const retval = Q[0]; + + if (['undefined', 'string', 'number'].indexOf(typeof retval) !== -1) { + throw new UnexpectedTokenError('Unexpected token!'); + } + + return retval; + } catch (error) { + if (error.message === 'timeout') { + throw error; + } + // Rethrow non-parsing errors (TypeError, ReferenceError, etc.) as-is + // to preserve stack traces for debugging + if (error instanceof TypeError || error instanceof ReferenceError || error instanceof RangeError) { + throw error; + } + const rethrowErrors = [OutOfFunctionDomainError]; + // Rethrow certain errors in the same class to preserve them + rethrowErrors.forEach(E => { + if (error instanceof E) { + const col = /** @type {{ column?: number }} */ (error).column; + throw new E(`${error.message}${col ? `: ${col}` : ''}`); + } + }); + + const errCol = /** @type {{ column?: number }} */ (error).column; + throw new ParseError(`${error.message}${errCol ? `: ${errCol}` : ''}`); + } + }; + /** + * This is the method that triggers the parsing of the string. It generates a parse tree but processes it right + * away. The operator functions are called when their respective operators are reached. For instance + * + * - With cause this.add to be called with the left and right hand values. It works by walking along each + * character of the string and placing the operators on the stack and values on the output. When an operator + * having a lower order than the last is reached then the stack is processed from the last operator on the + * stack. + */ + + /** Node class for representing parse tree nodes */ + class Node { + /** @param {{ type: string; value: string; left?: Node; right?: Node }} token */ + constructor(token) { + this.type = token.type; + this.value = token.value; + // The incoming token may already be a Node type + this.left = token.left; + this.right = token.right; + } + + toString() { + const left = this.left ? `${this.left.toString()}---` : ''; + const right = this.right ? `---${this.right.toString()}` : ''; + return `${left}(${this.value})${right}`; + } + + toHTML(depth, indent) { + depth ||= 0; + indent = typeof indent === 'undefined' ? 4 : indent; + const tab = function tab(n) { + return ' '.repeat(indent * n); + }; + let html = ''; + const left = this.left + ? `${tab(depth + 1)}
  • \n${this.left.toHTML(depth + 2, indent)}${tab(depth + 1)}
  • \n` + : ''; + const right = this.right + ? `${tab(depth + 1)}
  • \n${this.right.toHTML(depth + 2, indent)}${tab(depth + 1)}
  • \n` + : ''; + html = `${tab(depth)}
    ${this.value}
    ${tab(depth)}\n`; + if (left || right) { + html += `${tab(depth)}
      \n${left}${right}${tab(depth)}
    \n`; + } + return html; + } + } + + // eslint-disable-next-line no-shadow -- intentionally shadows outer tree for Parser method + this.tree = function tree(tokens) { + const Q = []; + for (let i = 0; i < tokens.length; i++) { + let e = tokens[i]; + // Arrays indicate a new scope so parse that out + if (Array.isArray(e)) { + e = this.tree(e); + // If it's a comma then it's just arguments + Q.push(e); + continue; + } + if (e.type === Token.OPERATOR) { + if (e.is_prefix || e.postfix) { + // Prefixes go to the left, postfix to the right + const location = e.is_prefix ? 'left' : 'right'; + const last = Q.pop(); + e = new Node(e); + e[location] = last; + Q.push(e); + } else { + e = new Node(e); + e.right = Q.pop(); + e.left = Q.pop(); + Q.push(e); + } + } else if (e.type === Token.FUNCTION) { + e = new Node(e); + const args = Q.pop(); + e.right = args; + if (e.value === 'object') { + // Check if Q has a value + let last = Q[Q.length - 1]; + if (last) { + while (last.right) { + last = last.right; + } + last.right = e; + continue; + } + } + + Q.push(e); + } else { + Q.push(new Node(e)); + } + } + + return Q[0]; + }; + // eslint-disable-next-line no-shadow -- intentionally shadows outer parse for Parser method + this.parse = function parse(e, substitutions) { + e = prepareExpression(e, this); + substitutions ||= {}; + // Three passes but easier to debug + const tokens = this.tokenize(e); + const rpnTokens = this.toRPN(tokens); + return this.parseRPN(rpnTokens, substitutions); + }; + /** + * TODO: Switch to Parser.tokenize for this method Reads a string into an array of Symbols and operators + * + * @param {string} expressionString + * @returns {Array} + */ + this.toObject = function toObject(expressionString) { + const objectify = function objectify(tokens) { + const output = []; + for (let i = 0, l = tokens.length; i < l; i++) { + const token = tokens[i]; + const v = token.value; + if (token.type === Token.VARIABLE_OR_LITERAL) { + output.push(new NerdamerSymbol(v)); + } else if (token.type === Token.FUNCTION) { + // Jump ahead since the next object are the arguments + i++; + // Create a symbolic function and stick it on output + const f = _.symfunction(v, objectify(tokens[i])); + f.isConversion = true; + output.push(f); + } else if (token.type === Token.OPERATOR) { + output.push(v); + } else { + output.push(objectify(token)); + } + } + + return output; + }; + return objectify(_.tokenize(expressionString)); + }; + + // A helper method for toTeX + const chunkAtCommas = function chunkAtCommas(arr) { + let k = 0; + const chunks = [[]]; + for (let j = 0, l = arr.length; j < l; j++) { + if (arr[j] === ',') { + k++; + chunks[k] = []; + } else { + chunks[k].push(arr[j]); + } + } + return chunks; + }; + + // Helper method for toTeX + const remBrackets = function (str) { + return str.replace(/^\\left\((?.+)\\right\)$/gu, (match, a) => { + if (a) { + return a; + } + return match; + }); + }; + + const removeRedundantPowers = function (arr) { + // The filtered array + const narr = []; + + while (arr.length) { + // Remove the element from the front + const e = arr.shift(); + const next = arr[0]; + const nextIsArray = isArray(next); + const nextIsMinus = next === '-'; + + // Remove redundant plusses + if (e === '^') { + if (next === '+') { + arr.shift(); + } else if (nextIsArray && next[0] === '+') { + next.shift(); + } + + // Remove redundant parentheses + if (nextIsArray && next.length === 1) { + arr.unshift(arr.shift()[0]); + } + } + + // Check if it's a negative power + if (e === '^' && ((nextIsArray && next[0] === '-') || nextIsMinus)) { + // If so: + // - Remove it from the new array, place a one and a division sign in that array and put it back + const last = narr.pop(); + // Check if it's something multiplied by + const before = narr[narr.length - 1]; + let beforeLast = '1'; + + if (before === '*') { + narr.pop(); + // For simplicity we just pop it. + beforeLast = narr.pop(); + } + // Implied multiplication + else if (isArray(before)) { + beforeLast = narr.pop(); + } + + narr.push(beforeLast, '/', last, e); + + // Remove the negative sign from the power + if (nextIsArray) { + next.shift(); + } else { + arr.shift(); + } + + // Remove it from the array so we don't end up with redundant parentheses if we can + if (nextIsArray && next.length === 1) { + narr.push(arr.shift()[0]); + } + } else { + narr.push(e); + } + } + + return narr; + }; + /* + * Convert expression or object to LaTeX + * @param {string} expressionOrObj + * @param {object} opt + * @returns {string} + */ + this.toTeX = function toTeX(expressionOrObj, opt) { + opt ||= {}; + // Add decimal option as per issue #579. Consider passing an object to Latex.latex as option instead of string + const decimals = opt.decimals === true ? 'decimals' : undefined; + + let obj = typeof expressionOrObj === 'string' ? this.toObject(expressionOrObj) : expressionOrObj; + const TeX = []; + const cdot = typeof opt.cdot === 'undefined' ? '\\cdot' : opt.cdot; // NerdamerSet omit cdot to true by default + + // Remove negative powers as per issue #570 + obj = removeRedundantPowers(obj); + + if (isArray(obj)) { + const nobj = []; + let a; + let b; + // First handle ^ + for (let i = 0; i < obj.length; i++) { + a = obj[i]; + + if (obj[i + 1] === '^') { + b = obj[i + 2]; + nobj.push(`${LaTeX.braces(this.toTeX([a]))}^${LaTeX.braces(this.toTeX([b]))}`); + i += 2; + } else { + nobj.push(a); + } + } + obj = nobj; + } + + for (let i = 0, l = obj.length; i < l; i++) { + let e = obj[i]; + + // Convert * to cdot + if (e === '*') { + e = cdot; + } + + if (isSymbol(e)) { + if (e.group === FN) { + const { fname } = e; + let f; + + if (fname === SQRT) { + f = `\\sqrt${LaTeX.braces(this.toTeX(e.args))}`; + } else if (fname === ABS) { + f = LaTeX.brackets(this.toTeX(e.args), 'abs'); + } else if (fname === PARENTHESIS) { + f = LaTeX.brackets(this.toTeX(e.args), 'parens'); + } else if (fname === Settings.LOG) { + f = `\\${Settings.LOG_LATEX}\\left( ${this.toTeX(e.args)}\\right)`; + } else if (fname === Settings.LOG10) { + f = `\\${Settings.LOG10_LATEX}\\left( ${this.toTeX(e.args)}\\right)`; + } else if (fname === Settings.LOG2) { + f = `\\${Settings.LOG2_LATEX}\\left( ${this.toTeX(e.args)}\\right)`; + } else if (fname === Settings.LOG1P) { + f = `\\${format(Settings.LOG1P_LATEX, this.toTeX(e.args))}`; + } else if (fname === 'integrate') { + /* Retrive [Expression, x] */ + const chunks = chunkAtCommas(e.args); + /* Build TeX */ + const expr = LaTeX.braces(this.toTeX(chunks[0])); + const dx = this.toTeX(chunks[1]); + f = `\\int ${expr}\\, d${dx}`; + } else if (fname === 'defint') { + const chunks = chunkAtCommas(e.args); + const expr = LaTeX.braces(this.toTeX(chunks[0])); + const dx = this.toTeX(chunks[3]); + const lb = this.toTeX(chunks[1]); + const ub = this.toTeX(chunks[2]); + f = `\\int\\limits_{${lb}}^{${ub}} ${expr}\\, d${dx}`; + } else if (fname === 'diff') { + const chunks = chunkAtCommas(e.args); + let dx = ''; + const expr = LaTeX.braces(this.toTeX(chunks[0])); + /* Handle cases: one argument provided, we need to guess the variable, and assume n = 1 */ + if (chunks.length === 1) { + const vars = []; + for (let j = 0; j < chunks[0].length; j++) { + if (chunks[0][j].group === 3) { + vars.push(chunks[0][j].value); + } + } + vars.sort(); + dx = vars.length > 0 ? `\\frac{d}{d ${vars[0]}}` : '\\frac{d}{d x}'; + } else if (chunks.length === 2) { + /* If two arguments, we have expression and variable, we assume n = 1 */ + dx = `\\frac{d}{d ${chunks[1]}}`; + } else { + /* If we have more than 2 arguments, we assume we've got everything */ + dx = `\\frac{d^{${chunks[2]}}}{d ${this.toTeX(chunks[1])}^{${chunks[2]}}}`; + } + + f = `${dx}\\left(${expr}\\right)`; + } else if (fname === 'sum' || fname === 'product') { + // Split e.args into 4 parts based on locations of , symbols. + const argSplit = [[], [], [], []]; + let argIdx = 0; + for (let k = 0; k < e.args.length; k++) { + if (/** @type {string} */ (/** @type {unknown} */ (e.args[k])) === ',') { + argIdx++; + continue; + } + argSplit[argIdx].push(e.args[k]); + } + // Then build TeX string. + f = + (fname === 'sum' ? '\\sum_' : '\\prod_') + + LaTeX.braces(`${this.toTeX(argSplit[1])} = ${this.toTeX(argSplit[2])}`); + f += `^${LaTeX.braces(this.toTeX(argSplit[3]))}${LaTeX.braces(this.toTeX(argSplit[0]))}`; + } else if (fname === 'limit') { + const toTeXfn = this.toTeX.bind(this); + const parserRef = _; + const args = chunkAtCommas(e.args).map(x => { + if (Array.isArray(x)) { + return parserRef.toTeX(x.join('')); + } + return toTeXfn(String(x)); + }); + f = `\\lim_${LaTeX.braces(`${args[1]}\\to ${args[2]}`)} ${LaTeX.braces(args[0])}`; + } else if (fname === FACTORIAL || fname === DOUBLEFACTORIAL) { + f = this.toTeX(e.args) + (fname === FACTORIAL ? '!' : '!!'); + } else { + f = LaTeX.latex(e, decimals); + // F = '\\mathrm'+LaTeX.braces(fname.replace(/_/g, '\\_')) + LaTeX.brackets(this.toTeX(e.args), 'parens'); + } + + TeX.push(f); + } else { + TeX.push(LaTeX.latex(e, decimals)); + } + } else if (isArray(e)) { + TeX.push(LaTeX.brackets(this.toTeX(e))); + } else if (e === '/') { + TeX.push(LaTeX.frac(remBrackets(TeX.pop()), remBrackets(this.toTeX([obj[++i]])))); + } else { + TeX.push(e); + } + } + + return TeX.join(' '); + }; + + // Parser.functions ============================================================== + /* Although parens is not a "real" function it is important in some cases when the + * symbol must carry parenthesis. Once set you don't have to worry about it anymore + * as the parser will get rid of it at the first opportunity + */ + function parens(symbol) { + if (Settings.PARSE2NUMBER) { + return symbol; + } + return _.symfunction('parens', [symbol]); + } + + function abs(symbol) { + // |-∞| = ∞ + if (symbol.isInfinity) { + return NerdamerSymbol.infinity(); + } + if (symbol.multiplier.lessThan(0)) { + symbol.multiplier.negate(); + } + + if (symbol.isImaginary()) { + const re = symbol.realpart(); + const im = symbol.imagpart(); + if (re.isConstant() && im.isConstant()) { + return sqrt(_.add(_.pow(re, new NerdamerSymbol(2)), _.pow(im, new NerdamerSymbol(2)))); + } + } else if (isNumericSymbol(symbol) || even(symbol.power)) { + return symbol; + } + // Together.math baseunits are presumed positive + else if ( + isVariableSymbol(symbol) && + typeof symbol.value === 'string' && + symbol.value.startsWith('baseunit_') + ) { + return symbol; + } + + if (symbol.isComposite()) { + const ms = []; + symbol.each(x => { + ms.push(x.multiplier); + }); + const gcd = Math2.QGCD.apply(null, ms); + if (gcd.lessThan(0)) { + symbol.multiplier = symbol.multiplier.multiply(new Frac(-1)); + symbol.distributeMultiplier(); + } + } + + // Convert |n*x| to n*|x| + const m = _.parse(symbol.multiplier); + symbol.toUnitMultiplier(); + + return _.multiply(m, _.symfunction(ABS, [symbol])); + } + /** + * The factorial function + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + function _factorial(symbol) { + let retval; + if (isVector(symbol)) { + const V = new Vector(); + symbol.each((x, i) => { + // I start at one. + V.set( + /** @type {number} */ (/** @type {unknown} */ (i)) - 1, + /** @type {NerdamerSymbolType} */ (_factorial(x)) + ); + }); + return /** @type {VectorType} */ (V); + } + if (isMatrix(symbol)) { + const M = new Matrix(); + symbol.each((x, i, j) => { + // I start at one. + M.set(i, j, /** @type {NerdamerSymbolType} */ (_factorial(x))); + }); + return /** @type {MatrixType} */ (M); + } + if (Settings.PARSE2NUMBER && symbol.isConstant()) { + if (isInt(symbol)) { + retval = Math2.bigfactorial(symbol); + } else { + retval = Math2.gamma(symbol.multiplier.add(/** @type {FracType} */ (new Frac(1))).toDecimal()); + } + + retval = bigConvert(retval); + return retval; + } + if (symbol.isConstant()) { + const den = symbol.getDenom(); + if (den.equals(2)) { + const num = symbol.getNum(); + let a; + let b; + let n; + + if (symbol.multiplier.isNegative()) { + n = /** @type {NerdamerSymbolType} */ ( + _.subtract(num.negate(), new NerdamerSymbol(1)) + ).multiplier.divide(new Frac(2)); + a = /** @type {NerdamerSymbolType} */ ( + _.pow(new NerdamerSymbol(-4), new NerdamerSymbol(n)) + ).multiplier.multiply(Math2.bigfactorial(n)); + b = Math2.bigfactorial(new Frac(2).multiply(n)); + } else { + n = /** @type {NerdamerSymbolType} */ (_.add(num, new NerdamerSymbol(1))).multiplier.divide( + new Frac(2) + ); + a = Math2.bigfactorial(new Frac(2).multiply(n)); + b = /** @type {NerdamerSymbolType} */ ( + _.pow(new NerdamerSymbol(4), new NerdamerSymbol(n)) + ).multiplier.multiply(Math2.bigfactorial(n)); + } + const c = a.divide(b); + return /** @type {NerdamerSymbolType | VectorType | MatrixType} */ ( + _.multiply(_.parse('sqrt(pi)'), new NerdamerSymbol(c)) + ); + } + } + return /** @type {NerdamerSymbolType | VectorType | MatrixType} */ (_.symfunction(FACTORIAL, [symbol])); + } + /** + * Returns the continued fraction of a number + * + * @param {NerdamerSymbolType} symbol + * @param {NerdamerSymbolType} n + * @returns {NerdamerSymbolType | Vector} + */ + function continuedFraction(symbol, n) { + const _symbol = evaluate(symbol); + if (_symbol.isConstant()) { + const cf = Math2.continuedFraction(_symbol, n); + // Convert the fractions array to a new Vector + const fractions = Vector.fromArray(cf.fractions.map(x => new NerdamerSymbol(x))); + return Vector.fromArray([ + new NerdamerSymbol(cf.sign), + new NerdamerSymbol(cf.whole), + /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (fractions)), + ]); + } + return _.symfunction('continuedFraction', [symbol, n]); + } + /** + * Returns the error function + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function _erf(symbol) { + const _symbol = evaluate(symbol); + + if (_symbol.isConstant()) { + return new NerdamerSymbol(Math2.erf(_symbol)); + } + if (_symbol.isImaginary()) { + return complex.erf(symbol); + } + return _.symfunction('erf', [symbol]); + } + /** + * The mod function + * + * @param {NerdamerSymbolType} symbol1 + * @param {NerdamerSymbolType} symbol2 + * @returns {NerdamerSymbolType} + */ + function _mod(symbol1, symbol2) { + if (symbol1.isConstant() && symbol2.isConstant()) { + const retval = new NerdamerSymbol(1); + retval.multiplier = retval.multiplier.multiply(symbol1.multiplier.mod(symbol2.multiplier)); + return retval; + } + // Try to see if division has remainder of zero + const r = _.divide(symbol1.clone(), symbol2.clone()); + if (isInt(r)) { + return new NerdamerSymbol(0); + } + return _.symfunction('mod', [symbol1, symbol2]); + } + /** + * A branghing function + * + * @param {boolean} condition + * @param {NerdamerSymbolType} a + * @param {NerdamerSymbolType} b + * @returns {NerdamerSymbolType} + */ + function IF(condition, a, b) { + if (typeof condition !== 'boolean') { + if (isNumericSymbol(condition)) { + condition = !!Number(condition); + } + } + if (condition) { + return a; + } + return b; + } + /** + * @param {MatrixType | VectorType | SetType | CollectionType} obj + * @param {NerdamerSymbolType} item + * @returns {NerdamerSymbolType} + */ + function isIn(obj, item) { + if (isMatrix(obj)) { + for (let i = 0, l = obj.rows(); i < l; i++) { + for (let j = 0, l2 = obj.cols(); j < l2; j++) { + const element = /** @type {NerdamerSymbolType} */ (obj.elements[i][j]); + if (element.equals(item)) { + return new NerdamerSymbol(1); + } + } + } + } else if (obj.elements) { + for (let i = 0, l = obj.elements.length; i < l; i++) { + if (/** @type {NerdamerSymbolType} */ (obj.elements[i]).equals(item)) { + return new NerdamerSymbol(1); + } + } + } + + return new NerdamerSymbol(0); + } + + /** + * A symbolic extension for sinc + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function sinc(symbol) { + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant()) { + return new NerdamerSymbol(Math2.sinc(symbol)); + } + return _.parse(format('sin({0})/({0})', symbol)); + } + return _.symfunction('sinc', [symbol]); + } + + /** + * A symbolic extension for exp. This will auto-convert all instances of exp(x) to e^x. Thanks @ Happypig375 + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + function exp(symbol) { + if (symbol.fname === Settings.LOG && symbol.isLinear()) { + return _.pow(symbol.args[0], NerdamerSymbol.create(symbol.multiplier.toString())); + } + return _.parse(format('e^({0})', symbol)); + } + + /** + * Converts value degrees to radians + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function radians(symbol) { + return _.parse(format('({0})*pi/180', symbol)); + } + + /** + * Converts value from radians to degrees + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function degrees(symbol) { + return _.parse(format('({0})*180/pi', symbol)); + } + + function _nroots(symbol) { + let a; + let b; + /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ + let _roots; + if (symbol.group === FN && symbol.fname === '') { + a = NerdamerSymbol.unwrapPARENS(_.parse(symbol).toLinear()); + b = _.parse(symbol.power); + } else if (symbol.group === P) { + a = _.parse(symbol.value); + b = _.parse(symbol.power); + } + + if (a && b && a.group === N && b.group === N) { + _roots = []; + const _parts = NerdamerSymbol.toPolarFormArray(symbol); + const r = _.parse(a).abs().toString(); + // https://en.wikipedia.org/wiki/De_Moivre%27s_formula + const x = arg(a).toString(); + const n = b.multiplier.den.toString(); + const p = b.multiplier.num.toString(); + + const formula = '(({0})^({1})*(cos({3})+({2})*sin({3})))^({4})'; + for (let i = 0; i < Number(n); i++) { + const t = evaluate(_.parse(format('(({0})+2*pi*({1}))/({2})', x, i, n))).multiplier.toDecimal(); + _roots.push(evaluate(_.parse(format(formula, r, n, Settings.IMAGINARY, t, p)))); + } + return Vector.fromArray(/** @type {(string | number | NerdamerSymbolType)[]} */ (_roots)); + } + if (symbol.isConstant(true)) { + const signVal = symbol.sign(); + const x = evaluate(symbol.abs()); + const root = _.sqrt(x); + + _roots = [root.clone(), root.negate()]; + + if (signVal < 0) { + _roots = _roots.map(r => _.multiply(r, NerdamerSymbol.imaginary())); + } + } else { + _roots = [_.parse(symbol)]; + } + + return Vector.fromArray(/** @type {(string | number | NerdamerSymbolType)[]} */ (_roots)); + } + + /** + * Rationalizes a symbol + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + function rationalize(symbol) { + if (symbol.isComposite()) { + /** @type {NerdamerSymbolType} */ + let retval = new NerdamerSymbol(0); + let num; + let den; + let retnum; + let retden; + let a; + let b; + let n; + let d; + symbol.each(x => { + num = x.getNum(); + den = x.getDenom(); + retnum = retval.getNum(); + retden = retval.getDenom(); + a = _.multiply(den, retnum); + b = _.multiply(num, retden); + n = _.expand(_.add(a, b)); + d = _.multiply(retden, den); + retval = /** @type {NerdamerSymbolType} */ (_.divide(n, d)); + }, true); + + return retval; + } + return symbol; + } + + /** + * The square root function + * + * @param {string | number | NerdamerSymbolType | VectorType | MatrixType} symbol + * @returns {NerdamerSymbolType} + */ + function sqrt(symbol) { + if (!isSymbol(symbol)) { + symbol = /** @type {NerdamerSymbolType} */ (_.parse(/** @type {string | number} */ (symbol))); + } + + const original = _.symfunction('sqrt', [symbol]); + + // Exit early for EX + if (symbol.group === EX) { + return _.symfunction(SQRT, [symbol]); + } + + if (symbol.fname === '' && symbol.power.equals(1)) { + symbol = symbol.args[0]; + } + + const isNeg = symbol.multiplier.sign() < 0; + + if (Settings.PARSE2NUMBER) { + if (symbol.isConstant() && !isNeg) { + return new NerdamerSymbol(bigDec.sqrt(symbol.multiplier.toDecimal())); + } + if (symbol.isImaginary()) { + return /** @type {NerdamerSymbolType} */ (complex.sqrt(symbol)); + } + if (symbol.group === S) { + return _.symfunction('sqrt', [symbol]); + } + } + + let img; + let retval; + const isConstant = symbol.isConstant(); + + if (symbol.group === CB && symbol.isLinear()) { + let m = sqrt(new NerdamerSymbol(symbol.multiplier)); + for (const s in symbol.symbols) { + if (!Object.hasOwn(symbol.symbols, s)) { + continue; + } + const x = symbol.symbols[s]; + m = /** @type {NerdamerSymbolType} */ (_.multiply(m, /** @type {NerdamerSymbolType} */ (sqrt(x)))); + } + + retval = m; + } + // If the symbol is already sqrt then it's that symbol^(1/4) and we can unwrap it + else if (symbol.fname === SQRT) { + const s = symbol.args[0]; + const ms = symbol.multiplier; + s.setPower(/** @type {FracType} */ (symbol.power).multiply(new Frac(0.25))); + retval = s; + // Grab the multiplier + if (!ms.equals(1)) { + retval = _.multiply(sqrt(_.parse(ms)), retval); + } + } + // If the symbol is a fraction then we don't keep can unwrap it. For instance + // no need to keep sqrt(x^(1/3)) + else if (!symbol.power.isInteger()) { + symbol.setPower(/** @type {FracType} */ (symbol.power).multiply(new Frac(0.5))); + retval = symbol; + } else if (Number(symbol.multiplier) < 0 && symbol.group === S) { + const a = _.parse(symbol.multiplier).negate(); + const b = _.parse(symbol).toUnitMultiplier().negate(); + retval = _.multiply(_.symfunction(Settings.SQRT, [b]), sqrt(a)); + } else { + // Related to issue #401. Since sqrt(a)*sqrt(b^-1) relates in issues, we'll change the form + // to sqrt(a)*sqrt(b)^1 for better simplification + // the sign of the power + const signVal = symbol.power.sign(); + // Remove the sign + symbol.power = symbol.power.abs(); + + // If the symbols is imagary then we place in the imaginary part. We'll return it + // as a product + if (isConstant && symbol.multiplier.lessThan(0)) { + img = NerdamerSymbol.imaginary(); + symbol.multiplier = symbol.multiplier.abs(); + } + + let q = Number(symbol.multiplier.toDecimal()); + const qa = Math.abs(q); + const t = Math.sqrt(qa); + + let m; + // It's a perfect square so take the square + if (isInt(t)) { + m = new NerdamerSymbol(t); + } else if (isInt(q)) { + const factors = Math2.ifactor(q); + let tw = 1; + for (const x in factors) { + if (!Object.hasOwn(factors, x)) { + continue; + } + const n = factors[x]; + const nn = n - (n % 2); // Get out the whole numbers + if (nn) { + // If there is a whole number ... + const w = Number(x) ** nn; + tw *= Number(x) ** (nn / 2); // Add to total wholes + q /= w; // Reduce the number by the wholes + } + } + m = _.multiply(_.symfunction(SQRT, [new NerdamerSymbol(q)]), new NerdamerSymbol(tw)); + } else { + // Reduce the numerator and denominator using prime factorization + const c = [new NerdamerSymbol(symbol.multiplier.num), new NerdamerSymbol(symbol.multiplier.den)]; + /** @type {NerdamerSymbolType[]} */ + const r = [new NerdamerSymbol(1), new NerdamerSymbol(1)]; + /** @type {NerdamerSymbolType[]} */ + const sq = [new NerdamerSymbol(1), new NerdamerSymbol(1)]; + // Capture _ to avoid no-loop-func warning + const parserRef = _; + for (let i = 0; i < 2; i++) { + const n = c[i]; + // Get the prime factors and loop through each. + pfactor(n).each(factor => { + factor = NerdamerSymbol.unwrapPARENS(factor); + const b = factor.clone().toLinear(); + const p = Number(factor.power); + // We'll consider it safe to use the native Number since 2^1000 is already a pretty huge number + const rem = p % 2; // Get the remainder. This will be 1 if 3 since sqrt(n^2) = n where n is positive + const w = (p - rem) / 2; // Get the whole numbers of n/2 + r[i] = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ ( + parserRef.multiply(r[i], parserRef.pow(b, new NerdamerSymbol(w))) + ) + ); + sq[i] = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ ( + parserRef.multiply(sq[i], sqrt(parserRef.pow(b, new NerdamerSymbol(rem)))) + ) + ); + }); + } + m = _.divide(_.multiply(r[0], sq[0]), _.multiply(r[1], sq[1])); + } + + // Strip the multiplier since we already took the sqrt + symbol = symbol.toUnitMultiplier(true); + // If the symbol is one just return one and not the sqrt function + if (symbol.isOne()) { + retval = symbol; + } else if (even(symbol.power.toString())) { + // Just raise it to the 1/2 + retval = _.pow(symbol.clone(), new NerdamerSymbol(0.5)); + } else { + retval = _.symfunction(SQRT, [symbol]); + } + + // Put back the sign that was removed earlier + if (signVal < 0) { + /** @type {NerdamerSymbolType} */ (retval).power.negate(); + } + + if (m) { + retval = _.multiply(m, retval); + } + + if (img) { + retval = _.multiply(img, retval); + } + } + + if (isNegative && Settings.PARSE2NUMBER && retval.text() !== original.text()) { + return _.parse(/** @type {NerdamerSymbolType} */ (retval)); + } + + return /** @type {NerdamerSymbolType} */ (retval); + } + + /** + * The cube root function + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + function cbrt(symbol) { + if (!symbol.isConstant(true)) { + let retval; + + const n = Number(symbol.power) / 3; + // Take the cube root of the multplier + const m = _.pow(_.parse(symbol.multiplier), new NerdamerSymbol(1 / 3)); + // Strip the multiplier + const sym = symbol.toUnitMultiplier(); + + // Simplify the power + if (isInt(n)) { + retval = _.pow(sym.toLinear(), _.parse(String(n))); + } else if (sym.group === CB) { + retval = new NerdamerSymbol(1); + sym.each(x => { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, cbrt(x))); + }); + } else { + retval = _.symfunction('cbrt', [sym]); + } + + return _.multiply(m, retval); + } + return nthroot(symbol, new NerdamerSymbol(3)); + } + + function scientific(symbol, sigfigs) { + // Just set the flag and keep it moving. NerdamerSymbol.toString will deal with how to + // display this + symbol.scientific = sigfigs || 10; + return symbol; + } + + /** + * @param {NerdamerSymbolType} num - The number being raised + * @param {NerdamerSymbolType} p - The exponent + * @param {number} [prec] - The precision wanted + * @param {boolean} [asbig] - True if a bigDecimal is wanted + * @returns {NerdamerSymbolType} + */ + function nthroot(num, p, prec = undefined, asbig = undefined) { + // Clone p and convert to a number if possible + p = evaluate(_.parse(p)); + + // Cannot calculate if p = 0. nthroot(0, 0) => 0^(1/0) => undefined + if (p.equals(0)) { + throw new UndefinedError('Unable to calculate nthroots of zero'); + } + + // Stop computation if it negative and even since we have an imaginary result + if (Number(num) < 0 && even(p)) { + throw new Error('Cannot calculate nthroot of negative number for even powers'); + } + + // Return non numeric values unevaluated + if (!num.isConstant(true)) { + /** @type {NerdamerSymbolType[]} */ + const symArgs = [num, p]; + if (typeof prec !== 'undefined') { + symArgs.push(new NerdamerSymbol(prec)); + } + if (typeof asbig !== 'undefined') { + symArgs.push(new NerdamerSymbol(asbig ? 1 : 0)); + } + return _.symfunction('nthroot', symArgs); + } + + // Evaluate numeric values + if (num.group !== N) { + num = evaluate(num); + } + + // Default is to return a big value + if (typeof asbig === 'undefined') { + asbig = true; + } + + prec ||= 25; + + const signVal = num.sign(); + let retval; + let ans; + + if (signVal < 0) { + num = abs(num); // Remove the sign + } + + if (isInt(num) && p.isConstant()) { + if (Number(num) < 18446744073709551616) { + // 2^64 + ans = Frac.create(Number(num) ** (1 / Number(p))); + } else { + ans = Math2.nthroot(num, p); + } + + if (asbig) { + retval = new NerdamerSymbol(ans); + } else { + retval = new NerdamerSymbol(ans.toDecimal(prec)); + } + + return /** @type {NerdamerSymbolType} */ (_.multiply(new NerdamerSymbol(signVal), retval)); + } + return undefined; + } + + function pfactor(symbol) { + // Fix issue #458 | nerdamer("sqrt(1-(3.3333333550520926e-7)^2)").evaluate().text() + // More Big Number issues >:( + if (symbol.greaterThan(9.999999999998891e41) || symbol.equals(-1)) { + return symbol; + } + // Fix issue #298 + if (symbol.equals(Math.PI)) { + return new NerdamerSymbol(Math.PI); + } + // Evaluate the symbol to merge constants + symbol = evaluate(symbol.clone()); + + let retval; + if (symbol.isConstant()) { + retval = new NerdamerSymbol(1); + const m = symbol.toString(); + if (isInt(m)) { + const factors = Math2.ifactor(m); + for (const factor in factors) { + if (!Object.hasOwn(factors, factor)) { + continue; + } + const p = factors[factor]; + retval = _.multiply( + retval, + _.symfunction('parens', [new NerdamerSymbol(factor).setPower(new Frac(p))]) + ); + } + } else { + const n = pfactor(new NerdamerSymbol(symbol.multiplier.num)); + const d = pfactor(new NerdamerSymbol(symbol.multiplier.den)); + retval = _.multiply(_.symfunction('parens', [n]), _.symfunction('parens', [d]).invert()); + } + } else { + retval = _.symfunction('pfactor', [symbol]); + } + return retval; + } + + /** + * Get's the real part of a complex number. Return number if real + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function realpart(symbol) { + return /** @type {NerdamerSymbolType} */ (symbol.realpart()); + } + + /** + * Get's the imaginary part of a complex number + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function imagpart(symbol) { + return /** @type {NerdamerSymbolType} */ (symbol.imagpart()); + } + + /** + * Computes the conjugate of a complex number + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function conjugate(symbol) { + const re = symbol.realpart(); + const im = symbol.imagpart(); + return /** @type {NerdamerSymbolType} */ (_.add(re, _.multiply(im.negate(), NerdamerSymbol.imaginary()))); + } + + /** + * Returns the arugment of a complex number + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function arg(symbol) { + const re = symbol.realpart(); + const im = symbol.imagpart(); + if (re.isConstant() && im.isConstant()) { + // Right angles + if (im.equals(0) && re.equals(1)) { + return _.parse('0'); + } + if (im.equals(1) && re.equals(0)) { + return _.parse('pi/2'); + } + if (im.equals(0) && re.equals(-1)) { + return _.parse('pi'); + } + if (im.equals(-1) && re.equals(0)) { + return _.parse('-pi/2'); + } + + // 45 degrees + if (im.equals(1) && re.equals(1)) { + return _.parse('pi/4'); + } + if (im.equals(1) && re.equals(-1)) { + return _.parse('pi*3/4'); + } + if (im.equals(-1) && re.equals(1)) { + return _.parse('-pi/4'); + } + if (im.equals(-1) && re.equals(-1)) { + return _.parse('-pi*3/4'); + } + + // All the rest + return new NerdamerSymbol(Math.atan2(Number(im), Number(re))); + } + return _.symfunction('atan2', [im, re]); + } + + /** + * Returns the polarform of a complex number + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function polarform(symbol) { + const p = NerdamerSymbol.toPolarFormArray(symbol); + const theta = p[1]; + const r = p[0]; + const e = _.parse(format('e^({0}*({1}))', Settings.IMAGINARY, theta)); + return /** @type {NerdamerSymbolType} */ (_.multiply(r, e)); + } + + /** + * Returns the rectangular form of a complex number. Does not work for symbolic coefficients + * + * @param {NerdamerSymbolType} symbol + * @returns {NerdamerSymbolType} + */ + function rectform(symbol) { + // TODO: e^((i*pi)/4) + const original = symbol.clone(); + /** + * @typedef {{ + * a: NerdamerSymbolType; + * x: NerdamerSymbolType; + * ax: NerdamerSymbolType; + * b: NerdamerSymbolType; + * }} RectformDecompose + */ + try { + const f = /** @type {RectformDecompose} */ (decomposeFn(symbol, 'e', true)); + const xPower = NerdamerSymbolDeps.isSymbol(f.x.power) ? f.x.power : _.parse(f.x.power); + const p = _.divide(/** @type {NerdamerSymbolType} */ (xPower), NerdamerSymbol.imaginary()); + const q = evaluate(trig.tan(p)); + const _s = _.pow(f.a, new NerdamerSymbol(2)); + const d = /** @type {NerdamerSymbolType} */ (q.getDenom()); + const n = /** @type {NerdamerSymbolType} */ (q.getNum()); + const h = NerdamerSymbol.hyp(n, d); + // Check + if (h.equals(f.a)) { + return /** @type {NerdamerSymbolType} */ (_.add(d, _.multiply(NerdamerSymbol.imaginary(), n))); + } + return /** @type {NerdamerSymbolType} */ (original); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return /** @type {NerdamerSymbolType} */ (original); + } + } + + function symMinMax(f, args) { + args.forEach(x => { + x.numVal = evaluate(x).multiplier; + }); + let l; + let a; + let b; + let _a_val; + let _b_val; + while (true) { + l = args.length; + if (l < 2) { + return args[0]; + } + a = args.pop(); + b = args[l - 2]; + if (f === 'min' ? a.numVal < b.numVal : a.numVal > b.numVal) { + args.pop(); + args.push(a); + } + } + } + + /** + * Returns maximum of a set of numbers + * + * @returns {NerdamerSymbolType} + */ + function max(...args) { + if (allSame(args)) { + return args[0]; + } + if (allNumbers(args)) { + return new NerdamerSymbol(Math.max.apply(null, args)); + } + if (Settings.SYMBOLIC_MIN_MAX && allConstants(args)) { + return symMinMax('max', args); + } + return _.symfunction('max', args); + } + + /** + * Returns minimum of a set of numbers + * + * @returns {NerdamerSymbolType} + */ + function min(...args) { + if (allSame(args)) { + return args[0]; + } + if (allNumbers(args)) { + return new NerdamerSymbol(Math.min.apply(null, args)); + } + if (Settings.SYMBOLIC_MIN_MAX && allConstants(args)) { + return symMinMax('min', args); + } + return _.symfunction('min', args); + } + + /** + * Returns the sign of a number + * + * @param {NerdamerSymbolType} x + * @returns {NerdamerSymbolType} + */ + function sign(x) { + if (x.isConstant(true)) { + return new NerdamerSymbol(Math.sign(/** @type {number} */ (/** @type {unknown} */ (evaluate(x))))); + } + return _.symfunction('sign', [x]); + } + + function sort(symbol, opt) { + opt = opt ? opt.toString() : 'asc'; + const getval = function (e) { + if (e.group === N) { + return e.multiplier; + } + if (e.group === FN) { + if (e.fname === '') { + return getval(e.args[0]); + } + return e.fname; + } + if (e.group === S) { + return e.power; + } + + return e.value; + }; + const symbols = /** @type {NerdamerSymbolType[]} */ ( + isVector(symbol) ? symbol.elements : symbol.collectSymbols() + ); + return new Vector( + symbols.sort((a, b) => { + const aval = getval(a); + const bval = getval(b); + if (opt === 'desc') { + return bval - aval; + } + return aval - bval; + }) + ); + } + + /** + * The log function + * + * @param {NerdamerSymbolType | VectorType | MatrixType} symbol + * @param {NerdamerSymbolType | VectorType | MatrixType} [base] + * @returns {NerdamerSymbolType} + */ + function log(symbol, base = undefined) { + // Narrow types for internal use + const sym = /** @type {NerdamerSymbolType} */ (symbol); + const baseSymbol = base === undefined ? undefined : /** @type {NerdamerSymbolType} */ (base); + + if (sym.equals(1)) { + return new NerdamerSymbol(0); + } + + /** @type {NerdamerSymbolType | undefined} */ + let retval; + + if (sym.fname === SQRT && sym.multiplier.equals(1)) { + retval = /** @type {NerdamerSymbolType} */ (_.divide(log(sym.args[0]), new NerdamerSymbol(2))); + + if (sym.power.sign() < 0) { + retval.negate(); + } + + // Exit early + return retval; + } + + // Log(0) is undefined so complain + if (sym.equals(0)) { + throw new UndefinedError(`${Settings.LOG}(0) is undefined!`); + } + + // Deal with imaginary values + if (sym.isImaginary()) { + return complex.evaluate(sym, Settings.LOG); + } + + if (sym.isConstant() && typeof baseSymbol !== 'undefined' && baseSymbol.isConstant()) { + const logSym = Math.log(/** @type {number} */ (/** @type {unknown} */ (sym))); + const logBase = Math.log(/** @type {number} */ (/** @type {unknown} */ (baseSymbol))); + retval = new NerdamerSymbol(logSym / logBase); + } else if ( + (sym.group === EX && /** @type {NerdamerSymbolType} */ (sym.power).multiplier.lessThan(0)) || + sym.power.toString() === '-1' + ) { + sym.power.negate(); + // Move the negative outside but keep the positive inside :) + retval = log(sym).negate(); + } else if (sym.value === 'e' && sym.multiplier.equals(1)) { + const p = sym.power; + retval = isSymbol(p) ? /** @type {NerdamerSymbolType} */ (p) : new NerdamerSymbol(p); + } else if (sym.group === FN && sym.fname === 'exp') { + const s = sym.args[0]; + if (sym.multiplier.equals(1)) { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(s, new NerdamerSymbol(sym.power))); + } else { + retval = _.symfunction(Settings.LOG, [sym]); + } + } else if (Settings.PARSE2NUMBER && isNumericSymbol(sym)) { + // Parse for safety. + const numSym = /** @type {NerdamerSymbolType} */ ( + _.parse(/** @type {string | number} */ (/** @type {unknown} */ (sym))) + ); + + let imgPart; + if (numSym.multiplier.lessThan(0)) { + numSym.negate(); + imgPart = _.multiply(new NerdamerSymbol(Math.PI), new NerdamerSymbol('i')); + } + + retval = new NerdamerSymbol(Math.log(/** @type {number} */ (numSym.multiplier.toDecimal()))); + + if (imgPart) { + retval = /** @type {NerdamerSymbolType} */ (_.add(retval, imgPart)); + } + } else { + let s; + if (!sym.power.equals(1) && !sym.contains('e') && sym.multiplier.isOne()) { + s = sym.group === EX ? sym.power : new NerdamerSymbol(sym.power); + sym.toLinear(); + } + // Log(a,a) = 1 since the base is allowed to be changed. + // This was pointed out by Happypig375 in issue #280 + const args = typeof baseSymbol === 'undefined' ? [sym] : [sym, baseSymbol]; + if (args.length > 1 && allSame(/** @type {NerdamerSymbolType[]} */ (args))) { + retval = new NerdamerSymbol(1); + } else { + retval = _.symfunction(Settings.LOG, args); + } + + if (s) { + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(/** @type {NerdamerSymbolType} */ (s), retval) + ); + } + } + + return retval; + } + + /** + * Round a number up to s decimal places + * + * @param {NerdamerSymbolType} x + * @param {NerdamerSymbolType | number} [s] - The number of decimal places + * @returns {NerdamerSymbolType} + */ + function round(x, s) { + // Convert number to NerdamerSymbol if needed + if (typeof s === 'number') { + s = new NerdamerSymbol(s); + } + const sIsConstant = (s && s.isConstant()) || typeof s === 'undefined'; + if (x.isConstant() && sIsConstant) { + let v; + let e; + let exponent; + /** @type {NerdamerSymbolType | string} */ + v = x; + // Round the coefficient of then number but not the actual decimal value + // we know this because a negative number was passed + if (s && s.lessThan(0)) { + s = abs(s); + // Convert the number to exponential form + e = Number(x).toExponential().toString().split('e'); + // Point v to the coefficient of then number + v = e[0]; + // NerdamerSet the expontent + exponent = e[1]; + } + // Round the number to the requested precision + const retval = new NerdamerSymbol(nround(Number(v), Number(s) || 0)); + // If there's a exponent then put it back + return /** @type {NerdamerSymbolType} */ ( + _.multiply(retval, _.pow(new NerdamerSymbol(10), new NerdamerSymbol(exponent || 0))) + ); + } + + const roundArgs = [x]; + if (typeof s !== 'undefined') { + roundArgs.push(s); + } + return _.symfunction('round', roundArgs); + } + + /** + * Gets the quadrant of the trig function + * + * @param {FracType} m + * @returns {number} + */ + function getQuadrant(m) { + let v = Number(m) % 2; + let quadrant; + + if (v < 0) { + v = 2 + v; + } // Put it in terms of pi + + if (v >= 0 && v <= 0.5) { + quadrant = 1; + } else if (v > 0.5 && v <= 1) { + quadrant = 2; + } else if (v > 1 && v <= 1.5) { + quadrant = 3; + } else { + quadrant = 4; + } + return quadrant; + } + + /* + * Serves as a bridge between numbers and bigNumbers + * @param {FracType|number} n + * @returns {NerdamerSymbolType} + */ + function bigConvert(n) { + if (!isFinite(n)) { + const signVal = Math.sign(n); + const r = new NerdamerSymbol(String(Math.abs(n))); + r.multiplier = r.multiplier.multiply(new Frac(signVal)); + return r; + } + if (isSymbol(n)) { + return n; + } + if (typeof n === 'number') { + try { + n = Frac.simple(n); + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + n = new Frac(n); + } + } + + const symbol = new NerdamerSymbol(0); + symbol.multiplier = n; + return symbol; + } + function clean(symbol) { + // Handle functions with numeric values + // handle denominator within denominator + // handle trig simplifications + const g = symbol.group; + let retval; + // Now let's get to work + if (g === CP) { + const num = symbol.getNum(); + const den = symbol.getDenom() || new NerdamerSymbol(1); + const p = Number(symbol.power); + /** @type {NerdamerSymbolType} */ + let factor = new NerdamerSymbol(1); + if (Math.abs(p) === 1) { + den.each(x => { + if (x.group === CB) { + factor = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (_.multiply(factor, clean(x.getDenom()))) + ); + } else if (x.power.lessThan(0)) { + factor = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (_.multiply(factor, clean(x.clone().toUnitMultiplier()))) + ); + } + }); + + /** @type {NerdamerSymbolType} */ + let newDen = new NerdamerSymbol(0); + // Now divide out the factor and add to new den + den.each(x => { + newDen = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (_.add(_.divide(x, factor.clone()), newDen)) + ); + }); + + factor.invert(); // Invert so it can be added to the top + /** @type {NerdamerSymbolType | undefined} */ + let newNum; + if (num.isComposite()) { + newNum = new NerdamerSymbol(0); + num.each(x => { + newNum = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (_.add(_.multiply(clean(x), factor.clone()), newNum)) + ); + }); + } else { + newNum = /** @type {NerdamerSymbolType} */ (_.multiply(factor, num)); + } + + retval = /** @type {NerdamerSymbolType} */ (_.divide(newNum, newDen)); + } + } else if (g === CB) { + retval = new NerdamerSymbol(1); + symbol.each(x => { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, _.clean(x))); + }); + } else if (g === FN) { + if (symbol.args.length === 1 && symbol.args[0].isConstant()) { + retval = block('PARSE2NUMBER', () => _.parse(symbol), true); + } + } + + retval ||= symbol; + + return retval; + } + + /** + * A wrapper for the expand function + * + * @param {NerdamerSymbolType} symbol + * @param {ExpandOptions} [opt] + * @returns {NerdamerSymbolType} + */ + function expandall(symbol, opt) { + opt ||= { + expand_denominator: true, + expand_functions: true, + }; + return /** @type {NerdamerSymbolType} */ (expand(symbol, opt)); + } + /** + * Expands a symbol + * + * @param {NerdamerSymbolType | VectorType | MatrixType} symbol + * @param {ExpandOptions} [opt] + * @returns {NerdamerSymbolType | VectorType | MatrixType} + */ + // Old expand + function expand(symbol, opt) { + if (Array.isArray(symbol)) { + return /** @type {NerdamerSymbolType | VectorType | MatrixType} */ ( + /** @type {unknown} */ (symbol.map(x => expand(x, opt))) + ); + } + // Vector/Matrix have their own expand method - delegate to it + if ('expand' in symbol && typeof symbol.expand === 'function') { + return /** @type {VectorType | MatrixType} */ (symbol).expand(opt); + } + // From this point on, symbol is definitely a NerdamerSymbol + /** @type {NerdamerSymbolType} */ + const sym = /** @type {NerdamerSymbolType} */ (symbol); + opt ||= {}; + // Deal with parenthesis + if (sym.group === FN && sym.fname === '') { + const f = expand(sym.args[0], opt); + const x = expand(_.pow(f, _.parse(sym.power)), opt); + return /** @type {NerdamerSymbolType} */ ( + _.multiply(_.parse(sym.multiplier), x) + ).distributeMultiplier(); + } + // We cannot expand these groups so no need to waste time. Just return and be done. + if ([N, P, S].indexOf(sym.group) !== -1) { + return sym; // Nothing to do + } + + const original = sym.clone(); + + // NerdamerSet up a try-catch block. If anything goes wrong then we simply return the original symbol + try { + // Store the power and multiplier + const m = sym.multiplier.toString(); + const p = Number(sym.power); + let retval = sym; + + // Handle (a+b)^2 | (x+x^2)^2 + if (sym.isComposite() && isInt(sym.power) && p > 0) { + const n = p - 1; + // Strip the expression of it's multiplier and power. We'll call it f. The power will be p and the multiplier m. + /** @type {NerdamerSymbolType} */ + let f = new NerdamerSymbol(0); + + sym.each((/** @type {NerdamerSymbolType} */ x) => { + f = /** @type {NerdamerSymbolType} */ (_.add(f, expand(_.parse(x), opt))); + }); + + /** @type {NerdamerSymbolType} */ + let expanded = _.parse(f); + + for (let i = 0; i < n; i++) { + expanded = mix(expanded, f, opt); + } + + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.parse(m), expanded) + ).distributeMultiplier(); + } else if (sym.group === FN && opt.expand_functions === true) { + const args = []; + // Expand function the arguments + sym.args.forEach(x => { + args.push(expand(x, opt)); + }); + // Put back the power and multiplier + retval = /** @type {NerdamerSymbolType} */ ( + _.pow(_.symfunction(sym.fname, args), _.parse(sym.power)) + ); + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, _.parse(sym.multiplier))); + } else if (sym.isComposite() && isInt(sym.power) && p < 0 && opt.expand_denominator === true) { + // Invert it. Expand it and then re-invert it. + const inverted = sym.invert(); + retval = /** @type {NerdamerSymbolType} */ (expand(inverted, opt)); + retval.invert(); + } else if (sym.group === CB) { + const rank = function (s) { + switch (s.group) { + case CP: + return 0; + case PL: + return 1; + case CB: + return 2; + case FN: + return 3; + default: + return 4; + } + }; + // Consider (a+b)(c+d). The result will be (a*c+a*d)+(b*c+b*d). + // We start by moving collecting the symbols. We want others>FN>CB>PL>CP + const symbols = /** @type {NerdamerSymbolType[]} */ (sym.collectSymbols()) + .sort((a, b) => rank(b) - rank(a)) + // Distribute the power to each symbol and expand + .map(s => { + const x = _.pow(s, _.parse(String(p))); + const e = /** @type {NerdamerSymbolType} */ (expand(x, opt)); + return e; + }); + + /** @type {NerdamerSymbolType} */ + let f = /** @type {NerdamerSymbolType} */ (symbols.pop()); + + // If the first symbols isn't a composite then we're done + if (f.isComposite() && f.isLinear()) { + symbols.forEach(s => { + f = /** @type {NerdamerSymbolType} */ (mix(f, s, opt)); + }); + + // If f is of group PL or CP then we can expand some more + if (f.isComposite()) { + if (Number(f.power) > 1) { + f = /** @type {NerdamerSymbolType} */ (expand(_.pow(f, _.parse(f.power)), opt)); + } + // Put back the multiplier + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(_.parse(m), f) + ).distributeMultiplier(); + } else { + // Everything is expanded at this point so if it's still a CB + // then just return the symbol + retval = f; + } + } else { + // Just multiply back in the expanded form of each + retval = f; + symbols.forEach(s => { + retval = /** @type {NerdamerSymbolType} */ (_.multiply(retval, s)); + }); + // Put back the multiplier + retval = /** @type {NerdamerSymbolType} */ ( + _.multiply(retval, _.parse(m)) + ).distributeMultiplier(); + } + + // TODO: This exists solely as a quick fix for sqrt(11)*sqrt(33) not simplifying. + if (retval.group === CB) { + retval = _.parse(retval); + } + } else { + // Otherwise just return the expression + retval = sym; + } + // Final cleanup and return + return retval; + } catch (e) { + if (e.message === 'timeout') { + throw e; + } + return original; + } + } + + /** + * Returns an identity matrix of nxn + * + * @param {number} n + * @returns {MatrixType} + */ + function imatrix(n) { + return Matrix.identity(n); + } + + /** + * Retrieves and item from a vector + * + * @param {VectorType} vec + * @param {NerdamerSymbolType} index + * @returns {VectorType | NerdamerSymbolType} + */ + function vecget(vec, index) { + if (index.isConstant() && isInt(index)) { + return /** @type {NerdamerSymbolType | VectorType} */ (vec.elements[Number(index)]); + } + return _.symfunction('vecget', [/** @type {VectorType} */ (vec), index]); + } + + /** + * Removes duplicates from a vector + * + * @param {VectorType} vec + * @param {number} tolerance + * @returns {VectorType} + */ + function vectrim(vec, tolerance) { + tolerance = typeof tolerance === 'undefined' ? 1e-14 : tolerance; + + vec = vec.clone(); + + tolerance = Number(tolerance); + // Place algebraic solutions first + vec.elements.sort( + (a, b) => /** @type {NerdamerSymbolType} */ (b).group - /** @type {NerdamerSymbolType} */ (a).group + ); + // Depending on the start point we may have duplicates so we need to clean those up a bit. + // start by creating an object with the solution and the numeric value. This way we don't destroy algebraic values + vec.elements = removeDuplicates(vec.elements, (a, b) => { + const diff = Number(/** @type {NerdamerSymbolType} */ (_.subtract(evaluate(a), evaluate(b))).abs()); + return diff <= tolerance; + }); + + return vec; + } + + /** + * NerdamerSet a value for a vector at a given index + * + * @param {VectorType} vec + * @param {NerdamerSymbolType} index + * @param {NerdamerSymbolType} value + * @returns {VectorType | NerdamerSymbolType} + */ + function vecset(vec, index, value) { + if (!index.isConstant) { + return _.symfunction('vecset', [ + /** @type {VectorType} */ (/** @type {unknown} */ (vec)), + index, + value, + ]); + } + vec.elements[Number(index)] = value; + return vec; + } + + /** + * @param {MatrixType} mat + * @param {NerdamerSymbolType} i + * @param {NerdamerSymbolType} j + * @returns {NerdamerSymbolType} + */ + function matget(mat, i, j) { + if (i.isConstant() && j.isConstant()) { + return /** @type {NerdamerSymbolType} */ (mat.elements[Number(i)][Number(j)]); + } + return _.symfunction('matget', [/** @type {MatrixType} */ (mat), i, j]); + } + + /** + * @param {MatrixType} mat + * @param {NerdamerSymbolType} i + * @returns {VectorType | NerdamerSymbolType} + */ + function matgetrow(mat, i) { + if (i.isConstant()) { + return Vector.fromArray(/** @type {NerdamerSymbolType[]} */ (mat.elements[Number(i)])); + } + return _.symfunction('matgetrow', [/** @type {MatrixType} */ (mat), i]); + } + + /** + * Sets a row in a matrix + * + * @param {MatrixType} mat + * @param {NerdamerSymbolType} i + * @param {VectorType} x + * @returns {MatrixType | NerdamerSymbolType} + */ + function matsetrow(mat, i, x) { + // Handle symbolics + if (!i.isConstant()) { + return _.symfunction('matsetrow', [/** @type {MatrixType} */ (mat), i, /** @type {VectorType} */ (x)]); + } + if (mat.elements[Number(i)].length !== x.elements.length) { + throw new DimensionError('Matrix row must match row dimensions!'); + } + const M = /** @type {MatrixType} */ (mat.clone()); + M.elements[Number(i)] = x.clone().elements; + return M; + } + + /** + * Gets a column from a matrix + * + * @param {MatrixType} mat + * @param {NerdamerSymbolType} colIndex + * @returns {MatrixType | NerdamerSymbolType} + */ + function matgetcol(mat, colIndex) { + // Handle symbolics + if (!colIndex.isConstant()) { + return _.symfunction('matgetcol', [/** @type {MatrixType} */ (mat), colIndex]); + } + const colIndexNum = Number(colIndex); + /** @type {MatrixType} */ + const M = Matrix.fromArray([]); + mat.each((x, i, j) => { + if (j === colIndexNum) { + M.elements.push([x.clone()]); + } + }); + return M; + } + + /** + * Sets a column in a matrix + * + * @param {MatrixType} mat + * @param {NerdamerSymbolType} j + * @param {MatrixType} col + * @returns {MatrixType | NerdamerSymbolType} + */ + function matsetcol(mat, j, col) { + // Handle symbolics + if (!j.isConstant()) { + return _.symfunction('matsetcol', [ + /** @type {MatrixType} */ (mat), + j, + /** @type {MatrixType} */ (col), + ]); + } + const jNum = Number(j); + if (mat.rows() !== col.elements.length) { + throw new DimensionError('Matrix column length must match number of rows!'); + } + col.each( + /** @type {(element: NerdamerSymbolType, row: number, col: number) => void} */ ( + /** @type {unknown} */ ( + (/** @type {NerdamerSymbolType | VectorType | MatrixType} */ x, i) => { + mat.set(i - 1, jNum, /** @type {VectorType} */ (x).elements[0].clone()); + } + ) + ) + ); + return mat; + } + + function matset(mat, i, j, value) { + mat.elements[i][j] = value; + return mat; + } + + // The constructor for vectors + function vector(...args) { + return new Vector(args); + } + + // The constructor for matrices + function matrix(...args) { + return Matrix.fromArray(args); + } + + // The constructor for sets + // eslint-disable-next-line no-shadow -- intentionally shadows outer set for parser function + function set(...args) { + return NerdamerSet.fromArray(args); + } + + function determinant(symbol) { + if (isMatrix(symbol)) { + return symbol.determinant(); + } + return symbol; + } + + function size(symbol) { + let retval; + if (isMatrix(symbol)) { + retval = [new NerdamerSymbol(symbol.cols()), new NerdamerSymbol(symbol.rows())]; + } else if (isVector(symbol) || isSet(symbol)) { + retval = new NerdamerSymbol(symbol.elements.length); + } else { + err('size expects a matrix or a vector'); + } + return retval; + } + + function dot(vec1, vec2) { + if (isMatrix(vec1)) { + vec1 = new Vector(vec1); + } + if (isMatrix(vec2)) { + vec2 = new Vector(vec2); + } + + if (isVector(vec1) && isVector(vec2)) { + return vec1.dot(vec2); + } + + return _.multiply(vec1.clone(), vec2.clone()); + // Err('function dot expects 2 vectors'); + } + + function cross(vec1, vec2) { + if (isMatrix(vec1)) { + vec1 = new Vector(vec1); + } + if (isMatrix(vec2)) { + vec2 = new Vector(vec2); + } + + if (isVector(vec1) && isVector(vec2)) { + return vec1.cross(vec2); + } + + return _.multiply(vec1.clone(), vec2.clone()); + // Err('function cross expects 2 vectors'); + } + + function transpose(mat) { + if (isMatrix(mat)) { + return mat.transpose(); + } + return err('function transpose expects a matrix'); + } + + function invert(mat) { + if (isMatrix(mat)) { + return mat.invert(); + } + return err('invert expects a matrix'); + } + + // Basic set functions + function union(set1, set2) { + return set1.union(set2); + } + + function intersection(set1, set2) { + return set1.intersection(set2); + } + + function contains(set1, e) { + return set1.contains(e); + } + + function difference(set1, set2) { + return set1.difference(set2); + } + + function intersects(set1, set2) { + return new NerdamerSymbol(Number(set1.intersects(set2))); + } + + function isSubset(set1, set2) { + return new NerdamerSymbol(Number(set1.isSubset(set2))); + } + function primes(a, b) { + b ??= a; + const primeList = PRIMES.slice(a, b).map(p => new NerdamerSymbol(p)); + if (primeList.length === 1) { + return primeList[0]; + } + if (primeList.length === 0) { + return new NerdamerSymbol(0); + } + return new Vector(primeList); + } + + function print(...args) { + args.forEach(x => { + // eslint-disable-next-line no-console + console.log(x.toString()); + }); + } + + function testSQRT(symbol) { + // Wrap the symbol in sqrt. This eliminates one more check down the line. + if (!isSymbol(symbol.power) && symbol.power.absEquals(0.5)) { + const signVal = symbol.power.sign(); + // Don't devide the power directly. Notice the use of toString. This makes it possible + // to use a bigNumber library in the future + const retval = sqrt(symbol.group === P ? new NerdamerSymbol(symbol.value) : symbol.toLinear()); + // Place back the sign of the power + if (signVal < 0) { + retval.invert(); + } + return retval; + } + return symbol; + } + + // Try to reduce a symbol by pulling its power + function testPow(symbol) { + if (symbol.group === P) { + const v = symbol.value; + + const fct = primeFactors(v)[0]; + + // Safety + if (!fct) { + warn('Unable to compute prime factors. This should not happen. Please review and report.'); + return symbol; + } + + const n = new Frac(Math.log(v) / Math.log(fct)); + const p = n.multiply(symbol.power); + + // We don't want a more complex number than before + if (p.den > symbol.power.den) { + return symbol; + } + + if (isInt(p)) { + symbol = new NerdamerSymbol(fct ** Number(p)); + } else { + symbol = /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(fct)) + ).setPower(p); + } + } + + return symbol; + } + + // Link the functions to the parse so they're available outside of the library. + // This is strictly for convenience and may be deprecated. + this.expand = expand; + this.round = round; + this.clean = /** @type {ParserType['clean']} */ (clean); + this.sqrt = sqrt; + this.cbrt = cbrt; + this.abs = /** @type {ParserType['abs']} */ (abs); + this.log = log; + this.rationalize = /** @type {ParserType['rationalize']} */ (rationalize); + this.nthroot = /** @type {ParserType['nthroot']} */ (nthroot); + this.arg = /** @type {ParserType['arg']} */ (arg); + this.conjugate = /** @type {ParserType['conjugate']} */ (conjugate); + this.imagpart = /** @type {ParserType['imagpart']} */ (imagpart); + this.realpart = /** @type {ParserType['realpart']} */ (realpart); + + // TODO: + // Utilize the function below instead of the linked function + this.getFunction = function getFunction(name) { + return functions[name][0]; + }; + + // Parser.methods =============================================================== + this.addPreprocessor = function addPreprocessor(name, action, order, shiftCells) { + const { names } = preprocessors; + const { actions } = preprocessors; + if (typeof action !== 'function') // The person probably forgot to specify a name + { + throw new Error('Incorrect parameters. Function expected!'); + } + if (!order) { + names.push(name); + actions.push(action); + } else if (shiftCells) { + names.splice(order, 0, name); + actions.splice(order, 0, action); + } else { + names[order] = name; + actions[order] = action; + } + }; + + /** @returns {Record} */ + this.getPreprocessors = function getPreprocessors() { + /** @type {Record} */ + const result = {}; + for (let i = 0, l = preprocessors.names.length; i < l; i++) { + const name = preprocessors.names[i]; + result[name] = { + order: i, + action: preprocessors.actions[i], + }; + } + return result; + }; + + this.removePreprocessor = function removePreprocessor(name, shiftCells) { + const i = preprocessors.names.indexOf(name); + if (shiftCells) { + remove(preprocessors.names, i); + remove(preprocessors.actions, i); + } else { + preprocessors.names[i] = undefined; + preprocessors.actions[i] = undefined; + } + }; + + // The loader for functions which are not part of Math2 + /** @this {{ params: string[]; body: string }} */ + this.mappedFunction = function mappedFunction(...args) { + /** @type {Record} */ + const subs = {}; + const { params } = this; + + for (let i = 0; i < params.length; i++) { + subs[params[i]] = String(args[i]); + } + + return _.parse(this.body, subs); + }; + /** + * Adds two symbols + * + * @param {ArithmeticOperand} a + * @param {ArithmeticOperand} b + * @returns {ArithmeticOperand} + */ + this.add = function add(a, b) { + let aIsSymbol = isSymbol(a); + let bIsSymbol = isSymbol(b); + // We're dealing with two symbols + if (aIsSymbol && bIsSymbol) { + // Cast to NerdamerSymbol since we've verified with isSymbol + /** @type {NerdamerSymbolType} */ + let symA = /** @type {NerdamerSymbolType} */ (a); + /** @type {NerdamerSymbolType} */ + let symB = /** @type {NerdamerSymbolType} */ (b); + // Forward the adding of symbols with units to the Unit module + if (symA.unit || symB.unit) { + return _.Unit.add(symA, symB); + } + // Handle Infinity + // https://www.encyclopediaofmath.org/index.php/Infinity + if (symA.isInfinity || symB.isInfinity) { + const aneg = symA.multiplier.lessThan(0); + const bneg = symB.multiplier.lessThan(0); + + if (symA.isInfinity && symB.isInfinity && aneg !== bneg) { + throw new UndefinedError(`(${symA})+(${symB}) is not defined!`); + } + + const inf = NerdamerSymbol.infinity(); + if (bneg) { + inf.negate(); + } + return inf; + } + + if (symA.isComposite() && symA.isLinear() && symB.isComposite() && symB.isLinear()) { + symA.distributeMultiplier(); + symB.distributeMultiplier(); + // Fix for issue #606 + if (symB.length > symA.length && symA.group === symB.group) { + [symA, symB] = [symB, symA]; + } + } + + // No need to waste time on zeroes + if (symA.multiplier.equals(0)) { + return symB; + } + if (symB.multiplier.equals(0)) { + return symA; + } + + if (symA.isConstant() && symB.isConstant() && Settings.PARSE2NUMBER) { + const result = new NerdamerSymbol( + symA.multiplier.add(symB.multiplier).toDecimal(Settings.PRECISION) + ); + return result; + } + + let g1 = symA.group; + let g2 = symB.group; + let ap = symA.power.toString(); + let bp = symB.power.toString(); + + // Always keep the greater group on the left. + if (g1 < g2 || (g1 === g2 && Number(ap) > Number(bp) && Number(bp) > 0)) { + return this.add(symB, symA); + } + + /* Note to self: Please don't forget about this dilemma ever again. In this model PL and CB goes crazy + * because it doesn't know which one to prioritize. */ + // correction to PL dilemma + if (g1 === CB && g2 === PL && symA.value === symB.value) { + // Swap + const t = symA; + symA = symB; + symB = t; + g1 = symA.group; + g2 = symB.group; + ap = symA.power.toString(); + bp = symB.power.toString(); + } + + const powEQ = ap === bp; + let v1 = symA.value; + let v2 = symB.value; + const aIsComposite = symA.isComposite(); + const bIsComposite = symB.isComposite(); + let h1; + let h2; + let result; + + if (aIsComposite) { + h1 = text(symA, 'hash'); + } + if (bIsComposite) { + h2 = text(symB, 'hash'); + } + + if (g1 === CP && g2 === CP && symB.isLinear() && !symA.isLinear() && h1 !== h2) { + return this.add(symB, symA); + } + + // PL & PL should compare hashes and not values e.g. compare x+x^2 with x+x^3 and not x with x + if (g1 === PL && g2 === PL) { + v1 = h1; + v2 = h2; + } + + const PN = g1 === P && g2 === N; + const PNEQ = symA.value === symB.multiplier.toString(); + const valEQ = v1 === v2 || (h1 === h2 && h1 !== undefined) || (PN && PNEQ); + + // Equal values, equal powers + if (valEQ && powEQ && g1 === g2) { + // Make sure to convert N to something P can work with + if (PN) { + symB = symB.convert(P); + } // CL + + // handle PL + if (g1 === PL && (g2 === S || g2 === P)) { + symA.distributeMultiplier(); + result = symA.attach(symB); + } else { + result = symA; // CL + if ( + symA.multiplier.isOne() && + symB.multiplier.isOne() && + g1 === CP && + symA.isLinear() && + symB.isLinear() + ) { + for (const s in symB.symbols) { + if (!Object.hasOwn(symB.symbols, s)) { + continue; + } + const x = symB.symbols[s]; + result.attach(x); + } + } else { + result.multiplier = result.multiplier.add(symB.multiplier); + } + } + } + // Equal values uneven powers + else if (valEQ && g1 !== PL) { + // Break the tie for e.g. (x+1)+((x+1)^2+(x+1)^3) + if (g1 === CP && g2 === PL) { + symB.insert(symA, 'add'); + result = symB; + } else { + result = NerdamerSymbol.shell(PL).attach([symA, symB]); + // Update the hash + result.value = g1 === PL ? h1 : v1; + } + } else if (aIsComposite && symA.isLinear()) { + let canIterate = g1 === g2; + const bothPL = g1 === PL && g2 === PL; + + // We can only iterate group PL if they values match + if (bothPL) { + canIterate = symA.value === symB.value; + } + // Distribute the multiplier over the entire symbol + symA.distributeMultiplier(); + + if (symB.isComposite() && symB.isLinear() && canIterate) { + symB.distributeMultiplier(); + // CL + for (const s in symB.symbols) { + if (!Object.hasOwn(symB.symbols, s)) { + continue; + } + const x = symB.symbols[s]; + symA.attach(x); + } + result = symA; + } + // Handle cases like 2*(x+x^2)^2+2*(x+x^2)^3+4*(x+x^2)^2 + else if ((bothPL && symA.value !== h2) || (g1 === PL && !valEQ)) { + result = NerdamerSymbol.shell(CP).attach([symA, symB]); + result.updateHash(); + } else { + result = symA.attach(symB); + } + } else { + if (g1 === FN && symA.fname === SQRT && g2 !== EX && symB.power.equals(0.5)) { + const m = symB.multiplier.clone(); + symB = sqrt(symB.toUnitMultiplier().toLinear()); + symB.multiplier = m; + } + // Fix for issue #3 and #159 + if (symA.length === 2 && symB.length === 2 && even(symA.power) && even(symB.power)) { + result = _.add(expand(symA), expand(symB)); + } else { + result = NerdamerSymbol.shell(CP).attach([symA, symB]); + result.updateHash(); + } + } + + if (result.multiplier.equals(0)) { + result = new NerdamerSymbol(0); + } + + // Make sure to remove unnecessary wraps + // At this point result is always a NerdamerSymbol + const symbolResult = /** @type {NerdamerSymbolType} */ (result); + if (symbolResult.length === 1) { + const m = symbolResult.multiplier; + const unwrapped = /** @type {NerdamerSymbolType} */ (firstObject(symbolResult.symbols)); + unwrapped.multiplier = unwrapped.multiplier.multiply(m); + return unwrapped; + } + + return result; + } + // Keep symbols to the right + if (bIsSymbol && !aIsSymbol) { + const tempOp = a; + a = b; + b = tempOp; // Swap + const tempBool = bIsSymbol; + bIsSymbol = aIsSymbol; + aIsSymbol = tempBool; + } + + const bIsMatrix = isMatrix(b); + + if (aIsSymbol && bIsMatrix) { + const M = new Matrix(); + const bMatrix = /** @type {MatrixType} */ (b); + bMatrix.eachElement((e, i, j) => { + M.set( + i, + j, + /** @type {NerdamerSymbolType} */ (_.add(/** @type {NerdamerSymbolType} */ (a).clone(), e)) + ); + }); + + b = M; + } else if (isMatrix(a) && bIsMatrix) { + b = /** @type {MatrixType} */ (a).add(/** @type {MatrixType} */ (b)); + } else if (aIsSymbol && isVector(b)) { + const bVec = /** @type {VectorType} */ (b); + bVec.each((el, i) => { + i--; + bVec.elements[i] = /** @type {NerdamerSymbolType} */ ( + _.add(/** @type {NerdamerSymbolType} */ (a).clone(), bVec.elements[i]) + ); + }); + } else if (isVector(a) && isVector(b)) { + const aVec = /** @type {VectorType} */ (a); + const bVec = /** @type {VectorType} */ (b); + bVec.each((el, i) => { + i--; + bVec.elements[i] = /** @type {NerdamerSymbolType} */ (_.add(aVec.elements[i], bVec.elements[i])); + }); + } else if (isVector(a) && isMatrix(b)) { + // Try to convert a to a matrix + return /** @type {MatrixType} */ (_.add(/** @type {MatrixType} */ (b), /** @type {VectorType} */ (a))); + } else if (isMatrix(a) && isVector(b)) { + if (b.elements.length === a.rows()) { + const M = new Matrix(); + const l = a.cols(); + b.each((e, i) => { + const row = []; + if (isVector(e)) { + for (let j = 0; j < l; j++) { + row.push(_.add(a.elements[i - 1][j].clone(), e.elements[j].clone())); + } + } else { + for (let j = 0; j < l; j++) { + row.push(_.add(a.elements[i - 1][j].clone(), e.clone())); + } + } + M.elements.push(row); + }); + return M; + } + err('Dimensions must match!'); + } + return b; + }; + /** + * Gets called when the parser finds the - operator. Not the prefix operator. See this.add + * + * @param {ArithmeticOperand} a + * @param {ArithmeticOperand} b + * @returns {ArithmeticOperand} + */ + this.subtract = function subtract(a, b) { + const aIsSymbol = isSymbol(a); + const bIsSymbol = isSymbol(b); + let _t; + + if (aIsSymbol && bIsSymbol) { + const aSymbol = /** @type {NerdamerSymbolType} */ (a); + const bSymbol = /** @type {NerdamerSymbolType} */ (b); + if (aSymbol.unit || bSymbol.unit) { + return _.Unit.subtract(aSymbol, bSymbol); + } + return this.add(aSymbol, bSymbol.negate()); + } + if (bIsSymbol && isVector(a)) { + b = /** @type {VectorType} */ ( + a.map( + x => + /** @type {NerdamerSymbolType} */ ( + _.subtract(x, /** @type {NerdamerSymbolType} */ (b).clone()) + ) + ) + ); + } else if (aIsSymbol && isVector(b)) { + b = /** @type {VectorType} */ ( + b.map( + x => + /** @type {NerdamerSymbolType} */ ( + _.subtract(/** @type {NerdamerSymbolType} */ (a).clone(), x) + ) + ) + ); + } else if ((isVector(a) && isVector(b)) || (isCollection(a) && isCollection(b))) { + if (a.dimensions() === b.dimensions()) { + // Both a and b are the same type (either Vector or Collection) + b = /** @type {VectorType} */ ( + /** @type {VectorType | CollectionType} */ (a).subtract( + /** @type {VectorType & CollectionType} */ (b) + ) + ); + } else { + _.error('Unable to subtract vectors/collections. Dimensions do not match.'); + } + } else if (isMatrix(a) && isVector(b)) { + if (b.elements.length === a.rows()) { + const M = new Matrix(); + const l = a.cols(); + b.each((e, i) => { + const row = []; + for (let j = 0; j < l; j++) { + row.push(_.subtract(a.elements[i - 1][j].clone(), e.clone())); + } + M.elements.push(row); + }); + return M; + } + err('Dimensions must match!'); + } else if (isVector(a) && isMatrix(b)) { + const M = b.clone().negate(); + return /** @type {MatrixType} */ (_.add(/** @type {MatrixType} */ (M), a)); + } else if (isMatrix(a) && isMatrix(b)) { + b = a.subtract(b); + } else if (isMatrix(a) && bIsSymbol) { + const M = new Matrix(); + a.each((x, i, j) => { + M.set(i, j, /** @type {NerdamerSymbolType} */ (_.subtract(x, b.clone()))); + }); + b = M; + } else if (aIsSymbol && isMatrix(b)) { + const M = new Matrix(); + b.each((x, i, j) => { + M.set(i, j, /** @type {NerdamerSymbolType} */ (_.subtract(a.clone(), x))); + }); + b = M; + } + return b; + }; + /** + * Gets called when the parser finds the * operator. See this.add + * + * @param {ArithmeticOperand} a + * @param {ArithmeticOperand} b + * @returns {ArithmeticOperand} + */ + this.multiply = function multiply(a, b) { + let aIsSymbol = isSymbol(a); + let bIsSymbol = isSymbol(b); + // We're dealing with function assignment here + if (aIsSymbol && b instanceof Collection) { + /** @type {CollectionType} */ (b).elements.push(/** @type {NerdamerSymbolType} */ (a)); + return /** @type {ArithmeticOperand} */ (/** @type {unknown} */ (b)); + } + if (aIsSymbol && bIsSymbol) { + // Cast to NerdamerSymbol since we've verified with isSymbol + /** @type {NerdamerSymbolType} */ + let symA = /** @type {NerdamerSymbolType} */ (a); + /** @type {NerdamerSymbolType} */ + let symB = /** @type {NerdamerSymbolType} */ (b); + // If it has a unit then add it and return it right away. + if (symB.isUnit) { + const result = symA.clone(); + symA.unit = symB; + return result; + } + + // If it has units then just forward that problem to the unit module + if (symA.unit || symB.unit) { + return _.Unit.multiply(symA, symB); + } + + // Handle Infinty + if (symA.isInfinity || symB.isInfinity) { + if (symA.equals(0) || symB.equals(0)) { + throw new UndefinedError(`${symA}*${symB} is undefined!`); + } + // X/infinity + if (symB.power.lessThan(0)) { + if (!symA.isInfinity) { + return new NerdamerSymbol(0); + } + throw new UndefinedError('Infinity/Infinity is not defined!'); + } + + const signVal = symA.multiplier.multiply(symB.multiplier).sign(); + const inf = NerdamerSymbol.infinity(); + if (symA.isConstant() || symB.isConstant() || (symA.isInfinity && symB.isInfinity)) { + if (signVal < 0) { + inf.negate(); + } + + return inf; + } + } + + // The quickies + if (symA.multiplier.equals(0) || symB.multiplier.equals(0)) { + return new NerdamerSymbol(0); + } + + if (symA.isOne()) { + return symB.clone(); + } + if (symB.isOne()) { + return symA.clone(); + } + + // Now we know that neither is 0 + if (symA.isConstant() && symB.isConstant() && Settings.PARSE2NUMBER) { + let retval; + + // Check if either fraction has magnitude outside the precision range. + // If so, toDecimal() would lose significant digits, so we must use + // exact fraction arithmetic instead. + // + // The magnitude of a fraction num/den is approximately: + // log10(num) - log10(den) ≈ numDigits - denDigits + // + // With PRECISION decimal places, we can only represent numbers in + // the range [10^(-PRECISION), 10^(+PRECISION)] accurately. + // We use a buffer of 5 digits to ensure we have enough significant + // digits for accurate multiplication. + const aNumDigits = symA.multiplier.num.abs().toString().length; + const aDenDigits = symA.multiplier.den.toString().length; + const aMagnitude = aNumDigits - aDenDigits; + + const bNumDigits = symB.multiplier.num.abs().toString().length; + const bDenDigits = symB.multiplier.den.toString().length; + const bMagnitude = bNumDigits - bDenDigits; + + const magnitudeLimit = Settings.PRECISION - 5; // Need at least 5 significant digits + const needsExactArithmetic = + aMagnitude < -magnitudeLimit || + aMagnitude > magnitudeLimit || + bMagnitude < -magnitudeLimit || + bMagnitude > magnitudeLimit; + + if (needsExactArithmetic) { + // Use exact fraction arithmetic via bigDec to avoid precision loss + const anum = new bigDec(String(symA.multiplier.num)); + const aden = new bigDec(String(symA.multiplier.den)); + const bnum = new bigDec(String(symB.multiplier.num)); + const bden = new bigDec(String(symB.multiplier.den)); + retval = new NerdamerSymbol(anum.times(bnum).dividedBy(aden).dividedBy(bden).toFixed()); + } else { + // Safe to use decimal approximation + const ad = new bigDec(symA.multiplier.toDecimal()); + const bd = new bigDec(symB.multiplier.toDecimal()); + const t = ad.times(bd).toFixed(); + retval = new NerdamerSymbol(t); + } + return retval; + } + + if (symB.group > symA.group && !(symB.group === CP)) { + return this.multiply(symB, symA); + } + // Correction for PL/CB dilemma + if (symA.group === CB && symB.group === PL && symA.value === symB.value) { + const t = symA; + symA = symB; + symB = t; // Swap + } + + let g1 = symA.group; + const g2 = symB.group; + const bnum = symB.multiplier.num; + const bden = symB.multiplier.den; + + if ( + g1 === FN && + symA.fname === SQRT && + !symB.isConstant() && + symA.args[0].value === symB.value && + !symA.args[0].multiplier.lessThan(0) + ) { + // Unwrap sqrt + const aPow = symA.power; + const aMultiplier = _.parse(symA.multiplier); + symA = /** @type {NerdamerSymbolType} */ (_.multiply(aMultiplier, symA.args[0].clone())); + symA.setPower(new Frac(0.5).multiply(/** @type {FracType} */ (aPow))); + g1 = symA.group; + } + // Simplify n/sqrt(n). Being very specific + else if ( + g1 === FN && + symA.fname === SQRT && + symA.multiplier.equals(1) && + symA.power.equals(-1) && + symB.isConstant() && + symA.args[0].equals(symB) + ) { + symA = _.symfunction(SQRT, [symB.clone()]); + symB = new NerdamerSymbol(1); + } + let v1 = symA.value; + let v2 = symB.value; + /** @type {FracType} */ + let signVal = /** @type {FracType} */ (/** @type {unknown} */ (new Frac(symA.sign()))); + // Since P is just a morphed version of N we need to see if they relate + const ONN = + g1 === P && + g2 === N && + symB.multiplier.equals(/** @type {PowerValueType} */ (/** @type {unknown} */ (symA.value))); + // Don't multiply the multiplier of b since that's equal to the value of a + const m = ONN + ? new Frac(1).multiply(symA.multiplier).abs() + : symA.multiplier.multiply(symB.multiplier).abs(); + let result = symA.clone().toUnitMultiplier(); + symB = symB.clone().toUnitMultiplier(true); + + // Further simplification of sqrt + if (g1 === FN && g2 === FN) { + const u = symA.args[0].clone(); + const v = symB.args[0].clone(); + if (symA.fname === SQRT && symB.fname === SQRT && symA.isLinear() && symB.isLinear()) { + const q = /** @type {NerdamerSymbolType} */ (_.divide(u, v)).invert(); + if (q.gt(1) && isInt(q)) { + // B contains a factor a which can be moved to a + result = /** @type {NerdamerSymbolType} */ ( + _.multiply(symA.args[0].clone(), sqrt(q.clone())) + ); + symB = new NerdamerSymbol(1); + } + } + // Simplify factorial but only if + // 1 - It's division so b will have a negative power + // 2 - We're not dealing with factorials of numbers + else if ( + symA.fname === FACTORIAL && + symB.fname === FACTORIAL && + !u.isConstant() && + !v.isConstant() && + Number(symB.power) < 0 + ) { + // Assume that n = positive + const d = /** @type {NerdamerSymbolType} */ (_.subtract(u.clone(), v.clone())); + + // If it's not numeric then we don't know if we can simplify so just return + if (d.isConstant()) { + // There will never be a case where d == 0 since this will already have + // been handled at the beginning of this function + /** @type {NerdamerSymbolType} */ + let t = new NerdamerSymbol(1); + if (Number(d) < 0) { + // If d is negative then the numerator is larger so expand that + for (let i = 0, n = Math.abs(Number(d)); i <= n; i++) { + const s = _.add(u.clone(), new NerdamerSymbol(i)); + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, s)); + } + + result = /** @type {NerdamerSymbolType} */ ( + _.multiply( + _.pow(u, new NerdamerSymbol(symA.power)), + _.pow(t, new NerdamerSymbol(symB.power)) + ) + ); + + symB = new NerdamerSymbol(1); + } else { + // Otherwise the denominator is larger so expand that + for (let i = 0, n = Math.abs(Number(d)); i <= n; i++) { + const s = _.add(v.clone(), new NerdamerSymbol(i)); + t = /** @type {NerdamerSymbolType} */ (_.multiply(t, s)); + } + + result = /** @type {NerdamerSymbolType} */ ( + _.multiply( + _.pow(t, new NerdamerSymbol(symA.power)), + _.pow(v, new NerdamerSymbol(symB.power)) + ) + ); + + symB = new NerdamerSymbol(1); + } + } + } + } + + // If both are PL then their hashes have to match + if (v1 === v2 && g1 === PL && g1 === g2) { + v1 = symA.text('hash'); + v2 = symB.text('hash'); + } + + // Same issue with (x^2+1)^x*(x^2+1) + // EX needs an exception when multiplying because it needs to recognize + // that (x+x^2)^x has the same hash as (x+x^2). The latter is kept as x + if (g2 === EX && symB.previousGroup === PL && g1 === PL) { + v1 = text(symA, 'hash', EX); + } + + if ( + (v1 === v2 || ONN) && + !(g1 === PL && (g2 === S || g2 === P || g2 === FN)) && + !(g1 === PL && g2 === CB) + ) { + const p1 = symA.power; + const p2 = symB.power; + const isSymbolP1 = isSymbol(p1); + const isSymbolP2 = isSymbol(p2); + const toEX = isSymbolP1 || isSymbolP2; + // TODO: this needs cleaning up + if (g1 === PL && g2 !== PL && symB.previousGroup !== PL && p1.equals(1)) { + result = new NerdamerSymbol(0); + symA.each(x => { + result = /** @type {NerdamerSymbolType} */ (_.add(result, _.multiply(x, symB.clone()))); + }, true); + } else { + // Add the powers + if (toEX) { + result.power = /** @type {NerdamerSymbolType} */ ( + _.add( + isSymbol(p1) ? p1 : new NerdamerSymbol(p1), + isSymbol(p2) ? p2 : new NerdamerSymbol(p2) + ) + ); + } else if (g1 === N) { + // Don't add powers for N + result.power = p1; + } else { + result.power = /** @type {FracType} */ (p1).add(/** @type {FracType} */ (p2)); + } + + // Eliminate zero power values and convert them to numbers + if (result.power.equals(0)) { + result = result.convert(N); + } + + // Properly convert to EX + if (toEX) { + result.convert(EX); + } + + // Take care of imaginaries + if (symA.imaginary && symB.imaginary) { + const isEven = even(Number(result.power) % 2); + if (isEven) { + result = new NerdamerSymbol(1); + m.negate(); + } + } + + // Cleanup: this causes the LaTeX generator to get confused as to how to render the symbol + if (result.group !== EX && result.previousGroup) { + result.previousGroup = undefined; + } + // The sign for b is floating around. Remember we are assuming that the odd variable will carry + // the sign but this isn't true if they're equals symbols + result.multiplier = result.multiplier.multiply(symB.multiplier); + } + } else if (g1 === CB && symA.isLinear()) { + if (g2 === CB) { + symB.distributeExponent(); + } + if (g2 === CB && symB.isLinear()) { + for (const s in symB.symbols) { + if (!Object.hasOwn(symB.symbols, s)) { + continue; + } + const x = symB.symbols[s]; + result = result.combine(x); + } + result.multiplier = result.multiplier.multiply(symB.multiplier); + } else { + result.combine(symB); + } + // The multiplier was already handled so nothing left to do + } else if (g1 === N) { + result = symB.clone().toUnitMultiplier(true); + } else if (g1 === CB) { + result.distributeExponent(); + result.combine(symB); + } else if (!symB.isOne()) { + const bm = symB.multiplier.clone(); + symB.toUnitMultiplier(); + result = NerdamerSymbol.shell(CB).combine([result, symB]); + // Transfer the multiplier to the outside + result.multiplier = result.multiplier.multiply(bm); + } + + if (result.group === P) { + const logV = Math.log(Number(result.value)); + const n1 = Math.log(Number(bnum)) / logV; + const n2 = Math.log(Number(bden)) / logV; + const ndiv = Number(m.num) / Number(bnum); + const ddiv = Number(m.den) / Number(bden); + // We don't want to divide by zero no do we? Strange things happen. + if (n1 !== 0 && isInt(n1) && isInt(ndiv)) { + result.power = /** @type {FracType} */ (result.power).add(new Frac(n1)); + m.num = m.num.divide(bnum); + } + if (n2 !== 0 && isInt(n2) && isInt(ddiv)) { + result.power = /** @type {FracType} */ (result.power).subtract(new Frac(n2)); + m.den = m.den.divide(bden); + } + } + + // Unpack CB if length is only one + if (result.length === 1) { + const t = result.multiplier; + // Transfer the multiplier + result = /** @type {NerdamerSymbolType} */ (firstObject(result.symbols)); + result.multiplier = result.multiplier.multiply(t); + } + + // Reduce square root + const ps = result.power.toString(); + if (even(ps) && result.fname === SQRT) { + // Grab the sign of the symbol + signVal = signVal.multiply( + /** @type {FracType} */ (/** @type {unknown} */ (new Frac(result.sign()))) + ); + const p = /** @type {FracType} */ (result.power); + result = result.args[0]; + result = /** @type {NerdamerSymbolType} */ ( + _.multiply(new NerdamerSymbol(m), _.pow(result, new NerdamerSymbol(p.divide(new Frac(2))))) + ); + // Flip it back to the correct sign + if (signVal.lessThan(0)) { + result.negate(); + } + } else { + result.multiplier = result.multiplier.multiply(m).multiply(signVal); + if (result.group === CP && result.isImaginary()) { + result.distributeMultiplier(); + } + } + + // Back convert group P to a simpler group N if possible + if (result.group === P && isInt(/** @type {FracType} */ (result.power).toDecimal())) { + result = result.convert(N); + } + + return result; + } + //* ***** Matrices & Vector *****// + if (bIsSymbol && !aIsSymbol) { + // Keep symbols to the right + const tempOp = a; + a = b; + b = tempOp; // Swap + const tempBool = bIsSymbol; + bIsSymbol = aIsSymbol; + aIsSymbol = tempBool; + } + + const isMatrixB = isMatrix(b); + const isMatrixA = isMatrix(a); + if (aIsSymbol && isMatrixB) { + const M = new Matrix(); + const bMatrix = /** @type {MatrixType} */ (b); + bMatrix.eachElement((e, row, col) => { + M.set( + row, + col, + /** @type {NerdamerSymbolType} */ (_.multiply(/** @type {NerdamerSymbolType} */ (a).clone(), e)) + ); + }); + + b = M; + } else if (isMatrixA && isMatrixB) { + b = /** @type {MatrixType} */ (a).multiply(/** @type {MatrixType} */ (b)); + } else if (aIsSymbol && isVector(b)) { + const bVec = /** @type {VectorType} */ (b); + bVec.each((el, idx) => { + idx--; + bVec.elements[idx] = /** @type {NerdamerSymbolType} */ ( + _.multiply(/** @type {NerdamerSymbolType} */ (a).clone(), bVec.elements[idx]) + ); + }); + } else if (isVector(a) && isVector(b)) { + const aVec = /** @type {VectorType} */ (a); + const bVec = /** @type {VectorType} */ (b); + bVec.each((el, idx) => { + idx--; + bVec.elements[idx] = /** @type {NerdamerSymbolType} */ ( + _.multiply(aVec.elements[idx], bVec.elements[idx]) + ); + }); + } else if (isVector(a) && isMatrix(b)) { + // Try to convert a to a matrix + return this.multiply(b, a); + } else if (isMatrix(a) && isVector(b)) { + const aMatrix = /** @type {MatrixType} */ (a); + const bVec = /** @type {VectorType} */ (b); + if (bVec.elements.length === aMatrix.rows()) { + const M = new Matrix(); + const l = aMatrix.cols(); + bVec.each((e, idx) => { + const row = []; + for (let j = 0; j < l; j++) { + row.push(_.multiply(aMatrix.elements[idx - 1][j].clone(), e.clone())); + } + M.elements.push(row); + }); + return M; + } + err('Dimensions must match!'); + } + + return b; + }; + /** + * Gets called when the parser finds the / operator. See this.add + * + * @param {ArithmeticOperand} a + * @param {ArithmeticOperand} b + * @returns {ArithmeticOperand} + */ + this.divide = function divide(a, b) { + const aIsSymbol = isSymbol(a); + const bIsSymbol = isSymbol(b); + + if (aIsSymbol && bIsSymbol) { + // Cast to NerdamerSymbol since we've verified with isSymbol + const symA = /** @type {NerdamerSymbolType} */ (a); + const symB = /** @type {NerdamerSymbolType} */ (b); + // Forward to Unit division + if (symA.unit || symB.unit) { + return _.Unit.divide(symA, symB); + } + let result; + if (symB.equals(0)) { + throw new DivisionByZero('Division by zero not allowed!'); + } + + if (symA.isConstant() && symB.isConstant()) { + result = symA.clone(); + result.multiplier = result.multiplier.divide(symB.multiplier); + } else { + symB.invert(); + result = /** @type {NerdamerSymbolType} */ (_.multiply(symA, symB)); + } + return result; + } + //* ****** Vectors & Matrices *********// + const isVectorA = isVector(a); + const isVectorB = isVector(b); + if (aIsSymbol && isVectorB) { + b = /** @type {VectorType} */ (b).map( + x => /** @type {NerdamerSymbolType} */ (_.divide(/** @type {NerdamerSymbolType} */ (a).clone(), x)) + ); + } else if (isVectorA && bIsSymbol) { + b = /** @type {VectorType} */ (a).map( + x => /** @type {NerdamerSymbolType} */ (_.divide(x, /** @type {NerdamerSymbolType} */ (b).clone())) + ); + } else if (isVectorA && isVectorB) { + const aVec = /** @type {VectorType} */ (a); + if (aVec.dimensions() === /** @type {VectorType} */ (b).dimensions()) { + b = /** @type {VectorType} */ (b).map( + (x, i) => /** @type {NerdamerSymbolType} */ (_.divide(aVec.elements[--i], x)) + ); + } else { + _.error('Cannot divide vectors. Dimensions do not match!'); + } + } else { + const isMatrixA = isMatrix(a); + const isMatrixB = isMatrix(b); + if (isMatrixA && bIsSymbol) { + const M = new Matrix(); + /** @type {MatrixType} */ (a).eachElement((x, i, j) => { + M.set( + i, + j, + /** @type {NerdamerSymbolType} */ ( + _.divide(x, /** @type {NerdamerSymbolType} */ (b).clone()) + ) + ); + }); + b = M; + } else if (aIsSymbol && isMatrixB) { + const M = new Matrix(); + /** @type {MatrixType} */ (b).eachElement((x, i, j) => { + M.set( + i, + j, + /** @type {NerdamerSymbolType} */ ( + _.divide(/** @type {NerdamerSymbolType} */ (a).clone(), x) + ) + ); + }); + b = M; + } else if (isMatrixA && isMatrixB) { + const M = new Matrix(); + const aMatrix = /** @type {MatrixType} */ (a); + const bMatrix = /** @type {MatrixType} */ (b); + if (aMatrix.rows() === bMatrix.rows() && aMatrix.cols() === bMatrix.cols()) { + aMatrix.eachElement((x, i, j) => { + M.set(i, j, /** @type {NerdamerSymbolType} */ (_.divide(x, bMatrix.elements[i][j]))); + }); + b = M; + } else { + _.error('Dimensions do not match!'); + } + } else if (isMatrixA && isVectorB) { + const aMatrix = /** @type {MatrixType} */ (a); + const bVec = /** @type {VectorType} */ (b); + if (aMatrix.cols() === bVec.dimensions()) { + const M = new Matrix(); + aMatrix.eachElement((x, i, j) => { + M.set(i, j, /** @type {NerdamerSymbolType} */ (_.divide(x, bVec.elements[i].clone()))); + }); + b = M; + } else { + _.error('Unable to divide matrix by vector.'); + } + } + } + return b; + }; + /** + * Gets called when the parser finds the ^ operator. See this.add + * + * @param {ArithmeticOperand} a + * @param {ArithmeticOperand} b + * @returns {ArithmeticOperand} + */ + this.pow = function pow(a, b) { + const aIsSymbol = isSymbol(a); + const bIsSymbol = isSymbol(b); + if (aIsSymbol && bIsSymbol) { + // Cast to NerdamerSymbol since we've verified with isSymbol + const symA = /** @type {NerdamerSymbolType} */ (a); + const symB = /** @type {NerdamerSymbolType} */ (b); + // It has units then it's the Unit module's problem + if (symA.unit || symB.unit) { + return _.Unit.pow(symA, symB); + } + + // Handle abs + if (symA.group === FN && symA.fname === ABS && even(symB)) { + const m = symA.multiplier.clone(); + const raised = /** @type {NerdamerSymbolType} */ (_.pow(symA.args[0], symB)); + raised.multiplier = m; + return raised; + } + + // Handle infinity + if (symA.isInfinity || symB.isInfinity) { + if (symA.isInfinity && symB.isInfinity) { + throw new UndefinedError(`(${symA})^(${symB}) is undefined!`); + } + + if (symA.isConstant() && symB.isInfinity) { + if (symA.equals(0)) { + if (symB.lessThan(0)) { + throw new UndefinedError('0^Infinity is undefined!'); + } + return new NerdamerSymbol(0); + } + if (symA.equals(1)) { + throw new UndefinedError(`1^${symB.toString()} is undefined!`); + } + // A^-oo + if (symB.lessThan(0)) { + return new NerdamerSymbol(0); + } + // A^oo + if (!symA.lessThan(0)) { + return NerdamerSymbol.infinity(); + } + } + + if (symA.isInfinity && symB.isConstant()) { + if (symB.equals(0)) { + throw new UndefinedError(`${symA}^0 is undefined!`); + } + if (symB.lessThan(0)) { + return new NerdamerSymbol(0); + } + return /** @type {NerdamerSymbolType} */ ( + _.multiply(NerdamerSymbol.infinity(), _.pow(new NerdamerSymbol(symA.sign()), symB.clone())) + ); + } + } + + const aIsZero = symA.equals(0); + const bIsZero = symB.equals(0); + if (aIsZero && bIsZero) { + throw new UndefinedError('0^0 is undefined!'); + } + + // Return 0 right away if possible + if (aIsZero && symB.isConstant() && symB.multiplier.greaterThan(0)) { + return new NerdamerSymbol(0); + } + + if (bIsZero) { + return new NerdamerSymbol(1); + } + + const bIsConstant = symB.isConstant(); + const aIsConstant = symA.isConstant(); + const bIsInt = symB.isInteger(); + const m = symA.multiplier; + let result = symA.clone(); + + // 0^0, 1/0, etc. Complain. + if (aIsConstant && bIsConstant && symA.equals(0) && symB.lessThan(0)) { + throw new UndefinedError('Division by zero is not allowed!'); + } + + // Compute imaginary numbers right away + if ( + Settings.PARSE2NUMBER && + aIsConstant && + bIsConstant && + symA.sign() < 0 && + evenFraction(/** @type {number} */ (/** @type {unknown} */ (symB))) + ) { + const k = Math.PI * Number(symB.multiplier.toDecimal()); + const re = new NerdamerSymbol(Math.cos(k)); + const im = /** @type {NerdamerSymbolType} */ ( + _.multiply(NerdamerSymbol.imaginary(), new NerdamerSymbol(Math.sin(k))) + ); + return _.add(re, im); + } + + // Imaginary number under negative nthroot or to the n + if ( + Settings.PARSE2NUMBER && + symA.isImaginary() && + bIsConstant && + isInt(/** @type {number} */ (/** @type {unknown} */ (symB))) && + !symB.lessThan(0) + ) { + let r; + let theta; + let nre; + let nim; + let phi; + const re = symA.realpart(); + const im = symA.imagpart(); + if (re.isConstant('all') && im.isConstant('all')) { + phi = Settings.USE_BIG + ? nerdamerBigDecimal + .atan2(im.multiplier.toDecimal(), re.multiplier.toDecimal()) + .times(symB.toString()) + : Math.atan2(Number(im.multiplier.toDecimal()), Number(re.multiplier.toDecimal())) * + Number(symB.multiplier.toDecimal()); + theta = new NerdamerSymbol(phi); + r = /** @type {NerdamerSymbolType} */ (_.pow(NerdamerSymbol.hyp(re, im), symB)); + nre = /** @type {NerdamerSymbolType} */ (_.multiply(r.clone(), _.trig.cos(theta.clone()))); + nim = /** @type {NerdamerSymbolType} */ (_.multiply(r, _.trig.sin(theta))); + return _.add(nre, _.multiply(NerdamerSymbol.imaginary(), nim)); + } + } + + // Take care of the symbolic part + result.toUnitMultiplier(); + let signVal; + // Simpifly sqrt + if (result.group === FN && result.fname === SQRT && !bIsConstant) { + const s = result.args[0]; + s.multiplyPower(new NerdamerSymbol(0.5)); + s.multiplier.multiply(result.multiplier); + s.multiplyPower(symB); + result = s; + } else { + signVal = m.sign(); + // Handle cases such as (-a^3)^(1/4) + if (evenFraction(/** @type {number} */ (/** @type {unknown} */ (symB))) && signVal < 0) { + // Swaperoo + // First put the sign back on the symbol + result.negate(); + // Wrap it in brackets + result = /** @type {NerdamerSymbolType} */ (_.symfunction(PARENTHESIS, [result])); + // Move the sign back the exterior and let nerdamer handle the rest + result.negate(); + } + + result.multiplyPower(symB); + } + + let num; + let den; + if (aIsConstant && bIsConstant && Settings.PARSE2NUMBER) { + let c; + // Remove the sign + if (signVal < 0) { + symA.negate(); + if ( + symB.multiplier.den.equals(2) + ) // We know that the numerator has to be odd and therefore it's i + { + c = new NerdamerSymbol(Settings.IMAGINARY); + } else if (isInt(symB.multiplier)) { + if (even(symB.multiplier)) { + c = new NerdamerSymbol(1); + } else { + c = new NerdamerSymbol(-1); + } + } else if (even(symB.multiplier.den)) { + c = /** @type {NerdamerSymbolType} */ ( + _.pow(_.symfunction(PARENTHESIS, [new NerdamerSymbol(signVal)]), symB.clone()) + ); + } else { + c = new NerdamerSymbol( + signVal ** /** @type {number} */ (/** @type {unknown} */ (symB.multiplier.num)) + ); + } + } + + const _pow = Number(symA.multiplier.toDecimal()) ** Number(symB.multiplier.toDecimal()); + if (_pow !== 0 || symA.multiplier.equals(0)) { + result = new NerdamerSymbol(_pow); + } else { + // Should not be here, must have underflowed precision + const ad = new bigDec(symA.multiplier.toDecimal()); + const bd = new bigDec(symB.multiplier.toDecimal()); + result = new NerdamerSymbol(ad.pow(bd).toFixed()); + } + // Put the back sign + if (c) { + result = /** @type {NerdamerSymbolType} */ (_.multiply(result, c)); + } + } else if (bIsInt && !m.equals(1)) { + const absB = symB.abs(); + // Provide fall back to JS until big number implementation is improved + if (absB.gt(Settings.MAX_EXP)) { + if (symB.sign() < 0) { + return new NerdamerSymbol(0); + } + return NerdamerSymbol.infinity(); + } + const p = /** @type {FracType} */ (symB.multiplier).toDecimal(); + const sgn = Math.sign(Number(p)); + const absP = Math.abs(Number(p)); + const multiplier = new Frac(1); + multiplier.num = /** @type {BigIntegerType} */ (m.num).pow(Number(absP)); + multiplier.den = /** @type {BigIntegerType} */ (m.den).pow(Number(absP)); + if (sgn < 0) { + multiplier.invert(); + } + // Multiplying is justified since after mulltiplyPower if it was of group P it will now be of group N + result.multiplier = result.multiplier.multiply(multiplier); + } else { + const aSignVal = symA.sign(); + if (symB.isConstant() && symA.isConstant() && !symB.multiplier.den.equals(1) && aSignVal < 0) { + // We know the sign is negative so if the denominator for b == 2 then it's i + if (symB.multiplier.den.equals(2)) { + const i = new NerdamerSymbol(Settings.IMAGINARY); + symA.negate(); // Remove the sign + // if the power is negative then i is negative + if (symB.lessThan(0)) { + i.negate(); + symB.negate(); // Remove the sign from the power + } + // Pull the power normally and put back the imaginary + result = /** @type {NerdamerSymbolType} */ (_.multiply(_.pow(symA, symB), i)); + } else { + const aa = symA.clone(); + aa.multiplier.negate(); + result = /** @type {NerdamerSymbolType} */ ( + _.pow(_.symfunction(PARENTHESIS, [new NerdamerSymbol(signVal)]), symB.clone()) + ); + const _a = /** @type {NerdamerSymbolType} */ ( + _.pow(new NerdamerSymbol(aa.multiplier.num), symB.clone()) + ); + const _b = /** @type {NerdamerSymbolType} */ ( + _.pow(new NerdamerSymbol(aa.multiplier.den), symB.clone()) + ); + const r = /** @type {NerdamerSymbolType} */ (_.divide(_a, _b)); + result = /** @type {NerdamerSymbolType} */ (_.multiply(result, r)); + } + } else if (Settings.PARSE2NUMBER && symB.isImaginary()) { + // 4^(i + 2) = e^(- (2 - 4 i) π n + (2 + i) log(4)) + + const re = symB.realpart(); + const im = symB.imagpart(); + /* + If(b.group === CP && false) { + let ex = _.pow(a.clone(), re); + let xi = _.multiply(_.multiply(ex.clone(), trig.sin(im.clone())), NerdamerSymbol.imaginary()); + let xa = _.multiply(trig.cos(im), ex); + result = _.add(xi, xa); + } + else { + */ + const aa = symA.clone().toLinear(); + const a1 = /** @type {NerdamerSymbolType} */ (_.pow(aa.clone(), re)); + const logA = /** @type {NerdamerSymbolType} */ (log(aa.clone())); + const b1 = /** @type {NerdamerSymbolType} */ (trig.cos(_.multiply(im.clone(), logA))); + const c1 = /** @type {NerdamerSymbolType} */ ( + _.multiply(trig.sin(_.multiply(im, log(aa))), NerdamerSymbol.imaginary()) + ); + result = /** @type {NerdamerSymbolType} */ (_.multiply(a1, _.add(b1, c1))); + result = /** @type {NerdamerSymbolType} */ (_.expand(_.parse(result))); + /* + } + */ + } else { + // B is a symbol + const negNum = symA.group === N && aSignVal < 0; + num = testSQRT( + /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ ( + new NerdamerSymbol(negNum ? Number(m.num) : Math.abs(Number(m.num))) + ) + ).setPower(symB.clone()) + ); + den = testSQRT( + /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(Number(m.den))) + ) + .setPower(symB.clone()) + .invert() + ); + + // Eliminate imaginary if possible + if (symA.imaginary) { + if (bIsInt) { + const s = Math.sign(/** @type {number} */ (/** @type {unknown} */ (symB))); + const p = abs(symB); + const n = p % 4; + result = new NerdamerSymbol(even(n) ? -1 : Settings.IMAGINARY); + if (n === 0 || (s < 0 && n === 1) || (s > 0 && n === 3)) { + result.negate(); + } + } else { + // Assume i = sqrt(-1) -> (-1)^(1/2) + const nr = b.multiplier.multiply(Frac.quick(1, 2)); + // The denominator denotes the power so raise to it. It will turn positive it round + const tn = (-1) ** /** @type {number} */ (/** @type {unknown} */ (nr.num)); + result = even(nr.den) + ? /** @type {NerdamerSymbolType} */ ( + /** @type {unknown} */ (new NerdamerSymbol(-1)) + ).setPower(nr, true) + : new NerdamerSymbol(tn); + } + } + // Ensure that the sign is carried by the symbol and not the multiplier + // this enables us to check down the line if the multiplier can indeed be transferred + if (aSignVal < 0 && !negNum) { + result.negate(); + } + + // Retain the absolute value + if (bIsConstant && symA.group !== EX) { + const evenr = even(symB.multiplier.den); + const evenp = even(/** @type {FracType} */ (symA.power)); + const n = /** @type {FracType} */ (result.power).toDecimal(); + const evennp = even(n); + if (evenr && evenp && !evennp) { + if (n === '1' || n === '1') { + // Check for together.math baseunits + // don't have to wrap them in abs() + if (typeof result.value === 'string' && result.value.startsWith('baseunit_')) { + // Don't wrap baseunits in abs() + } else { + result = /** @type {NerdamerSymbolType} */ (_.symfunction(ABS, [result])); + } + } else if (isInt(n)) { + result = /** @type {NerdamerSymbolType} */ ( + _.multiply( + _.symfunction(ABS, [result.clone().toLinear()]), + result.clone().setPower(new Frac(Number(n) - 1)) + ) + ); + } else { + const p = /** @type {FracType} */ (result.power); + result = /** @type {NerdamerSymbolType} */ ( + _.symfunction(ABS, [result.toLinear()]) + ).setPower(p); + } + // Quick workaround. Revisit + if (Settings.POSITIVE_MULTIPLIERS && result.fname === ABS) { + result = result.args[0]; + } + } + } + // Multiply out sqrt + if (symB.equals(2) && result.group === CB) { + /** @type {NerdamerSymbolType} */ + let _result = new NerdamerSymbol(1); + result.each(sym => { + _result = /** @type {NerdamerSymbolType} */ (_.multiply(_result, _.pow(sym, symB))); + }); + result = _result; + } + } + } + + result = testSQRT(result); + + // Don't multiply until we've tested the remaining symbol + if (num && den) { + result = /** @type {NerdamerSymbolType} */ (_.multiply(result, testPow(_.multiply(num, den)))); + } + + // Reduce square root + if (result.fname === SQRT) { + const isEX = result.group === EX; + const t = isEX + ? /** @type {NerdamerSymbolType} */ (result.power).multiplier.toString() + : /** @type {FracType} */ (result.power).toString(); + if (even(t)) { + const pt = isEX + ? _.divide(/** @type {NerdamerSymbolType} */ (result.power), new NerdamerSymbol(2)) + : new NerdamerSymbol(/** @type {FracType} */ (result.power).divide(new Frac(2))); + const resultMult = result.multiplier; + result = /** @type {NerdamerSymbolType} */ (_.pow(result.args[0], pt)); + result.multiplier = result.multiplier.multiply(resultMult); + } + } + // Detect Euler's identity + else if ( + !Settings.IGNORE_E && + result.isE() && + result.group === EX && + /** @type {NerdamerSymbolType} */ (result.power).contains('pi') && + /** @type {NerdamerSymbolType} */ (result.power).contains(Settings.IMAGINARY) && + symB.group === CB + ) { + const theta = symB.stripVar(Settings.IMAGINARY); + result = /** @type {NerdamerSymbolType} */ ( + _.add(trig.cos(theta), _.multiply(NerdamerSymbol.imaginary(), trig.sin(theta))) + ); + } + + return result; + } + if (isVector(a) && bIsSymbol) { + a = /** @type {VectorType} */ (a).map( + x => /** @type {NerdamerSymbolType} */ (_.pow(x, /** @type {NerdamerSymbolType} */ (b).clone())) + ); + } else if (isMatrix(a) && bIsSymbol) { + const M = new Matrix(); + a.eachElement((x, row, col) => { + M.set(row, col, /** @type {NerdamerSymbolType} */ (_.pow(x, b.clone()))); + }); + a = M; + } else if (aIsSymbol && isMatrix(b)) { + const M = new Matrix(); + b.eachElement((x, row, col) => { + M.set(row, col, /** @type {NerdamerSymbolType} */ (_.pow(a.clone(), x))); + }); + a = M; + } + return a; + }; + // Gets called when the parser finds the , operator. + // Commas return a Collector object which is roughly an array + this.comma = function comma(a, b) { + if (!(a instanceof Collection)) { + a = Collection.create(a); + } + a.append(b); + return a; + }; + // Link to modulus + this.mod = function mod(a, b) { + return _mod(a, b); + }; + // Used to slice elements from arrays + this.slice = function slice(a, b) { + return new Slice(a, b); + }; + // The equality setter + this.equals = function equals(a, b) { + // Equality can only be set for group S so complain it's not + if (a.group !== S && !a.isLinear()) { + err(`Cannot set equality for ${a.toString()}`); + } + VARS[a.value] = b.clone(); + return b; + }; + // Percent + this.percent = function percent(a) { + return _.divide(a, new NerdamerSymbol(100)); + }; + // NerdamerSet variable + this.assign = function assign(a, b) { + if (a instanceof Collection && b instanceof Collection) { + a.elements.map((x, i) => _.assign(x, b.elements[i])); + return Vector.fromArray(b.elements); + } + if (a.parent) { + // It's referring to the parent instead. The current item can be discarded + const e = a.parent; + e.elements[e.getter] = b; + delete e.getter; + return e; + } + + if (a.group !== S) { + throw new NerdamerValueError(`Cannot complete operation. Incorrect LH value for ${a}`); + } + VARS[a.value] = b; + return b; + }; + this.functionAssign = function functionAssign(a, b) { + const f = a.elements.pop(); + return _setFunction(f, a.elements, b); + }; + // Function to quickly convert bools to Symbols + const bool2Symbol = function bool2Symbol(x) { + return new NerdamerSymbol(x === true ? 1 : 0); + }; + // Check for equality + this.eq = function eq(a, b) { + return bool2Symbol(a.equals(b)); + }; + // Checks for greater than + this.gt = function gt(a, b) { + return bool2Symbol(a.gt(b)); + }; + // Checks for greater than equal + this.gte = function gte(a, b) { + return bool2Symbol(a.gte(b)); + }; + // Checks for less than + this.lt = function lt(a, b) { + return bool2Symbol(a.lt(b)); + }; + // Checks for less than equal + this.lte = function lte(a, b) { + return bool2Symbol(a.lte(b)); + }; + // Wraps the factorial + this.factorial = function factorial(a) { + return this.symfunction(FACTORIAL, [a]); + }; + // Wraps the double factorial + this.dfactorial = function dfactorial(a) { + return this.symfunction(DOUBLEFACTORIAL, [a]); + }; + } // End constructor +} // End class Parser + +// Utils ======================================================================== +// Utility functions exported as part of the nerdamer core. +// All functions are already at module scope; Build.build uses a getter for lazy evaluation. +// Note: CoreUtilsInterface is the base type - Algebra.js extends it with additional methods at runtime. +/** @type {CoreUtilsInterface} */ +const Utils = { + allSame, + allNumeric, + arguments2Array, + armTimeout, + arrayAddSlices, + arrayClone, + arrayMax, + arrayMin, + arrayEqual, + arrayUnique, + err, + arrayGetVariables, + arraySum, + block, + checkTimeout, + clearU, + comboSort, + compare, + convertToVector, + customError, + customType, + decompose_fn: decomposeFn, + disarmTimeout, + each, + evaluate, + even, + evenFraction, + fillHoles, + firstObject, + format, + generatePrimes, + getCoeffs, + getU, + importFunctions, + inBrackets, + isArray, + isCollection, + isExpression, + isFraction, + isInt, + isMatrix, + isNegative, + isNumericSymbol, + isPrime, + isReserved, + isSet, + isSymbol, + isVariableSymbol, + isVector, + keys, + knownVariable, + nroots, + remove, + reserveNames, + range, + round: nround, + sameSign, + scientificToDecimal, + separate, + stringReplace, + text, + validateName, + variables, + warn, +}; + +// LibExports =================================================================== +// Factory function to create the main nerdamer library function. +// Extracted from IIFE to module scope for cleaner organization. +// Uses CoreDeps for all dependencies, which are populated by the IIFE before calling. + +/** + * Creates the main nerdamer library entry point function. Must be called after CoreDeps.parser and CoreDeps.state are + * initialized. + * + * @returns {NerdamerType} The libExports function (callable with additional methods attached by attachLibExports) + */ +function createLibExports() { + /** + * @param {string} expression The expression to be evaluated + * @param {object} subs The object containing the variable values + * @param {string | string[]} option Additional options + * @param {number} location A specific location in the equation list to insert the evaluated expression + * @returns {ExpressionType | NerdamerType} + */ + const libExports = /** @type {NerdamerType} */ ( + function (expression, subs, option, location) { + armTimeout(); + try { + const _ = CoreDeps.parser; + const exprs = CoreDeps.state.EXPRESSIONS; + + // Initiate the numer flag + let numer = false; + + // Is the user declaring a function? Try to add user function + if (typeof expression === 'string' || typeof expression === 'function') { + if (_setFunction(/** @type {string | Function} */ (expression))) { + return CoreDeps.libExports; // Return self-reference via CoreDeps + } + } + + // Var variable, fn, args; + // Convert any expression passed in to a string + if ( + expression !== null && + typeof expression === 'object' && + /** @type {ExpressionType} */ (expression) instanceof Expression + ) { + expression = /** @type {ExpressionType} */ (expression).toString(); + } + + // If it's still a NerdamerExpression (not Expression), convert to string + if ( + expression !== null && + typeof expression === 'object' && + 'text' in expression && + typeof expression.text === 'function' + ) { + expression = expression.text(); + } + + // Convert it to an array for simplicity + if (!isArray(option)) { + option = typeof option === 'undefined' ? [] : [option]; + } + + option.forEach(o => { + // Turn on the numer flag if requested + if (o === 'numer') { + numer = true; + return; + } + // Wrap it in a function if requested. This only holds true for + // functions that take a single argument which is the expression + const f = _.functions[o]; + // If there's a function and it takes a single argument, then wrap + // the expression in it + if (f && f[1] === 1) { + expression = `${o}(${/** @type {string} */ (expression)})`; + } + }); + + const e = block( + 'PARSE2NUMBER', + () => + _.parse( + /** @type {string | number | NerdamerSymbolType | FracType | BigIntegerType} */ ( + expression + ), + subs + ), + numer || Settings.PARSE2NUMBER + ); + + const expr = new Expression(e); + if (location) { + exprs[location - 1] = expr; + } else { + exprs.push(expr); + } + + return expr; + } finally { + disarmTimeout(); + } + } + ); + + return libExports; +} + +// Core Object ================================================================== +// Factory function to create the C (core) object that contains all nerdamer internals. +// Uses CoreDeps for parser reference. + +/** + * Creates the core object containing all nerdamer internals. Must be called after Parser is instantiated and + * CoreDeps.parser is set. + * + * @returns {CoreType} The core object + */ +function createCoreObject() { + const _ = CoreDeps.parser; + return { + groups: Groups, + NerdamerSymbol, + Expression, + Collection, + Frac, + Vector, + Matrix, + NerdamerSet, + Math2, + LaTeX, + Build, + // Utils is typed as CoreUtilsInterface here, but Algebra.js will add the remaining methods at runtime. + // Cast to UtilsInterface to satisfy the Core interface type. + Utils: /** @type {UtilsInterface} */ (/** @type {unknown} */ (Utils)), + PARSER: _, + Settings, + bigInt: nerdamerBigInt, + bigDec: nerdamerBigDecimal, + exceptions: CoreDeps.exceptions, + Solve: /** @type {Record} */ ({}), + Calculus: /** @type {Record} */ ({}), + Algebra: /** @type {Record} */ ({}), + Extra: /** @type {Record} */ ({}), + }; +} + +// Parser Finalization ========================================================== +// Finalizes parser setup after instantiation. + +/** + * Finalizes parser setup - reserves names, initializes constants, sets error handler. Must be called after Parser is + * instantiated. + */ +function finalizeParser() { + const _ = CoreDeps.parser; + reserveNames(_.CONSTANTS); + reserveNames(_.functions); + _.initConstants(); + _.error ||= err; + Settings.LOG_FNS = { + log: _.functions.log, + log10: _.functions.log10, + }; +} + +// Library Exports Attachment =================================================== +// Attaches all public API methods to the libExports function. + +/** + * Attaches all public API methods to libExports. + * + * @param {NerdamerType} libExports - The main nerdamer function to attach methods to + */ +function attachLibExports(libExports) { + // Library exports (most functions defined outside IIFE with dependency injection) + libExports.rpn = rpn; + libExports.convertToLaTeX = convertToLaTeX; + libExports.convertFromLaTeX = convertFromLaTeX; + libExports.version = version; + libExports.getWarnings = getWarnings; + libExports.setConstant = setConstant; + libExports.getConstant = getConstant; + libExports.clearConstants = clearConstants; + libExports.setFunction = setFunction; + libExports.clearFunctions = clearFunctions; + libExports.getCore = getCore; + libExports.getExpression = libExports.getEquation = Expression.getExpression; + libExports.reserved = reserved; + libExports.clear = clear; + libExports.flush = flush; + libExports.expressions = expressions; + libExports.functions = getFunctions; + libExports.register = register; + libExports.validateName = validateName; + libExports.validVarName = validVarName; + libExports.supported = supported; + libExports.numEquations = libExports.numExpressions = numExpressions; + libExports.setVar = setVar; + libExports.getVar = getVar; + libExports.clearVars = clearVars; + libExports.load = load; + libExports.getVars = getVars; + libExports.set = set; + libExports.get = getSetting; + libExports.updateAPI = updateAPI; + libExports.replaceFunction = replaceFunction; + libExports.setOperator = setOperator; + libExports.getOperator = getOperator; + libExports.aliasOperator = aliasOperator; + libExports.tree = tree; + libExports.htmlTree = htmlTree; + libExports.addPeeker = addPeeker; + libExports.removePeeker = removePeeker; + libExports.parse = parse; +} + +const nerdamer = (function initNerdamerCore() { + // ============================================================================ + // Parser Initialization + // ============================================================================ + // The IIFE's main purpose is to instantiate Parser and wire up parser-dependent values. + // Most dependencies are centralized in CoreDeps via getters at module scope. + + // Create parser instance + /** @type {ParserType} */ + const _ = /** @type {ParserType} */ (/** @type {unknown} */ (new Parser())); + + // Set CoreDeps.parser immediately so all getters can access it + CoreDeps.parser = _; + CoreDeps.classes.Parser = /** @type {ParserConstructor} */ (/** @type {unknown} */ (Parser)); + + // Initialize LaTeX.parser + LaTeX.initParser(); + + // Initialize Build dependencies + Build.initDependencies(); + + // Finalize parser setup + finalizeParser(); + + // Create the core object + const C = createCoreObject(); + + // ============================================================================ + // CoreDeps Finalization - Complete the centralized dependency registry + // ============================================================================ + // Classes are already assigned at module scope. Only set runtime values here. + CoreDeps.core = C; + CoreDeps.classes.Math2 = C.Math2; + + // Parser-bound utils (these need runtime parser binding) + CoreDeps.utils.symfunction = _.symfunction.bind(_); + CoreDeps.utils.callfunction = _.callfunction.bind(_); + + // Complete state references that point to IIFE-local arrays/objects + // Note: EXPRESSIONS, VARS are the same arrays initialized in CoreDeps.state earlier + // CONSTANTS is only available after Parser is created + CoreDeps.state.CONSTANTS = _.CONSTANTS; + + // LibExports =================================================================== + // Create libExports using the factory function (now that CoreDeps is populated) + const libExports = createLibExports(); + + // Complete CoreDeps with libExports + CoreDeps.libExports = libExports; + + // Attach all public API methods to libExports + attachLibExports(libExports); + + libExports.updateAPI(); + + return libExports; // Done +})(); + +if (typeof module !== 'undefined') { + module.exports = nerdamer; +} diff --git a/tools/ui/src/lib/vendors/package.json b/tools/ui/src/lib/vendors/package.json new file mode 100644 index 000000000..a0df0c867 --- /dev/null +++ b/tools/ui/src/lib/vendors/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tools/ui/src/virtual-nerdamer.d.ts b/tools/ui/src/virtual-nerdamer.d.ts new file mode 100644 index 000000000..433fb2b3b --- /dev/null +++ b/tools/ui/src/virtual-nerdamer.d.ts @@ -0,0 +1,4 @@ +declare module 'virtual:nerdamer' { + const code: string; + export default code; +} diff --git a/tools/ui/tests/client/sandbox.service.svelte.test.ts b/tools/ui/tests/client/sandbox.service.svelte.test.ts new file mode 100644 index 000000000..8f2e3df20 --- /dev/null +++ b/tools/ui/tests/client/sandbox.service.svelte.test.ts @@ -0,0 +1,59 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { SandboxService } from '$lib/services/sandbox.service'; +import { SANDBOX_TOOL_NAME } from '$lib/constants'; + +const run = (code: string, timeoutMs?: number) => + SandboxService.executeTool(SANDBOX_TOOL_NAME, { + code, + ...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}) + }); + +describe('sandbox service', () => { + beforeEach(async () => { + const { settingsStore } = await import('$lib/stores/settings.svelte'); + settingsStore.config = { + ...settingsStore.config, + symbolicMathEnabled: true + }; + }); + + it('executes plain JavaScript', async () => { + const reply = await run('return 1 + 1;'); + expect(reply.isError).toBe(false); + expect(reply.content).toContain('=> 2'); + }); + + it('exposes nerdamer for symbolic computation', async () => { + const reply = await run("return nerdamer.diff('sin(x)/x', 'x').toString();"); + expect(reply.isError).toBe(false); + expect(reply.content).toContain('cos(x)'); + }); + + it('computes exact rational arithmetic', async () => { + const reply = await run("return nerdamer('1/3 + 1/6').toString();"); + expect(reply.isError).toBe(false); + expect(reply.content).toContain('=> 1/2'); + }); + + it('proves a polynomial identity symbolically', async () => { + const reply = await run( + "return nerdamer('expand((1+x*y)^3 - (1 + 3*x*y + 3*x^2*y^2 + x^3*y^3))').toString();" + ); + expect(reply.isError).toBe(false); + expect(reply.content).toContain('=> 0'); + }); + + it('blocks network egress in the worker via CSP', async () => { + const reply = await run( + "try { await fetch('https://example.com/'); return 'leaked'; } catch { return 'blocked'; }" + ); + expect(reply.isError).toBe(false); + expect(reply.content).toContain('=> blocked'); + }); + + it('enforces the timeout on runaway code', async () => { + const reply = await run('while (true) {}', 500); + expect(reply.isError).toBe(true); + expect(reply.content).toContain('timed out'); + }); +}); diff --git a/tools/ui/tsconfig.json b/tools/ui/tsconfig.json index 51f55971e..60047e995 100644 --- a/tools/ui/tsconfig.json +++ b/tools/ui/tsconfig.json @@ -25,7 +25,7 @@ ".storybook/**/*.ts", ".storybook/**/*.svelte" ], - "exclude": ["src/lib/services/sandbox-worker.js"] + "exclude": ["src/lib/services/sandbox-worker.js", "src/lib/vendors/**"] // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // diff --git a/tools/ui/vite.config.ts b/tools/ui/vite.config.ts index fbb7dc4c3..4c1e9724a 100644 --- a/tools/ui/vite.config.ts +++ b/tools/ui/vite.config.ts @@ -9,6 +9,7 @@ import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; import { splashScreenPlugin } from './scripts/vite-plugin-splash-screen'; import { buildInfoPlugin } from './scripts/vite-plugin-build-info'; import { relativizeBasePlugin } from './scripts/vite-plugin-relativize-base'; +import { nerdamerPlugin } from './scripts/vite-plugin-nerdamer'; import { playwright } from '@vitest/browser-playwright'; import { SVELTEKIT_PWA_OPTIONS } from './src/lib/constants/pwa'; @@ -46,6 +47,7 @@ export default defineConfig({ SvelteKitPWA(SVELTEKIT_PWA_OPTIONS), splashScreenPlugin(), buildInfoPlugin(), + nerdamerPlugin(), relativizeBasePlugin() ], From e0833bf6860c29fc4efc0e33699dd48f8c23dcc5 Mon Sep 17 00:00:00 2001 From: Niklas Wenzel Date: Wed, 22 Jul 2026 18:10:03 +0200 Subject: [PATCH 04/21] mtmd: use RAII for setting and resetting non-causal attention (#25723) * mtmd: use RAII for setting and resetting non-causal attention * mtmd: drop dependency on * mtmd: shorten class and variable names --- tools/mtmd/mtmd-helper.cpp | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/tools/mtmd/mtmd-helper.cpp b/tools/mtmd/mtmd-helper.cpp index 3c73db443..84422c89f 100644 --- a/tools/mtmd/mtmd-helper.cpp +++ b/tools/mtmd/mtmd-helper.cpp @@ -238,6 +238,29 @@ struct decode_embd_batch { } }; +// Helper class to set non-causal attention via RAII +class scope_non_causal { +public: + scope_non_causal(llama_context * context, bool enabled) : context_(context), enabled_(enabled) { + if (enabled_) { + // TODO @ngxson : need to make sure only one image is processed at a time, and n_ubatch must be enough to hold the image + llama_set_causal_attn(context_, false); + } + } + ~scope_non_causal() { + if (enabled_) { + llama_set_causal_attn(context_, true); + } + } + + scope_non_causal(const scope_non_causal &) = delete; + scope_non_causal & operator=(const scope_non_causal &) = delete; + +private: + llama_context * context_; + bool enabled_; +}; + // Helper function for decoding an image whose embeddings have already been calculated int32_t mtmd_helper_decode_image_chunk( mtmd_context * ctx, @@ -288,10 +311,7 @@ int32_t mtmd_helper_decode_image_chunk( } const bool use_non_causal = mtmd_decode_use_non_causal(ctx, chunk); - if (use_non_causal) { - llama_set_causal_attn(lctx, false); - // TODO @ngxson : need to make sure only one image is processed at a time, and n_ubatch must be enough to hold the image - } + const scope_non_causal non_causal(lctx, use_non_causal); while (i_batch < n_img_batches) { // split into batches int pos_offset = i_batch*n_batch; @@ -304,9 +324,6 @@ int32_t mtmd_helper_decode_image_chunk( int32_t ret = llama_decode(lctx, batch_embd_view); if (ret != 0) { LOG_ERR("failed to decode %s\n", name); - if (use_non_causal) { - llama_set_causal_attn(lctx, true); - } return ret; } @@ -314,9 +331,6 @@ int32_t mtmd_helper_decode_image_chunk( ret = callback(batch_embd_view, user_data); if (ret != 0) { LOG_ERR("post-decode callback failed\n"); - if (use_non_causal) { - llama_set_causal_attn(lctx, true); - } return ret; } } @@ -329,9 +343,6 @@ int32_t mtmd_helper_decode_image_chunk( n_past += mtmd_input_chunk_get_n_pos(chunk); *new_n_past = n_past; - if (use_non_causal) { - llama_set_causal_attn(lctx, true); - } return 0; } From 0278d8362d78c5de291bc03b76016f7f74b2ab77 Mon Sep 17 00:00:00 2001 From: Todor Boinovski Date: Wed, 22 Jul 2026 09:25:04 -0700 Subject: [PATCH 05/21] hexagon: activation ops update (#25974) * hex-geglu: optimized all-in-one geglu microkernel * hex-geglu: enable non-contiguous src and strided DMA * hex-act: enable non-contiguous srs and strided DMA for rest of ACT ops * hex-act: generalize GLU per-thread functions via DEFINE_GLU_PER_THREAD macro * hexagon: move UNARY_SILU and UNARY_GELU to unary-ops * hex-act: replace the generic ops_context scratchpad usage with a local htp_vtcm_layout computation per act op. --------- Co-authored-by: Max Krasnyansky --- ggml/src/ggml-hexagon/ggml-hexagon.cpp | 11 +- ggml/src/ggml-hexagon/htp/act-ops.c | 954 ++++++++++--------------- ggml/src/ggml-hexagon/htp/main.c | 4 +- ggml/src/ggml-hexagon/htp/unary-ops.c | 56 ++ ggml/src/ggml-hexagon/htp/unary-ops.h | 2 + 5 files changed, 455 insertions(+), 572 deletions(-) diff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp index 54d313ade..d19d2d5f0 100644 --- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp +++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp @@ -3084,7 +3084,10 @@ static bool ggml_hexagon_supported_activations(const struct ggml_hexagon_session return false; } - if (!ggml_is_contiguous(src0) || !ggml_is_contiguous(dst)) { + if (!ggml_is_contiguous_1(src0)) { + return false; + } + if (!ggml_is_contiguous(dst)) { return false; } @@ -3095,7 +3098,7 @@ static bool ggml_hexagon_supported_activations(const struct ggml_hexagon_session if (!ggml_are_same_shape(src0, src1)) { return false; } - if (!ggml_is_contiguous(src1)) { + if (!ggml_is_contiguous_1(src1)) { return false; } } @@ -4152,12 +4155,10 @@ static bool ggml_backend_hexagon_device_supports_op(ggml_backend_dev_t dev, cons case GGML_UNARY_OP_SIGMOID: case GGML_UNARY_OP_SOFTPLUS: case GGML_UNARY_OP_TANH: - supp = ggml_hexagon_supported_unary(sess, op); - break; case GGML_UNARY_OP_SILU: case GGML_UNARY_OP_GELU: case GGML_UNARY_OP_GELU_QUICK: - supp = ggml_hexagon_supported_activations(sess, op); + supp = ggml_hexagon_supported_unary(sess, op); break; default: break; diff --git a/ggml/src/ggml-hexagon/htp/act-ops.c b/ggml/src/ggml-hexagon/htp/act-ops.c index e56f2b6a5..9973c088d 100644 --- a/ggml/src/ggml-hexagon/htp/act-ops.c +++ b/ggml/src/ggml-hexagon/htp/act-ops.c @@ -17,6 +17,7 @@ #include "htp-ops.h" #include "htp-ops.h" #include "htp-tensor.h" +#include "htp-vtcm.h" #define htp_act_preamble \ const struct htp_tensor * src0 = actx->octx->src[0]; \ @@ -54,581 +55,413 @@ const uint32_t nb3 = dst->nb[3]; struct htp_act_context { - struct htp_ops_context * octx; + struct htp_ops_context * octx; // Precomputed values - const uint8_t * data_src0; - const uint8_t * data_src1; - uint8_t * data_dst; + const uint8_t * data_src0; + const uint8_t * data_src1; + uint8_t * data_dst; - size_t src0_row_size; - size_t src1_row_size; - size_t dst_row_size; + size_t src0_row_size; + size_t src1_row_size; + size_t dst_row_size; - size_t src0_row_size_aligned; - size_t src1_row_size_aligned; - size_t dst_row_size_aligned; + size_t src0_row_stride; + size_t src1_row_stride; - size_t src0_spad_half_size; - size_t src1_spad_half_size; - size_t dst_spad_half_size; + size_t src0_row_size_aligned; + size_t src1_row_size_aligned; + size_t dst_row_size_aligned; - uint32_t block; - uint32_t src0_nrows; - uint32_t src0_nrows_per_thread; - int nc; + size_t src0_spad_half_size; + size_t src1_spad_half_size; + size_t dst_spad_half_size; + + uint32_t block; + uint32_t src0_nrows; + uint32_t src0_nrows_per_thread; + int nc; + + uint8_t * vtcm_src0; + uint8_t * vtcm_src1; + uint8_t * vtcm_dst; + + size_t vtcm_src0_size_per_thread; + size_t vtcm_src1_size_per_thread; + size_t vtcm_dst_size_per_thread; }; -static void glu_swiglu_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { - struct htp_act_context * actx = (struct htp_act_context *) data; - htp_act_preamble; +struct htp_act_vtcm_layout { + size_t total_bytes; + size_t off_src0; + size_t off_src1; + size_t off_dst; - size_t src0_row_size = actx->src0_row_size; - size_t src1_row_size = actx->src1_row_size; - size_t dst_row_size = actx->dst_row_size; + size_t src0_bytes_per_thread; + size_t src1_bytes_per_thread; + size_t dst_bytes_per_thread; - const uint32_t src0_nrows = actx->src0_nrows; - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + uint32_t vtcm_row_per_thread; +}; - // no work for this thread - if (src0_start_row >= src0_end_row) { - return; - } +static inline void htp_act_vtcm_layout_build(struct htp_act_vtcm_layout * L, + size_t src0_row_size_aligned, + size_t src1_row_size_aligned, + size_t dst_row_size_aligned, + uint32_t n_threads, + size_t vtcm_size) { + const size_t spad_size_per_row = src0_row_size_aligned + src1_row_size_aligned + dst_row_size_aligned; + const uint32_t vtcm_row_per_thread = (uint32_t) (vtcm_size / (n_threads * spad_size_per_row)); - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + L->vtcm_row_per_thread = vtcm_row_per_thread; - const uint8_t * restrict data_src0 = actx->data_src0; - const uint8_t * restrict data_src1 = actx->data_src1; - uint8_t * restrict data_dst = actx->data_dst; + L->src0_bytes_per_thread = src0_row_size_aligned * vtcm_row_per_thread; + L->src1_bytes_per_thread = src1_row_size_aligned * vtcm_row_per_thread; + L->dst_bytes_per_thread = dst_row_size_aligned * vtcm_row_per_thread; - const int nc = actx->nc; + L->off_src0 = 0; + L->off_src1 = L->off_src0 + L->src0_bytes_per_thread * n_threads; + L->off_dst = L->off_src1 + L->src1_bytes_per_thread * n_threads; - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; - const size_t src1_row_size_aligned = actx->src1_row_size_aligned; - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; - - uint8_t * restrict src0_spad_data = actx->octx->src0_spad.data + (ith * actx->octx->src0_spad.size_per_thread); - uint8_t * restrict src1_spad_data = actx->octx->src1_spad.data + (ith * actx->octx->src1_spad.size_per_thread); - uint8_t * restrict dst_spad_data = actx->octx->dst_spad.data + (ith * actx->octx->dst_spad.size_per_thread); - - size_t src0_spad_half_size = actx->src0_spad_half_size; - size_t src1_spad_half_size = actx->src1_spad_half_size; - size_t dst_spad_half_size = actx->dst_spad_half_size; - - const int BLOCK = actx->block; - if (BLOCK == 0) { - FARF(ERROR, - "swiglu-f32 : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", - actx->octx->src0_spad.size_per_thread, src0_row_size_aligned); - return; - } - - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; - - // See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - // Dummy DMA transation for sequencing (interleaving dst,src,dst,...) - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), - dst_row_size, dst_row_size_aligned, 0); - - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_size)), - src0_row_size_aligned, src0_row_size, block_size); - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_size)), - src1_row_size_aligned, src1_row_size, block_size); - } - - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - float * dst_spad = (float *) dma_queue_pop(dma_queue).src; - float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; - float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; - - for (uint32_t ib = 0; ib < block_size; ib++) { - const float * src0_spad_ptr = src0_spad + ib * (src0_row_size_aligned / sizeof(float)); - const float * src1_spad_ptr = src1_spad + ib * (src1_row_size_aligned / sizeof(float)); - float * dst_spad_ptr = dst_spad + ib * (dst_row_size_aligned / sizeof(float)); - - //swiglu(x) = x1 * sigmoid(x0) - hvx_sigmoid_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, nc); - hvx_mul_mul_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, (const uint8_t *) dst_spad_ptr, - (const uint8_t *) src1_spad_ptr, nc); - } - - dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), dst_row_size, - dst_row_size_aligned, block_size); - - // prefetch N+2 loop iteration if any - const uint32_t pref_block = (ir + BLOCK * 2); - if (pref_block < src0_end_row) { - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_size)), - src0_row_size_aligned, src0_row_size, pref_block_size); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_size)), - src1_row_size_aligned, src1_row_size, pref_block_size); - } - } - - dma_queue_flush(dma_queue); - - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "swiglu-f32 %d/%d: %ux%ux%ux%u (%u:%u) x %ux%ux%ux%u -> %ux%ux%ux%u usec %u\n", ith, nth, - ne00, ne01, ne02, ne03, src0_start_row, src0_end_row, ne10, ne11, ne12, ne13, ne0, ne1, ne2, ne3, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); + L->total_bytes = L->off_dst + L->dst_bytes_per_thread * n_threads; } -static void glu_swiglu_oai_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { - struct htp_act_context * actx = (struct htp_act_context *) data; - htp_act_preamble; +#define htp_glu_op_preamble \ + const size_t src0_row_size_aligned = actx->src0_row_size_aligned; \ + const size_t src1_row_size_aligned = actx->src1_row_size_aligned; \ + const size_t dst_row_size_aligned = actx->dst_row_size_aligned; \ + const int nc = actx->nc; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); +// swiglu(x) = x1 * sigmoid(x0) +static void swiglu_f32(const float * restrict src0, + const float * restrict src1, + float * restrict dst, + const uint32_t num_rows, + const struct htp_act_context * actx) { + htp_glu_op_preamble; - size_t src0_row_size = actx->src0_row_size; - size_t src1_row_size = actx->src1_row_size; - size_t dst_row_size = actx->dst_row_size; + for (uint32_t ib = 0; ib < num_rows; ib++) { + const uint8_t * restrict src0_ptr = (const uint8_t *) src0 + (ib * src0_row_size_aligned); + const uint8_t * restrict src1_ptr = (const uint8_t *) src1 + (ib * src1_row_size_aligned); + uint8_t * restrict dst_ptr = (uint8_t *) dst + (ib * dst_row_size_aligned); - const uint32_t src0_nrows = actx->src0_nrows; - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; - - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); - - // no work for this thread - if (src0_start_row >= src0_end_row) { - return; + hvx_sigmoid_f32_aa(dst_ptr, src0_ptr, nc); + hvx_mul_mul_f32_aa(dst_ptr, src0_ptr, dst_ptr, src1_ptr, nc); } +} - const uint8_t * restrict data_src0 = actx->data_src0; - const uint8_t * restrict data_src1 = actx->data_src1; - uint8_t * restrict data_dst = actx->data_dst; - - const int nc = actx->nc; - - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; - const size_t src1_row_size_aligned = actx->src1_row_size_aligned; - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; - - uint8_t * restrict src0_spad_data = actx->octx->src0_spad.data + (ith * actx->octx->src0_spad.size_per_thread); - uint8_t * restrict src1_spad_data = actx->octx->src1_spad.data + (ith * actx->octx->src1_spad.size_per_thread); - uint8_t * restrict dst_spad_data = actx->octx->dst_spad.data + (ith * actx->octx->dst_spad.size_per_thread); - - size_t src0_spad_half_size = actx->src0_spad_half_size; - size_t src1_spad_half_size = actx->src1_spad_half_size; - size_t dst_spad_half_size = actx->dst_spad_half_size; - - const int BLOCK = actx->block; - if (BLOCK == 0) { - FARF(ERROR, - "swiglu-oai-f32 : current VTCM reservation %zu is too small for even 1 row per thread, needed at least " - "%zu\n", - actx->octx->src0_spad.size_per_thread, src0_row_size_aligned); - return; - } +// out = x * sigmoid(alpha * x) * (clamp(y, -limit, limit) + 1.f) +static void swiglu_oai_f32(const float * restrict src0, + const float * restrict src1, + float * restrict dst, + const uint32_t num_rows, + const struct htp_act_context * actx) { + htp_glu_op_preamble; const float alpha = ((const float *) (actx->octx->op_params))[2]; const float limit = ((const float *) (actx->octx->op_params))[3]; - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; + for (uint32_t ib = 0; ib < num_rows; ib++) { + const uint8_t * restrict src0_ptr = (const uint8_t *) src0 + (ib * src0_row_size_aligned); + const uint8_t * restrict src1_ptr = (const uint8_t *) src1 + (ib * src1_row_size_aligned); + uint8_t * restrict dst_ptr = (uint8_t *) dst + (ib * dst_row_size_aligned); - // See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - // Dummy DMA transation for sequencing (interleaving dst,src,dst,...) - dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), - dst_row_size, dst_row_size_aligned, 0); - - dma_queue_push_ddr_to_vtcm( - dma_queue, - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_size)), - src0_row_size_aligned, src0_row_size, block_size); - dma_queue_push_ddr_to_vtcm( - dma_queue, - dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_size)), - src1_row_size_aligned, src1_row_size, block_size); + // x (src0_ptr) = std::min(src0_p[k], limit); + hvx_min_scalar_f32((uint8_t *) src0_ptr, src0_ptr, limit, nc); + // y1 (src1_ptr) = std::clamp(src1_p[k], -limit, limit); + hvx_clamp_scalar_f32((uint8_t *) src1_ptr, src1_ptr, -limit, limit, nc); + // y (src1_ptr) = y1 + 1.f + hvx_add_scalar_f32((uint8_t *) src1_ptr, src1_ptr, 1.0, nc); + // x1 (dst_ptr) = alpha * x + hvx_mul_scalar_f32(dst_ptr, src0_ptr, alpha, nc); + // x2 (dst_ptr) = sigmoid(x1) = 1/(1+exp(-x1)) + hvx_sigmoid_f32_aa(dst_ptr, dst_ptr, nc); + // out = x * sigmoid(alpha * x) * (y + 1.f) + hvx_mul_mul_f32_aa(dst_ptr, src0_ptr, dst_ptr, src1_ptr, nc); } - - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - float * dst_spad = (float *) dma_queue_pop(dma_queue).src; - float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; - float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; - - for (uint32_t ib = 0; ib < block_size; ib++) { - const float * src0_spad_ptr = src0_spad + ib * (src0_row_size_aligned / sizeof(float)); - const float * src1_spad_ptr = src1_spad + ib * (src1_row_size_aligned / sizeof(float)); - float * dst_spad_ptr = dst_spad + ib * (dst_row_size_aligned / sizeof(float)); - - // x (src0_spad_data) = std::min(src0_p[k], limit); - hvx_min_scalar_f32((uint8_t *) src0_spad_ptr, (const uint8_t *) src0_spad_ptr, limit, nc); - // y1 (src1_spad_data) = std::clamp(src1_p[k], -limit, limit); - hvx_clamp_scalar_f32((uint8_t *) src1_spad_ptr, (const uint8_t *) src1_spad_ptr, -limit, limit, nc); - // y (src1_spad_data) = y1 + 1.f - hvx_add_scalar_f32((uint8_t *) src1_spad_ptr, (const uint8_t *) src1_spad_ptr, 1.0, nc); - // x1 (dst_spad_data) = alpha * (x) - hvx_mul_scalar_f32((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, alpha, nc); - // x2 (dst_spad_data) = sigmoid(x1) = 1/(1+exp(-x1)) - hvx_sigmoid_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) dst_spad_ptr, nc); - // out = x * sigmoid(alpha * x) * (y + 1.f) - hvx_mul_mul_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, (const uint8_t *) dst_spad_ptr, - (const uint8_t *) src1_spad_ptr, nc); - } - - dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), dst_row_size, - dst_row_size_aligned, block_size); - - // prefetch N+2 loop iteration if any - const uint32_t pref_block = (ir + BLOCK * 2); - if (pref_block < src0_end_row) { - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_size)), - src0_row_size_aligned, src0_row_size, pref_block_size); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_size)), - src1_row_size_aligned, src1_row_size, pref_block_size); - } - } - - dma_queue_flush(dma_queue); - - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "swiglu-oai-f32 %d/%d: %ux%ux%ux%u (%u:%u) x %ux%ux%ux%u -> %ux%ux%ux%u usec %u\n", ith, nth, src0->ne[0], - src0->ne[1], src0->ne[2], src0->ne[3], src0_start_row, src0_end_row, src1->ne[0], src1->ne[1], src1->ne[2], - src1->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); -} - - -static void unary_gelu_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { - struct htp_act_context * actx = (struct htp_act_context *) data; - htp_act_preamble; - - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - - const size_t src0_row_size = actx->src0_row_size; - const size_t dst_row_size = actx->dst_row_size; - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; - - const uint32_t src0_nrows = actx->src0_nrows; - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; - - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); - - // no work for this thread - if (src0_start_row >= src0_end_row) { - return; - } - - const uint8_t * data_src0 = actx->data_src0; - uint8_t * data_dst = actx->data_dst; - - // nc/ne0 matches. - const int ne0_val = actx->nc; // == dst->ne[0] - - uint8_t * src0_spad_data = actx->octx->src0_spad.data + (ith * actx->octx->src0_spad.size_per_thread); - uint8_t * dst_spad_data = actx->octx->dst_spad.data + (ith * actx->octx->dst_spad.size_per_thread); - - size_t src0_spad_half_size = actx->src0_spad_half_size; - size_t dst_spad_half_size = actx->dst_spad_half_size; - - // In gelu = x*sigmoid(x*1.702) - const int BLOCK = actx->block; - - if (BLOCK == 0) { - FARF(ERROR, "gelu-f32 : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", - actx->octx->src0_spad.size_per_thread, src0_row_size_aligned); - return; - } - - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; - - // See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - // Dummy DMA transation for sequencing (interleaving dst,src,dst,...) - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), - dst_row_size, dst_row_size_aligned, 0); - - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_size)), - src0_row_size_aligned, src0_row_size, block_size); - } - - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - float* dst_spad = (float *) dma_queue_pop(dma_queue).src; - float* src0_spad = (float *) dma_queue_pop(dma_queue).dst; - - for (uint32_t ib = 0; ib < block_size; ib++) { - const float* src0_spad_ptr = src0_spad + ib * (src0_row_size_aligned / sizeof(float)); - float* dst_spad_ptr = dst_spad + ib * (dst_row_size_aligned / sizeof(float)); - - // gelu = x * sigmoid(1.702 * x) // current implementation - hvx_mul_scalar_f32((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, (float) 1.702, ne0_val); - hvx_sigmoid_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) dst_spad_ptr, ne0_val); - hvx_mul_f32_aaa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, (const uint8_t *) dst_spad_ptr, ne0_val); - } - - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), - dst_row_size, dst_row_size_aligned, block_size); - - // prefetch N+2 loop iteration if any - const uint32_t pref_block = (ir + BLOCK * 2); - if (pref_block < src0_end_row) { - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_size)), - src0_row_size_aligned, src0_row_size, pref_block_size); - } - } - - dma_queue_flush(dma_queue); - - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "gelu-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", ith, nth, ne00, ne01, ne02, - ne03, src0_start_row, src0_end_row, ne0, ne1, ne2, ne3, (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); -} - - -static void unary_silu_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { - struct htp_act_context * actx = (struct htp_act_context *) data; - htp_act_preamble; - - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); - - const size_t src0_row_size = actx->src0_row_size; - const size_t dst_row_size = actx->dst_row_size; - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; - - const uint32_t src0_nrows = actx->src0_nrows; - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; - - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); - - // no work for this thread - if (src0_start_row >= src0_end_row) { - return; - } - - const uint8_t * data_src0 = actx->data_src0; - uint8_t * data_dst = actx->data_dst; - - const int ne0_val = actx->nc; // == dst->ne[0] - - uint8_t * src0_spad_data = actx->octx->src0_spad.data + (ith * actx->octx->src0_spad.size_per_thread); - uint8_t * dst_spad_data = actx->octx->dst_spad.data + (ith * actx->octx->dst_spad.size_per_thread); - - size_t src0_spad_half_size = actx->src0_spad_half_size; - size_t dst_spad_half_size = actx->dst_spad_half_size; - - const int BLOCK = actx->block; - - if (BLOCK == 0) { - FARF(ERROR, "silu-f32 : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", - actx->octx->src0_spad.size_per_thread, src0_row_size_aligned); - return; - } - - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; - - // See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - // Dummy DMA transation for sequencing (interleaving dst,src,dst,...) - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), - dst_row_size, dst_row_size_aligned, 0); - - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_size)), - src0_row_size_aligned, src0_row_size, block_size); - } - - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - float* dst_spad = (float *) dma_queue_pop(dma_queue).src; - float* src0_spad = (float *) dma_queue_pop(dma_queue).dst; - - for (uint32_t ib = 0; ib < block_size; ib++) { - const float* src0_spad_ptr = src0_spad + ib * (src0_row_size_aligned / sizeof(float)); - float* dst_spad_ptr = dst_spad + ib * (dst_row_size_aligned / sizeof(float)); - - // silu = x * sigmoid(x) - hvx_sigmoid_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, ne0_val); - hvx_mul_f32_aaa((uint8_t *) dst_spad_ptr, (const uint8_t *) src0_spad_ptr, (const uint8_t *) dst_spad_ptr, ne0_val); - } - - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), - dst_row_size, dst_row_size_aligned, block_size); - - // prefetch N+2 loop iteration if any - const uint32_t pref_block = (ir + BLOCK * 2); - if (pref_block < src0_end_row) { - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_size)), - src0_row_size_aligned, src0_row_size, pref_block_size); - } - } - - dma_queue_flush(dma_queue); - - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "silu-f32 %d/%d: %ux%ux%ux%u (%u:%u) -> %ux%ux%ux%u usec %u\n", ith, nth, ne00, ne01, ne02, - ne03, src0_start_row, src0_end_row, ne0, ne1, ne2, ne3, (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); } static const float GELU_COEF_A = 0.044715f; static const float SQRT_2_OVER_PI = 0.79788456080286535587989211986876f; -static void glu_geglu_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { - struct htp_act_context * actx = (struct htp_act_context *) data; - htp_act_preamble; +static inline void hvx_geglu_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src0, const uint8_t * restrict src1, uint32_t n) { + assert((unsigned long) dst % 128 == 0); + assert((unsigned long) src0 % 128 == 0); + assert((unsigned long) src1 % 128 == 0); - size_t src0_row_size = actx->src0_row_size; - size_t src1_row_size = actx->src1_row_size; - size_t dst_row_size = actx->dst_row_size; + HVX_Vector * restrict vdst = (HVX_Vector *) dst; + const HVX_Vector * restrict vsrc0 = (const HVX_Vector *) src0; + const HVX_Vector * restrict vsrc1 = (const HVX_Vector *) src1; - uint64_t t1, t2; - t1 = HAP_perf_get_qtimer_count(); + const uint32_t epv = 128 / sizeof(float); + const uint32_t nvec = n / epv; + const uint32_t nloe = n % epv; - const uint32_t src0_nrows = actx->src0_nrows; - const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; + const float GELU_COEF_A_TIMES_SQRT = GELU_COEF_A * SQRT_2_OVER_PI; - const uint32_t src0_start_row = src0_nrows_per_thread * ith; - const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); + const HVX_Vector v_coef_a_times_sqrt = hvx_vec_splat_f32(GELU_COEF_A_TIMES_SQRT); + const HVX_Vector v_sqrt_2_pi = hvx_vec_splat_f32(SQRT_2_OVER_PI); + const HVX_Vector v_half = hvx_vec_splat_f32(0.5f); + const HVX_Vector v_one = hvx_vec_splat_f32(1.0f); + const HVX_Vector v_two = hvx_vec_splat_f32(2.0f); - // no work for this thread - if (src0_start_row >= src0_end_row) { - return; + // Hoisted fast sigmoid / inverse constants to avoid loop-internal overhead + const HVX_Vector v_log2f = Q6_V_vsplat_R(FAST_SIGMOID_LOG2F); + const HVX_Vector v_c1 = Q6_V_vsplat_R(FAST_SIGMOID_C1); + const HVX_Vector v_c2 = Q6_V_vsplat_R(FAST_SIGMOID_C2); + const HVX_Vector v_inv_aprox = Q6_V_vsplat_R(0x7EEEEBB3); + const HVX_Vector v_max_exp = hvx_vec_splat_f32(87.0f); + const HVX_Vector v_min_exp = hvx_vec_splat_f32(-87.0f); + + uint32_t i = 0; + + for (; i < nvec; i++) { + HVX_Vector x = vsrc0[i]; + HVX_Vector g = vsrc1[i]; + + HVX_Vector x2 = hvx_vec_mul_f32_f32(x, x); + HVX_Vector coef = hvx_vec_mul_f32_f32(x2, v_coef_a_times_sqrt); + coef = hvx_vec_add_f32_f32(coef, v_sqrt_2_pi); + HVX_Vector inner = hvx_vec_mul_f32_f32(x, coef); + + // y2 = 2 * inner + HVX_Vector y2 = hvx_vec_mul_f32_f32(inner, v_two); + + // Sigmoid guard check predicates + HVX_VectorPred pred_max = Q6_Q_vcmp_gt_VsfVsf(v_max_exp, y2); + HVX_VectorPred pred_min = Q6_Q_vcmp_gt_VsfVsf(y2, v_min_exp); + + // Fast sigmoid approximation + HVX_Vector v = Q6_Vqf32_vmpy_VsfVsf(y2, v_log2f); + v = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(v), v_half); + + HVX_Vector in_int = hvx_vec_truncate_f32(Q6_Vsf_equals_Vqf32(v)); + HVX_Vector x_sig = Q6_Vqf32_vsub_Vqf32Vsf(v, Q6_Vsf_equals_Vw(in_int)); + HVX_Vector xx_sig = Q6_Vqf32_vmpy_Vqf32Vqf32(x_sig, x_sig); + + HVX_Vector v1 = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(xx_sig), v_c2); + v1 = Q6_Vqf32_vadd_Vqf32Vsf(v1, v_log2f); + + HVX_Vector v2 = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(x_sig), v_c1); + v2 = Q6_Vqf32_vmpy_Vqf32Vqf32(v2, xx_sig); + v2 = Q6_Vqf32_vadd_Vqf32Vqf32(v2, x_sig); + + HVX_Vector v3 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vadd_Vqf32Vqf32(v2, v1)); + v3 = Q6_Vw_vaslacc_VwVwR(v3, in_int, 24); + + HVX_Vector v4 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_Vqf32Vqf32(v2, v1)); + HVX_Vector v5 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_VsfVsf(v3, v4)); + + // Fast division (Newton-Raphson with 2 iterations) + HVX_Vector i_sf = Q6_Vw_vsub_VwVw(v_inv_aprox, v5); + HVX_Vector r_qf = Q6_Vqf32_vmpy_VsfVsf( + i_sf, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_VsfVsf(v_two, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(i_sf, v5))))); + r_qf = Q6_Vqf32_vmpy_Vqf32Vqf32( + r_qf, Q6_Vqf32_vsub_VsfVsf(v_two, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(r_qf), v5)))); + HVX_Vector res_inv = Q6_Vsf_equals_Vqf32(r_qf); + + HVX_Vector sig2y = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(v3, res_inv)); + + // Sigmoid guards + sig2y = Q6_V_vmux_QVV(pred_max, sig2y, v_one); + sig2y = Q6_V_vmux_QVV(pred_min, sig2y, Q6_V_vzero()); + + // tanh(inner) = 2 * sigmoid(2 * inner) - 1 + HVX_Vector tanh_val = hvx_vec_mul_f32_f32(sig2y, v_two); + tanh_val = hvx_vec_sub_f32_f32(tanh_val, v_one); + + HVX_Vector tanh_plus_one = hvx_vec_add_f32_f32(tanh_val, v_one); + HVX_Vector half_x = hvx_vec_mul_f32_f32(x, v_half); + HVX_Vector gelu_x = hvx_vec_mul_f32_f32(half_x, tanh_plus_one); + + vdst[i] = hvx_vec_mul_f32_f32(gelu_x, g); } - const uint8_t * restrict data_src0 = actx->data_src0; - const uint8_t * restrict data_src1 = actx->data_src1; - uint8_t * restrict data_dst = actx->data_dst; + if (nloe) { + HVX_Vector x = vsrc0[i]; + HVX_Vector g = vsrc1[i]; - const int nc = actx->nc; + HVX_Vector x2 = hvx_vec_mul_f32_f32(x, x); + HVX_Vector coef = hvx_vec_mul_f32_f32(x2, v_coef_a_times_sqrt); + coef = hvx_vec_add_f32_f32(coef, v_sqrt_2_pi); + HVX_Vector inner = hvx_vec_mul_f32_f32(x, coef); - const size_t src0_row_size_aligned = actx->src0_row_size_aligned; - const size_t src1_row_size_aligned = actx->src1_row_size_aligned; - const size_t dst_row_size_aligned = actx->dst_row_size_aligned; + HVX_Vector y2 = hvx_vec_mul_f32_f32(inner, v_two); - uint8_t * restrict src0_spad_data = actx->octx->src0_spad.data + (ith * actx->octx->src0_spad.size_per_thread); - uint8_t * restrict src1_spad_data = actx->octx->src1_spad.data + (ith * actx->octx->src1_spad.size_per_thread); - uint8_t * restrict dst_spad_data = actx->octx->dst_spad.data + (ith * actx->octx->dst_spad.size_per_thread); + HVX_VectorPred pred_max = Q6_Q_vcmp_gt_VsfVsf(v_max_exp, y2); + HVX_VectorPred pred_min = Q6_Q_vcmp_gt_VsfVsf(y2, v_min_exp); - size_t src0_spad_half_size = actx->src0_spad_half_size; - size_t src1_spad_half_size = actx->src1_spad_half_size; - size_t dst_spad_half_size = actx->dst_spad_half_size; + HVX_Vector v = Q6_Vqf32_vmpy_VsfVsf(y2, v_log2f); + v = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(v), v_half); - const int BLOCK = actx->block; - if (BLOCK == 0) { - FARF(ERROR, - "geglu-f32 : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", - actx->octx->src0_spad.size_per_thread, src0_row_size_aligned); - return; + HVX_Vector in_int = hvx_vec_truncate_f32(Q6_Vsf_equals_Vqf32(v)); + HVX_Vector x_sig = Q6_Vqf32_vsub_Vqf32Vsf(v, Q6_Vsf_equals_Vw(in_int)); + HVX_Vector xx_sig = Q6_Vqf32_vmpy_Vqf32Vqf32(x_sig, x_sig); + + HVX_Vector v1 = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(xx_sig), v_c2); + v1 = Q6_Vqf32_vadd_Vqf32Vsf(v1, v_log2f); + + HVX_Vector v2 = Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(x_sig), v_c1); + v2 = Q6_Vqf32_vmpy_Vqf32Vqf32(v2, xx_sig); + v2 = Q6_Vqf32_vadd_Vqf32Vqf32(v2, x_sig); + + HVX_Vector v3 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vadd_Vqf32Vqf32(v2, v1)); + v3 = Q6_Vw_vaslacc_VwVwR(v3, in_int, 24); + + HVX_Vector v4 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_Vqf32Vqf32(v2, v1)); + HVX_Vector v5 = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_VsfVsf(v3, v4)); + + HVX_Vector i_sf = Q6_Vw_vsub_VwVw(v_inv_aprox, v5); + HVX_Vector r_qf = Q6_Vqf32_vmpy_VsfVsf( + i_sf, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vsub_VsfVsf(v_two, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(i_sf, v5))))); + r_qf = Q6_Vqf32_vmpy_Vqf32Vqf32( + r_qf, Q6_Vqf32_vsub_VsfVsf(v_two, Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(Q6_Vsf_equals_Vqf32(r_qf), v5)))); + HVX_Vector res_inv = Q6_Vsf_equals_Vqf32(r_qf); + + HVX_Vector sig2y = Q6_Vsf_equals_Vqf32(Q6_Vqf32_vmpy_VsfVsf(v3, res_inv)); + + sig2y = Q6_V_vmux_QVV(pred_max, sig2y, v_one); + sig2y = Q6_V_vmux_QVV(pred_min, sig2y, Q6_V_vzero()); + + HVX_Vector tanh_val = hvx_vec_mul_f32_f32(sig2y, v_two); + tanh_val = hvx_vec_sub_f32_f32(tanh_val, v_one); + + HVX_Vector tanh_plus_one = hvx_vec_add_f32_f32(tanh_val, v_one); + HVX_Vector half_x = hvx_vec_mul_f32_f32(x, v_half); + HVX_Vector gelu_x = hvx_vec_mul_f32_f32(half_x, tanh_plus_one); + + HVX_Vector res = hvx_vec_mul_f32_f32(gelu_x, g); + hvx_vec_store_a((void *) &vdst[i], nloe * sizeof(float), res); } - - dma_queue * dma_queue = actx->octx->ctx->dma[ith]; - - // See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 - for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - // Dummy DMA transation for sequencing (interleaving dst,src,dst,...) - dma_queue_push_vtcm_to_ddr(dma_queue, - dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), - dst_row_size, dst_row_size_aligned, 0); - - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_size)), - src0_row_size_aligned, src0_row_size, block_size); - dma_queue_push_ddr_to_vtcm(dma_queue, - dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_size)), - src1_row_size_aligned, src1_row_size, block_size); - } - - for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { - const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); - - float * dst_spad = (float *) dma_queue_pop(dma_queue).src; - float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; - float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; - - for (uint32_t ib = 0; ib < block_size; ib++) { - const uint8_t * src0_spad_ptr = (const uint8_t *)(src0_spad + ib * (src0_row_size_aligned / sizeof(float))); - const uint8_t * src1_spad_ptr = (const uint8_t *)(src1_spad + ib * (src1_row_size_aligned / sizeof(float))); - uint8_t * dst_spad_ptr = (uint8_t *)(dst_spad + ib * (dst_row_size_aligned / sizeof(float))); - - // geglu tanh implementation - // geglu(x, g) = gelu(x) * g - // gelu(x) = 0.5f*x*(1.0f + tanhf(SQRT_2_OVER_PI*x*(1.0f + GELU_COEF_A*x*x))) - hvx_mul_f32_aaa(dst_spad_ptr, src0_spad_ptr, src0_spad_ptr, nc); // res = x*x - hvx_mul_scalar_f32_aa(dst_spad_ptr, (const uint8_t *)dst_spad_ptr, GELU_COEF_A, nc); // res = res * GELU_COEF_A - hvx_add_scalar_f32_aa(dst_spad_ptr, (const uint8_t *)dst_spad_ptr, 1.0f, nc); // res = res + 1.0f - hvx_mul_f32_aaa(dst_spad_ptr, src0_spad_ptr, (const uint8_t *)dst_spad_ptr, nc); // res = res * x - hvx_mul_scalar_f32_aa(dst_spad_ptr, (const uint8_t*)dst_spad_ptr, SQRT_2_OVER_PI, nc); // res = result * SQRT_2_OVER_PI - hvx_tanh_f32_aa((uint8_t *) dst_spad_ptr, (const uint8_t *) dst_spad_ptr, nc); // res = tanh(res) - hvx_add_scalar_f32_aa(dst_spad_ptr, (const uint8_t*)dst_spad_ptr, 1.0f, nc); // res = res + 1.0f - hvx_mul_f32_aaa(dst_spad_ptr, src0_spad_ptr, (const uint8_t *)dst_spad_ptr, nc); // res = res * x - hvx_mul_scalar_f32_aa(dst_spad_ptr, (const uint8_t *)dst_spad_ptr, 0.5f, nc); // res = res + 0.5f - hvx_mul_f32_aaa(dst_spad_ptr, (const uint8_t *)dst_spad_ptr, src1_spad_ptr, nc); // res = res * g - } - - dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), dst_row_size, - dst_row_size_aligned, block_size); - - // prefetch N+2 loop iteration if any - const uint32_t pref_block = (ir + BLOCK * 2); - if (pref_block < src0_end_row) { - const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_size)), - src0_row_size_aligned, src0_row_size, pref_block_size); - dma_queue_push_ddr_to_vtcm(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_size)), - src1_row_size_aligned, src1_row_size, pref_block_size); - } - } - - dma_queue_flush(dma_queue); - - t2 = HAP_perf_get_qtimer_count(); - - FARF(HIGH, "geglu-f32 %d/%d: %ux%ux%ux%u (%u:%u) x %ux%ux%ux%u -> %ux%ux%ux%u usec %u\n", ith, nth, - ne00, ne01, ne02, ne03, src0_start_row, src0_end_row, ne10, ne11, ne12, ne13, ne0, ne1, ne2, ne3, - (unsigned) HAP_perf_qtimer_count_to_us(t2 - t1)); } +// geglu(x, g) = gelu(x) * g +static void geglu_f32(const float * restrict src0, + const float * restrict src1, + float * restrict dst, + const uint32_t num_rows, + const struct htp_act_context * actx) { + htp_glu_op_preamble; + + for (uint32_t ib = 0; ib < num_rows; ib++) { + const uint8_t * restrict src0_ptr = (const uint8_t *) src0 + (ib * src0_row_size_aligned); + const uint8_t * restrict src1_ptr = (const uint8_t *) src1 + (ib * src1_row_size_aligned); + uint8_t * restrict dst_ptr = (uint8_t *) dst + (ib * dst_row_size_aligned); + + hvx_geglu_f32_aa(dst_ptr, src0_ptr, src1_ptr, nc); + } +} + +#define DEFINE_GLU_PER_THREAD(NAME, OP_STR, CORE_EXPR) \ + static void glu_##NAME##_f32_per_thread(unsigned int nth, unsigned int ith, void * data) { \ + struct htp_act_context * actx = (struct htp_act_context *) data; \ + htp_act_preamble; \ + \ + struct htp_thread_trace * tr = actx->octx->ctx ? &actx->octx->ctx->trace[ith] : NULL; \ + \ + size_t src0_row_size = actx->src0_row_size; \ + size_t src1_row_size = actx->src1_row_size; \ + size_t dst_row_size = actx->dst_row_size; \ + \ + size_t src0_row_stride = actx->src0_row_stride; \ + size_t src1_row_stride = actx->src1_row_stride; \ + \ + const uint32_t src0_nrows = actx->src0_nrows; \ + const uint32_t src0_nrows_per_thread = actx->src0_nrows_per_thread; \ + \ + const uint32_t src0_start_row = src0_nrows_per_thread * ith; \ + const uint32_t src0_end_row = MIN(src0_start_row + src0_nrows_per_thread, src0_nrows); \ + \ + /* no work for this thread */ \ + if (src0_start_row >= src0_end_row) { \ + return; \ + } \ + \ + const uint8_t * restrict data_src0 = actx->data_src0; \ + const uint8_t * restrict data_src1 = actx->data_src1; \ + uint8_t * restrict data_dst = actx->data_dst; \ + \ + const size_t src0_row_size_aligned = actx->src0_row_size_aligned; \ + const size_t src1_row_size_aligned = actx->src1_row_size_aligned; \ + const size_t dst_row_size_aligned = actx->dst_row_size_aligned; \ + \ + uint8_t * restrict src0_spad_data = actx->vtcm_src0 + (ith * actx->vtcm_src0_size_per_thread); \ + uint8_t * restrict src1_spad_data = actx->vtcm_src1 + (ith * actx->vtcm_src1_size_per_thread); \ + uint8_t * restrict dst_spad_data = actx->vtcm_dst + (ith * actx->vtcm_dst_size_per_thread); \ + \ + size_t src0_spad_half_size = actx->src0_spad_half_size; \ + size_t src1_spad_half_size = actx->src1_spad_half_size; \ + size_t dst_spad_half_size = actx->dst_spad_half_size; \ + \ + const int BLOCK = actx->block; \ + if (BLOCK == 0) { \ + FARF(ERROR, \ + OP_STR \ + " : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", \ + actx->vtcm_src0_size_per_thread, src0_row_size_aligned); \ + return; \ + } \ + \ + dma_queue * dma_queue = actx->octx->ctx->dma[ith]; \ + \ + /* See discussion: https://github.com/ggml-org/llama.cpp/pull/18151#issuecomment-3678235379 */ \ + for (uint32_t ir = src0_start_row, spad_idx = 0; ir < src0_end_row && spad_idx < 2; ir += BLOCK, spad_idx++) { \ + const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ + \ + /* Dummy DMA transation for sequencing (interleaving dst,src,dst,...) */ \ + dma_queue_push_vtcm_to_ddr(dma_queue, \ + dma_make_ptr(data_dst, dst_spad_data + (spad_idx * dst_spad_half_size)), \ + dst_row_size, dst_row_size_aligned, 0); \ + \ + dma_queue_push( \ + dma_queue, \ + dma_make_ptr(src0_spad_data + (spad_idx * src0_spad_half_size), data_src0 + (ir * src0_row_stride)), \ + src0_row_size_aligned, src0_row_stride, src0_row_size, block_size); \ + dma_queue_push( \ + dma_queue, \ + dma_make_ptr(src1_spad_data + (spad_idx * src1_spad_half_size), data_src1 + (ir * src1_row_stride)), \ + src1_row_size_aligned, src1_row_stride, src1_row_size, block_size); \ + } \ + \ + for (uint32_t ir = src0_start_row; ir < src0_end_row; ir += BLOCK) { \ + const uint32_t block_size = MIN(BLOCK, src0_end_row - ir); \ + \ + float * dst_spad = (float *) dma_queue_pop(dma_queue).src; \ + float * src0_spad = (float *) dma_queue_pop(dma_queue).dst; \ + float * src1_spad = (float *) dma_queue_pop(dma_queue).dst; \ + \ + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ + CORE_EXPR; \ + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ir); \ + \ + dma_queue_push_vtcm_to_ddr(dma_queue, dma_make_ptr(data_dst + (ir * dst_row_size), dst_spad), \ + dst_row_size, dst_row_size_aligned, block_size); \ + \ + /* prefetch N+2 loop iteration if any */ \ + const uint32_t pref_block = (ir + BLOCK * 2); \ + if (pref_block < src0_end_row) { \ + const uint32_t pref_block_size = MIN(BLOCK, src0_end_row - pref_block); \ + dma_queue_push(dma_queue, dma_make_ptr(src0_spad, data_src0 + (pref_block * src0_row_stride)), \ + src0_row_size_aligned, src0_row_stride, src0_row_size, pref_block_size); \ + dma_queue_push(dma_queue, dma_make_ptr(src1_spad, data_src1 + (pref_block * src1_row_stride)), \ + src1_row_size_aligned, src1_row_stride, src1_row_size, pref_block_size); \ + } \ + } \ + \ + dma_queue_flush(dma_queue); \ + \ + } + +DEFINE_GLU_PER_THREAD(swiglu, "swiglu-f32", swiglu_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) +DEFINE_GLU_PER_THREAD(swiglu_oai, "swiglu-oai-f32", swiglu_oai_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) +DEFINE_GLU_PER_THREAD(geglu, "geglu-f32", geglu_f32(src0_spad, src1_spad, dst_spad, block_size, actx)) + static int execute_op_activations_f32(struct htp_ops_context * octx) { const struct htp_tensor * src0 = octx->src[0]; const struct htp_tensor * src1 = octx->src[1]; const struct htp_tensor * dst = octx->dst; - if (((src0->ne[0] * SIZEOF_FP32) != src0->nb[1]) || ((dst->ne[0] * SIZEOF_FP32) != dst->nb[1])) { - FARF(ERROR, "Non-contiguous tensors are not supported at this time \n"); + if ((dst->ne[0] * SIZEOF_FP32) != dst->nb[1]) { + FARF(ERROR, "Non-contiguous dst is not supported at this time \n"); return HTP_STATUS_NO_SUPPORT; } @@ -636,11 +469,6 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { const char * op_type = NULL; switch (octx->op) { - case HTP_OP_UNARY_SILU: - act_op_func = (worker_callback_t)unary_silu_f32_per_thread; - op_type = "silu-f32"; - break; - case HTP_OP_GLU_SWIGLU: act_op_func = (worker_callback_t)glu_swiglu_f32_per_thread; op_type = "swiglu-f32"; @@ -650,10 +478,6 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { act_op_func = (worker_callback_t)glu_swiglu_oai_f32_per_thread; op_type = "swiglu-oai-f32"; break; - case HTP_OP_UNARY_GELU: - act_op_func = (worker_callback_t)unary_gelu_f32_per_thread; - op_type = "gelu-f32"; - break; case HTP_OP_GLU_GEGLU: act_op_func = (worker_callback_t)glu_geglu_f32_per_thread; @@ -667,51 +491,39 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { const uint32_t src0_nrows = src0->ne[1] * src0->ne[2] * src0->ne[3]; const uint32_t n_threads = MIN(octx->n_threads, src0_nrows); - size_t src0_row_size = src0->nb[1]; - size_t src1_row_size = src1 ? src1->nb[1] : src0->nb[1]; - size_t dst_row_size = dst->nb[1]; + // row_size = bytes of useful data per row (what the kernel touches / what DMA copies). + // row_stride = bytes between successive rows in DDR (may exceed row_size for non-contig src). + const size_t nc_bytes = dst->ne[0] * SIZEOF_FP32; + const size_t src0_row_size = nc_bytes; + const size_t src1_row_size = nc_bytes; + const size_t dst_row_size = nc_bytes; + const size_t src0_row_stride = src0->nb[1]; + const size_t src1_row_stride = src1 ? src1->nb[1] : src0->nb[1]; const size_t src0_row_size_aligned = hex_round_up(src0_row_size, VLEN); const size_t src1_row_size_aligned = hex_round_up(src1_row_size, VLEN); const size_t dst_row_size_aligned = hex_round_up(dst_row_size, VLEN); - // VTCM scratchpads for all tensors - // N rows per thread, padded to HVX vector size - size_t spad_size_per_row = (src0_row_size_aligned + src1_row_size_aligned) + dst_row_size_aligned; - size_t vtcm_row_per_thread = (octx->ctx->vtcm_size)/ (n_threads* spad_size_per_row); + struct htp_act_vtcm_layout L; + htp_act_vtcm_layout_build(&L, src0_row_size_aligned, src1_row_size_aligned, dst_row_size_aligned, n_threads, + octx->ctx->vtcm_size); // Make sure the reserved vtcm size is sufficient - if (vtcm_row_per_thread == 0) { + if (L.vtcm_row_per_thread == 0) { FARF(ERROR, "act-%s : current VTCM reservation %zu is too small for even 1 row per thread, needed at least %zu\n", op_type, octx->ctx->vtcm_size, - spad_size_per_row * n_threads); + (src0_row_size_aligned + src1_row_size_aligned + dst_row_size_aligned) * n_threads); return HTP_STATUS_VTCM_TOO_SMALL; } - octx->src0_spad.size_per_thread = src0_row_size_aligned * vtcm_row_per_thread; - octx->src1_spad.size_per_thread = src1_row_size_aligned * vtcm_row_per_thread; - octx->dst_spad.size_per_thread = dst_row_size_aligned * vtcm_row_per_thread; - - octx->dst_spad.size = n_threads* octx->dst_spad.size_per_thread; - octx->src0_spad.size = n_threads* octx->src0_spad.size_per_thread; - octx->src1_spad.size = n_threads* octx->src1_spad.size_per_thread; - - octx->src0_spad.data = octx->ctx->vtcm_base; - octx->src1_spad.data = octx->src0_spad.data + octx->src0_spad.size; - octx->dst_spad.data = octx->src1_spad.data + octx->src1_spad.size; - - octx->src0_spad.src = NULL; - octx->src1_spad.src = NULL; - octx->dst_spad.src = NULL; - if (src1) { - FARF(HIGH, "%s: %ux%ux%ux%u x %ux%ux%ux%u -> %ux%ux%ux%u : src0-spad-size %u src1-spad-size %u dst-spad-size %u\n", + FARF(HIGH, "%s: %ux%ux%ux%u x %ux%ux%ux%u -> %ux%ux%ux%u : src0-vtcm-size %zu src1-vtcm-size %zu dst-vtcm-size %zu\n", op_type, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], src1->ne[0], src1->ne[1], src1->ne[2], - src1->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], octx->src0_spad.size, octx->src1_spad.size, - octx->dst_spad.size); + src1->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], L.src0_bytes_per_thread * n_threads, + L.src1_bytes_per_thread * n_threads, L.dst_bytes_per_thread * n_threads); } else { - FARF(HIGH, "%s: %ux%ux%ux%u -> %ux%ux%ux%u : src0-spad-size %u src1-spad-size %u dst-spad-size %u\n", op_type, + FARF(HIGH, "%s: %ux%ux%ux%u -> %ux%ux%ux%u : src0-vtcm-size %zu src1-vtcm-size %zu dst-vtcm-size %zu\n", op_type, src0->ne[0], src0->ne[1], src0->ne[2], src0->ne[3], dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3], - octx->src0_spad.size, octx->src1_spad.size, octx->dst_spad.size); + L.src0_bytes_per_thread * n_threads, L.src1_bytes_per_thread * n_threads, L.dst_bytes_per_thread * n_threads); } if ((octx->flags & HTP_OPFLAGS_SKIP_COMPUTE)) { @@ -732,9 +544,21 @@ static int execute_op_activations_f32(struct htp_ops_context * octx) { actx.src1_row_size_aligned = src1_row_size_aligned; actx.dst_row_size_aligned = dst_row_size_aligned; - actx.src0_spad_half_size = octx->src0_spad.size_per_thread / 2; - actx.src1_spad_half_size = octx->src1_spad.size_per_thread / 2; - actx.dst_spad_half_size = octx->dst_spad.size_per_thread / 2; + actx.src0_row_stride = src0_row_stride; + actx.src1_row_stride = src1_row_stride; + + uint8_t * const base = (uint8_t *) octx->ctx->vtcm_base; + actx.vtcm_src0 = VTCM_LAYOUT_PTR(uint8_t, base, L.off_src0); + actx.vtcm_src1 = VTCM_LAYOUT_PTR(uint8_t, base, L.off_src1); + actx.vtcm_dst = VTCM_LAYOUT_PTR(uint8_t, base, L.off_dst); + + actx.vtcm_src0_size_per_thread = L.src0_bytes_per_thread; + actx.vtcm_src1_size_per_thread = L.src1_bytes_per_thread; + actx.vtcm_dst_size_per_thread = L.dst_bytes_per_thread; + + actx.src0_spad_half_size = L.src0_bytes_per_thread / 2; + actx.src1_spad_half_size = L.src1_bytes_per_thread / 2; + actx.dst_spad_half_size = L.dst_bytes_per_thread / 2; actx.block = actx.src0_spad_half_size / actx.src0_row_size_aligned; actx.src0_nrows = src0_nrows; diff --git a/ggml/src/ggml-hexagon/htp/main.c b/ggml/src/ggml-hexagon/htp/main.c index e76ac7f48..321b832e1 100644 --- a/ggml/src/ggml-hexagon/htp/main.c +++ b/ggml/src/ggml-hexagon/htp/main.c @@ -723,14 +723,14 @@ static int execute_op(struct htp_ops_context * octx) { case HTP_OP_SQRT: case HTP_OP_UNARY_SOFTPLUS: case HTP_OP_UNARY_SIGMOID: + case HTP_OP_UNARY_SILU: + case HTP_OP_UNARY_GELU: case HTP_OP_UNARY_NEG: case HTP_OP_UNARY_EXP: case HTP_OP_UNARY_TANH: case HTP_OP_L2_NORM: return op_unary(octx); - case HTP_OP_UNARY_SILU: - case HTP_OP_UNARY_GELU: case HTP_OP_GLU_SWIGLU: case HTP_OP_GLU_SWIGLU_OAI: case HTP_OP_GLU_GEGLU: diff --git a/ggml/src/ggml-hexagon/htp/unary-ops.c b/ggml/src/ggml-hexagon/htp/unary-ops.c index c6806efc3..b21415a67 100644 --- a/ggml/src/ggml-hexagon/htp/unary-ops.c +++ b/ggml/src/ggml-hexagon/htp/unary-ops.c @@ -276,6 +276,39 @@ static void sigmoid_f32(const float * restrict src, } } +// silu(x) = x * sigmoid(x) +static void silu_f32(const float * restrict src, + float * restrict dst, + const uint32_t num_rows, + const struct htp_unary_context * uctx) { + htp_unary_op_preamble; + + for (uint32_t ir = 0; ir < num_rows; ir++) { + const uint8_t * restrict src_local = (const uint8_t *)src + (ir * src0_row_size_aligned); + uint8_t * restrict dst_local = (uint8_t *)dst + (ir * dst_row_size_aligned); + + hvx_sigmoid_f32_aa(dst_local, src_local, ne0); + hvx_mul_f32_aaa(dst_local, src_local, dst_local, ne0); + } +} + +// gelu(x) = x * sigmoid(1.702 * x) (quick/sigmoid approximation, matches CPU GELU_QUICK reference) +static void gelu_f32(const float * restrict src, + float * restrict dst, + const uint32_t num_rows, + const struct htp_unary_context * uctx) { + htp_unary_op_preamble; + + for (uint32_t ir = 0; ir < num_rows; ir++) { + const uint8_t * restrict src_local = (const uint8_t *)src + (ir * src0_row_size_aligned); + uint8_t * restrict dst_local = (uint8_t *)dst + (ir * dst_row_size_aligned); + + hvx_mul_scalar_f32(dst_local, src_local, 1.702f, ne0); + hvx_sigmoid_f32_aa(dst_local, dst_local, ne0); + hvx_mul_f32_aaa(dst_local, src_local, dst_local, ne0); + } +} + static void tri_f32(const float * restrict src, float * restrict dst, const uint32_t num_rows, @@ -566,6 +599,8 @@ DEFINE_UNARY_TASK(sqrt, false, false, sqrt_f32(src0_vtcm, dst_vtcm, bl DEFINE_UNARY_TASK(unary_neg, false, false, neg_f32(src0_vtcm, dst_vtcm, block_size, uctx)) DEFINE_UNARY_TASK(unary_exp, false, false, exp_f32(src0_vtcm, dst_vtcm, block_size, uctx)) DEFINE_UNARY_TASK(unary_sigmoid, false, false, sigmoid_f32(src0_vtcm, dst_vtcm, block_size, uctx)) +DEFINE_UNARY_TASK(unary_silu, false, false, silu_f32(src0_vtcm, dst_vtcm, block_size, uctx)) +DEFINE_UNARY_TASK(unary_gelu, false, false, gelu_f32(src0_vtcm, dst_vtcm, block_size, uctx)) DEFINE_UNARY_TASK(unary_softplus, false, false, softplus_f32(src0_vtcm, dst_vtcm, block_size, uctx)) DEFINE_UNARY_TASK(unary_tanh, false, false, tanh_f32(src0_vtcm, dst_vtcm, block_size, uctx)) DEFINE_UNARY_TASK(l2_norm, false, false, l2_norm_f32(src0_vtcm, dst_vtcm, block_size, uctx)) @@ -717,6 +752,19 @@ static inline void tile_unary_softplus_f32(uint8_t * dst_vtcm, const uint8_t * s } } +// silu(x) = x * sigmoid(x) +static inline void tile_silu_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm, uint32_t tw) { + hvx_sigmoid_f32_aa(dst_vtcm, src_vtcm, tw); + hvx_mul_f32_aaa(dst_vtcm, src_vtcm, dst_vtcm, tw); +} + +// gelu(x) = x * sigmoid(1.702 * x) (quick/sigmoid approximation, matches CPU GELU_QUICK reference) +static inline void tile_gelu_f32(uint8_t * dst_vtcm, const uint8_t * src_vtcm, uint32_t tw) { + hvx_mul_scalar_f32(dst_vtcm, src_vtcm, 1.702f, tw); + hvx_sigmoid_f32_aa(dst_vtcm, dst_vtcm, tw); + hvx_mul_f32_aaa(dst_vtcm, src_vtcm, dst_vtcm, tw); +} + // Triangular mask applied to one column tile. Boundary is an absolute column index, so // each vector compares against its absolute column position (col_start + i*VLEN_FP32). static inline void tri_apply_tile_f32(const uint8_t * restrict src, uint8_t * restrict dst, @@ -798,6 +846,8 @@ DEFINE_UNARY_TILED_TASK(sqrt, false, hvx_sqrt_f32_aa(dst_vtcm, src_vtc DEFINE_UNARY_TILED_TASK(unary_neg, false, hvx_scale_f32_aa(dst_vtcm, src_vtcm, tw, -1.0f)) DEFINE_UNARY_TILED_TASK(unary_exp, false, hvx_exp_f32(dst_vtcm, src_vtcm, tw, false)) DEFINE_UNARY_TILED_TASK(unary_sigmoid, false, hvx_sigmoid_f32_aa(dst_vtcm, src_vtcm, tw)) +DEFINE_UNARY_TILED_TASK(unary_silu, false, tile_silu_f32(dst_vtcm, src_vtcm, tw)) +DEFINE_UNARY_TILED_TASK(unary_gelu, false, tile_gelu_f32(dst_vtcm, src_vtcm, tw)) DEFINE_UNARY_TILED_TASK(unary_softplus, false, tile_unary_softplus_f32(dst_vtcm, src_vtcm, tw)) DEFINE_UNARY_TILED_TASK(unary_tanh, false, hvx_tanh_f32_aa(dst_vtcm, src_vtcm, tw)) DEFINE_UNARY_TILED_TASK(tri, true, tri_apply_tile_f32(src_vtcm, dst_vtcm, tw, col, i01, ne0, tri_ttype)) @@ -821,6 +871,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) { case HTP_OP_UNARY_NEG: op_type = "neg-f32"; break; case HTP_OP_UNARY_EXP: op_type = "exp-f32"; break; case HTP_OP_UNARY_SIGMOID: op_type = "sigmoid-f32"; break; + case HTP_OP_UNARY_SILU: op_type = "silu-f32"; break; + case HTP_OP_UNARY_GELU: op_type = "gelu-f32"; break; case HTP_OP_UNARY_SOFTPLUS: op_type = "softplus-f32"; break; case HTP_OP_UNARY_TANH: op_type = "tanh-f32"; break; case HTP_OP_L2_NORM: op_type = "l2norm-f32"; break; @@ -917,6 +969,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) { case HTP_OP_UNARY_NEG: task_func = unary_task_f32_tiled_unary_neg; break; case HTP_OP_UNARY_EXP: task_func = unary_task_f32_tiled_unary_exp; break; case HTP_OP_UNARY_SIGMOID: task_func = unary_task_f32_tiled_unary_sigmoid; break; + case HTP_OP_UNARY_SILU: task_func = unary_task_f32_tiled_unary_silu; break; + case HTP_OP_UNARY_GELU: task_func = unary_task_f32_tiled_unary_gelu; break; case HTP_OP_UNARY_SOFTPLUS: task_func = unary_task_f32_tiled_unary_softplus; break; case HTP_OP_UNARY_TANH: task_func = unary_task_f32_tiled_unary_tanh; break; case HTP_OP_TRI: task_func = unary_task_f32_tiled_tri; break; @@ -934,6 +988,8 @@ static int execute_op_unary_f32(struct htp_ops_context * octx) { case HTP_OP_UNARY_NEG: task_func = unary_task_f32_unary_neg; break; case HTP_OP_UNARY_EXP: task_func = unary_task_f32_unary_exp; break; case HTP_OP_UNARY_SIGMOID: task_func = unary_task_f32_unary_sigmoid; break; + case HTP_OP_UNARY_SILU: task_func = unary_task_f32_unary_silu; break; + case HTP_OP_UNARY_GELU: task_func = unary_task_f32_unary_gelu; break; case HTP_OP_UNARY_SOFTPLUS: task_func = unary_task_f32_unary_softplus; break; case HTP_OP_UNARY_TANH: task_func = unary_task_f32_unary_tanh; break; case HTP_OP_L2_NORM: task_func = unary_task_f32_l2_norm; break; diff --git a/ggml/src/ggml-hexagon/htp/unary-ops.h b/ggml/src/ggml-hexagon/htp/unary-ops.h index 4005e6515..1f4c3a5c4 100644 --- a/ggml/src/ggml-hexagon/htp/unary-ops.h +++ b/ggml/src/ggml-hexagon/htp/unary-ops.h @@ -51,6 +51,8 @@ static inline bool htp_op_is_unary(uint32_t opcode) { case HTP_OP_UNARY_NEG: case HTP_OP_UNARY_EXP: case HTP_OP_UNARY_SIGMOID: + case HTP_OP_UNARY_SILU: + case HTP_OP_UNARY_GELU: case HTP_OP_UNARY_SOFTPLUS: case HTP_OP_UNARY_TANH: case HTP_OP_L2_NORM: From 1a064ab0921238c1daa397d6f4a900ef33884de2 Mon Sep 17 00:00:00 2001 From: Oliver Simons Date: Wed, 22 Jul 2026 19:28:02 +0200 Subject: [PATCH 06/21] CUDA: Improve NVFP4 W4A4 activation quantization (#25730) * Squash history before conflict-resolution during rebase on master WIP commit Add 32-byte loads, restore per-block amax Use nvfp4x4 intrinsic when available Fuse per-channel amax and quantization kernels Do pointer arithmetic only once on x Remove unnecessary ternary in the load We assert on host side that ne00 is 64-aligned Add back scale-search, but optimize it with intrinsics Code cleanup Make scale in MMQ-epilogue NVFP4-specific/restrictive for now Remove unneeded include, add comment Fix trailing whitespace Guard __builtin_align__(32) struct to NVIDIA Seems like HIP doesn't have this available, see https://github.com/ggml-org/llama.cpp/actions/runs/29438651734/job/87431623001 * compiler massaging to avoid unnecessary LDCs * kvalues_mxfp4 -> kvalues_nvfp4 in quantize_mmq_nvfp4 * Always pass in src1_scale.ptr * Extract ggml_cuda_is_aligned helper --- ggml/src/ggml-cuda/common.cuh | 9 + ggml/src/ggml-cuda/mmq.cu | 33 +++- ggml/src/ggml-cuda/mmq.cuh | 107 ++++++++-- ggml/src/ggml-cuda/quantize.cu | 339 +++++++++++++++++++++++--------- ggml/src/ggml-cuda/quantize.cuh | 4 + 5 files changed, 370 insertions(+), 122 deletions(-) diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh index ba5f673c7..fa2dd2620 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh @@ -362,6 +362,15 @@ static bool blackwell_mma_available(const int cc) { ggml_cuda_highest_compiled_arch(cc) < GGML_CUDA_CC_RUBIN; } +// Checks whether the tensor's base data pointer and higher-dimensional strides are byte-aligned to `alignment` bytes. +static bool ggml_cuda_is_aligned(const ggml_tensor * tensor, const size_t alignment) { + GGML_ASSERT(tensor != nullptr); + return (reinterpret_cast(tensor->data) % alignment) == 0 && + tensor->nb[1] % alignment == 0 && + tensor->nb[2] % alignment == 0 && + tensor->nb[3] % alignment == 0; +} + static constexpr __device__ int ggml_cuda_get_physical_warp_size() { #if defined(GGML_USE_HIP) && (defined(__GFX9__) || defined(__GFX8__)) return 64; diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 0dd68ebfb..1f0747456 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -130,14 +130,20 @@ void ggml_cuda_mul_mat_q( const size_t nbytes_src1_q8_1 = ne13*ne12 * ne11*ne10_padded * y_block_size/y_values_per_block + ggml_cuda_mmq_get_J_max(src0->type, fallback, cc, ne11) * sizeof(block_q8_1_mmq); ggml_cuda_pool_alloc src1_q8_1(ctx.pool(), nbytes_src1_q8_1); + ggml_cuda_pool_alloc src1_scale(ctx.pool()); + if (src0->type == GGML_TYPE_NVFP4 && use_native_fp4) { + src1_scale.alloc(ne13*ne12*ne11); + } { const int64_t s11 = src1->nb[1] / ts_src1; const int64_t s12 = src1->nb[2] / ts_src1; const int64_t s13 = src1->nb[3] / ts_src1; if (use_native_fp4) { + static constexpr size_t align_float8 = 32; + const bool use_aligned_float8 = ggml_cuda_is_aligned(src1, align_float8); static_assert(sizeof(block_fp4_mmq) == 4 * sizeof(block_q8_1)); - quantize_mmq_fp4_cuda(src1_d, nullptr, src1_q8_1.get(), src0->type, ne10, s11, s12, s13, ne10_padded, + quantize_mmq_fp4_cuda(src1_d, nullptr, src1_q8_1.get(), src1_scale.ptr, src0->type, use_aligned_float8, ne10, s11, s12, s13, ne10_padded, ne11, ne12, ne13, stream); } else { @@ -155,6 +161,7 @@ void ggml_cuda_mul_mat_q( const mmq_args args = { src0_d, src0->type, (const int *) src1_q8_1.ptr, nullptr, nullptr, dst_d, + src0->type == GGML_TYPE_NVFP4 && use_native_fp4 ? src1_scale.ptr : nullptr, ne00, ne01, ne1, s01, ne11, s1, ne02, ne12, s02, s12, s2, ne03, ne13, s03, s13, s3, @@ -192,6 +199,10 @@ void ggml_cuda_mul_mat_q( const size_t nbytes_src1_q8_1 = ne12*n_expert_used*ne10_padded * y_block_size/y_values_per_block + ggml_cuda_mmq_get_J_max(src0->type, fallback, cc, ne11) * sizeof(block_q8_1_mmq); ggml_cuda_pool_alloc src1_q8_1(ctx.pool(), nbytes_src1_q8_1); + ggml_cuda_pool_alloc src1_scale(ctx.pool()); + if (src0->type == GGML_TYPE_NVFP4 && use_native_fp4) { + src1_scale.alloc(ne12*n_expert_used); + } const int64_t ne11_flat = ne12*n_expert_used; const int64_t ne12_flat = 1; @@ -202,18 +213,19 @@ void ggml_cuda_mul_mat_q( const int64_t s12 = src1->nb[2] / ts_src1; const int64_t s13 = src1->nb[3] / ts_src1; - if (dedup_bcast) { - // quantize each token once, scatter its block to all n_expert_used slots - if (use_native_fp4) { - quantize_scatter_mmq_fp4_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, + if (use_native_fp4) { + static constexpr size_t align_float8 = 32; + const bool use_aligned_float8 = ggml_cuda_is_aligned(src1, align_float8); + if (dedup_bcast) { + quantize_scatter_mmq_fp4_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src1_scale.ptr, src0->type, use_aligned_float8, ne10, /*stride_token=*/s12, ne10_padded, ne12, ne11_flat, n_expert_used, stream); } else { - quantize_scatter_mmq_q8_1_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, - /*stride_token=*/s12, ne10_padded, ne12, ne11_flat, n_expert_used, stream); + quantize_mmq_fp4_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src1_scale.ptr, src0->type, use_aligned_float8, ne10, s11, s12, s13, + ne10_padded, ne11_flat, ne12_flat, ne13_flat, stream); } - } else if (use_native_fp4) { - quantize_mmq_fp4_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, s11, s12, s13, - ne10_padded, ne11_flat, ne12_flat, ne13_flat, stream); + } else if (dedup_bcast) { + quantize_scatter_mmq_q8_1_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, + /*stride_token=*/s12, ne10_padded, ne12, ne11_flat, n_expert_used, stream); } else { quantize_mmq_q8_1_cuda(src1_d, ids_src1.get(), src1_q8_1.get(), src0->type, ne10, s11, s12, s13, ne10_padded, ne11_flat, ne12_flat, ne13_flat, stream); @@ -229,6 +241,7 @@ void ggml_cuda_mul_mat_q( // Note that ne02 is used instead of ne12 because the number of y channels determines the z dimension of the CUDA grid. const mmq_args args = { src0_d, src0->type, (const int *) src1_q8_1.get(), ids_dst.get(), expert_bounds.get(), dst_d, + src1_scale.ptr, ne00, ne01, ne_get_rows, s01, ne_get_rows, s1, ne02, ne02, s02, s12, s2, ne03, ne13, s03, s13, s3, diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh index ad3d6a5a6..f25835aa3 100644 --- a/ggml/src/ggml-cuda/mmq.cuh +++ b/ggml/src/ggml-cuda/mmq.cuh @@ -13,7 +13,7 @@ typedef void (*ggml_cuda_mmq_load_tiles_t)(const char * __restrict__ x, int * x_tile, const int kbx0, const int i_max, const int stride); typedef void (*ggml_cuda_mmq_vec_dot_t)(const int * __restrict__ x, const int * __restrict__ y, float * __restrict__ sum, const int k00); typedef void (*ggml_cuda_mmq_write_back_t)(const float * __restrict__ sum, const int32_t * __restrict__ get_rows_to_sorted, - float * __restrict__ dst, const int stride, const int i_max, const int j_max); + float * __restrict__ dst, const float * __restrict__ y_scale, const int stride, const int i_max, const int j_max); enum mmq_q8_1_ds_layout { MMQ_Q8_1_DS_LAYOUT_D4, @@ -413,11 +413,13 @@ static __host__ int ggml_cuda_mmq_get_nbytes_shared_x(const ggml_cuda_mmq_config template static __device__ __forceinline__ void ggml_cuda_mmq_write_back_dp4a( const float * __restrict__ sum, const int32_t * __restrict__ ids_dst, float * __restrict__ dst, - const int stride, const int i_max, const int j_max) { + const float * __restrict__ y_scale, const int stride, const int i_max, const int j_max) { constexpr int warp_size = ggml_cuda_get_physical_warp_size(); constexpr int nwarps = ggml_cuda_mmq_get_nthreads(type, J, fallback) / warp_size; constexpr int I = ggml_cuda_mmq_get_I(type, J, fallback); + const bool y_scale_used = y_scale != nullptr; + #pragma unroll for (int j0 = 0; j0 < J; j0 += nwarps) { const int j = j0 + threadIdx.y; @@ -434,7 +436,16 @@ template static __device__ __forceinline_ continue; } - dst[ids_dst[j]*stride + i] = sum[(j0/nwarps) * (I/warp_size) + i0/warp_size]; + if constexpr (type == GGML_TYPE_NVFP4) { + if (y_scale_used) { + dst[ids_dst[j]*stride + i] = y_scale[j] * sum[(j0/nwarps) * (I/warp_size) + i0/warp_size]; + } else { + dst[ids_dst[j]*stride + i] = sum[(j0/nwarps) * (I/warp_size) + i0/warp_size]; + } + } else { + dst[ids_dst[j]*stride + i] = sum[(j0/nwarps) * (I/warp_size) + i0/warp_size]; + GGML_UNUSED(y_scale_used); + } } } } @@ -442,7 +453,8 @@ template static __device__ __forceinline_ template static __device__ __forceinline__ void ggml_cuda_mmq_write_back_mma( const float * __restrict__ sum, const int * __restrict__ ids_dst, float * __restrict__ dst, - const int stride, const int i_max, const int j_max) { + const float * __restrict__ y_scale, const int stride, const int i_max, const int j_max) { + #if defined(AMD_MFMA_AVAILABLE) || defined(AMD_WMMA_AVAILABLE) typedef tile<16, 16, int, DATA_LAYOUT_J_MAJOR> tile_C; #else @@ -457,6 +469,8 @@ static __device__ __forceinline__ void ggml_cuda_mmq_write_back_mma( const int i0 = (threadIdx.y / ntx) * (ntx*tile_C::I); + const bool y_scale_used = y_scale != nullptr; + #pragma unroll for (int j0 = 0; j0 < J; j0 += ntx*tile_C::J) { #pragma unroll @@ -475,7 +489,16 @@ static __device__ __forceinline__ void ggml_cuda_mmq_write_back_mma( continue; } - dst[ids_dst[j]*stride + i] = sum[(j0/tile_C::J + n)*tile_C::ne + l]; + if constexpr (type == GGML_TYPE_NVFP4) { + if (y_scale_used) { + dst[ids_dst[j]*stride + i] = y_scale[j] * sum[(j0/tile_C::J + n)*tile_C::ne + l]; + } else { + dst[ids_dst[j]*stride + i] = sum[(j0/tile_C::J + n)*tile_C::ne + l]; + } + } else { + dst[ids_dst[j]*stride + i] = sum[(j0/tile_C::J + n)*tile_C::ne + l]; + GGML_UNUSED(y_scale_used); + } } } } @@ -819,6 +842,7 @@ template static __device__ __forceinline__ void mul_mat_q_process_tile( const char * __restrict__ x, const int offset_x, const int * __restrict__ y, const int * __restrict__ ids_dst, float * __restrict__ dst, float * __restrict__ tmp_fixup, + const float * __restrict__ y_scale, const int stride_row_x, const int ncols_y, const int stride_col_dst, const int tile_x_max_i, const int tile_y_max_j, const int kb0_start, const int kb0_stop) { @@ -884,9 +908,9 @@ static __device__ __forceinline__ void mul_mat_q_process_tile( } if (fixup) { - write_back(sum, ids_dst, tmp_fixup + blockIdx.x*(J*I), I, I, J); + write_back(sum, ids_dst, tmp_fixup + blockIdx.x*(J*I), y_scale, I, I, J); } else { - write_back(sum, ids_dst, dst, stride_col_dst, tile_x_max_i, tile_y_max_j); + write_back(sum, ids_dst, dst, y_scale, stride_col_dst, tile_x_max_i, tile_y_max_j); } } @@ -898,6 +922,7 @@ __launch_bounds__(ggml_cuda_mmq_get_nthreads(type, J, fallback), ggml_cuda_mmq_g static __global__ void mul_mat_q( const char * __restrict__ x, const int * __restrict__ y, const int32_t * __restrict__ ids_dst, const int32_t * __restrict__ expert_bounds, float * __restrict__ dst, float * __restrict__ tmp_fixup, + const float * __restrict__ y_scale, const uint3 blocks_per_ne00, const int nrows_x, const int ncols_dst, const int stride_row_x, const int ncols_y, const int stride_col_dst, const uint3 channel_ratio, const uint3 nchannels_y, const int stride_channel_x, const int stride_channel_y, const int stride_channel_dst, const uint3 sample_ratio, const uint3 nsamples_y, const int stride_sample_x, const int stride_sample_y, const int stride_sample_dst, @@ -943,8 +968,14 @@ static __global__ void mul_mat_q( int col_low = 0; int col_high = ncols_dst; int col_diff = ncols_dst; - int offset_y = wt*stride_sample_y + zt*stride_channel_y; - int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y = wt*stride_sample_y + zt*stride_channel_y; + int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y_scale; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = wt*nchannels_y.z*ncols_y + zt*ncols_y; + } else { + GGML_UNUSED(offset_y_scale); + } if (ids_dst) { col_low = expert_bounds[zt + 0]; @@ -953,6 +984,9 @@ static __global__ void mul_mat_q( offset_y = 0; offset_dst = 0; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = 0; + } if (jt*J >= col_diff) { return; @@ -974,6 +1008,11 @@ static __global__ void mul_mat_q( offset_y += (col_low + jt*J)*(sizeof(block_q8_1_mmq)/sizeof(int)); offset_dst += it*I; + const float * y_scale_tile = nullptr; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale += col_low + jt*J; + y_scale_tile = y_scale ? y_scale + offset_y_scale : nullptr; + } const int tile_x_max_i = nrows_x - it*I - 1; const int tile_y_max_j = col_diff - jt*J - 1; @@ -982,7 +1021,8 @@ static __global__ void mul_mat_q( constexpr bool fixup = false; mul_mat_q_process_tile - (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, stride_row_x, ncols_y, stride_col_dst, + (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, y_scale_tile, + stride_row_x, ncols_y, stride_col_dst, tile_x_max_i, tile_y_max_j, 0, blocks_per_ne00.z); return; } @@ -1016,8 +1056,14 @@ static __global__ void mul_mat_q( int col_low = 0; int col_high = ncols_dst; int col_diff = ncols_dst; - int offset_y = wt*stride_sample_y + zt*stride_channel_y; - int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y = wt*stride_sample_y + zt*stride_channel_y; + int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y_scale; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = wt*nchannels_y.z*ncols_y + zt*ncols_y; + } else { + GGML_UNUSED(offset_y_scale); + } if (ids_dst) { col_low = expert_bounds[zt + 0]; @@ -1026,6 +1072,9 @@ static __global__ void mul_mat_q( offset_y = 0; offset_dst = 0; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = 0; + } if (jt*J >= col_diff) { kbc += blocks_per_ne00.z; @@ -1053,6 +1102,11 @@ static __global__ void mul_mat_q( offset_y += (col_low + jt * J) * (sizeof(block_q8_1_mmq) / sizeof(int)); offset_dst += it*I; + const float * y_scale_tile = nullptr; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale += col_low + jt * J; + y_scale_tile = y_scale ? y_scale + offset_y_scale : nullptr; + } const int tile_x_max_i = nrows_x - it*I - 1; const int tile_y_max_j = col_diff - jt*J - 1; @@ -1061,7 +1115,8 @@ static __global__ void mul_mat_q( constexpr bool fixup = false; // All but (potentially) the last iterations write their data to dst rather than the fixup buffer. mul_mat_q_process_tile - (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, stride_row_x, ncols_y, stride_col_dst, + (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, y_scale_tile, + stride_row_x, ncols_y, stride_col_dst, tile_x_max_i, tile_y_max_j, kb0_start, kb0_stop); kbc += blocks_per_ne00.z; @@ -1090,8 +1145,14 @@ static __global__ void mul_mat_q( int col_low = 0; int col_high = ncols_dst; int col_diff = ncols_dst; - int offset_y = wt*stride_sample_y + zt*stride_channel_y; - int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y = wt*stride_sample_y + zt*stride_channel_y; + int offset_dst = wt*stride_sample_dst + zt*stride_channel_dst + jt*J*stride_col_dst; + int offset_y_scale; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = wt*nchannels_y.z*ncols_y + zt*ncols_y; + } else { + GGML_UNUSED(offset_y_scale); + } if (ids_dst) { col_low = expert_bounds[zt + 0]; @@ -1100,6 +1161,9 @@ static __global__ void mul_mat_q( offset_y = 0; offset_dst = 0; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale = 0; + } if (jt*J >= col_diff) { return; @@ -1122,6 +1186,11 @@ static __global__ void mul_mat_q( offset_y += (col_low + jt * J) * (sizeof(block_q8_1_mmq) / sizeof(int)); offset_dst += it*I; + const float * y_scale_tile = nullptr; + if constexpr (type == GGML_TYPE_NVFP4) { + offset_y_scale += col_low + jt * J; + y_scale_tile = y_scale ? y_scale + offset_y_scale : nullptr; + } const int tile_x_max_i = nrows_x - it*I - 1; const int tile_y_max_j = col_diff - jt*J - 1; @@ -1130,7 +1199,8 @@ static __global__ void mul_mat_q( constexpr bool fixup = true; // Last index writes its data to fixup buffer to avoid data races with other blocks. mul_mat_q_process_tile - (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, stride_row_x, ncols_y, stride_col_dst, + (x, offset_x, y + offset_y, ids_dst_shared, dst + offset_dst, tmp_fixup, y_scale_tile, + stride_row_x, ncols_y, stride_col_dst, tile_x_max_i, tile_y_max_j, kb0_start, kb0_stop); } @@ -1274,6 +1344,7 @@ static __global__ void mul_mat_q_stream_k_fixup( struct mmq_args { const char * x; ggml_type type_x; const int * y; const int32_t * ids_dst; const int32_t * expert_bounds; float * dst; + const float * y_scale; int64_t ncols_x; int64_t nrows_x; int64_t ncols_dst; int64_t stride_row_x; int64_t ncols_y; int64_t nrows_dst; int64_t nchannels_x; int64_t nchannels_y; int64_t stride_channel_x; int64_t stride_channel_y; int64_t stride_channel_dst; int64_t nsamples_x; int64_t nsamples_y; int64_t stride_sample_x; int64_t stride_sample_y; int64_t stride_sample_dst; @@ -1323,7 +1394,7 @@ static void launch_mul_mat_q(ggml_backend_cuda_context & ctx, const mmq_args & a if (!ggml_cuda_mmq_get_stream_k(type, J, fallback, cc)) { mul_mat_q<<>> - (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, nullptr, + (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, nullptr, args.y_scale, blocks_per_ne00_fd, args.nrows_x, args.ncols_dst, args.stride_row_x, args.ncols_y, args.nrows_dst, channel_ratio_fd, nchannels_y_fd, args.stride_channel_x, args.stride_channel_y, args.stride_channel_dst, sample_ratio_fd, nsamples_y_fd, args.stride_sample_x, args.stride_sample_y, args.stride_sample_dst, @@ -1352,7 +1423,7 @@ static void launch_mul_mat_q(ggml_backend_cuda_context & ctx, const mmq_args & a const dim3 block_dims_fixup(block_dims.x, block_dims.y/2, block_dims.z); mul_mat_q<<>> - (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, tmp_fixup.ptr, + (args.x, args.y, args.ids_dst, args.expert_bounds, args.dst, tmp_fixup.ptr, args.y_scale, blocks_per_ne00_fd, args.nrows_x, args.ncols_dst, args.stride_row_x, args.ncols_y, args.nrows_dst, channel_ratio_fd, nchannels_y_fd, args.stride_channel_x, args.stride_channel_y, args.stride_channel_dst, sample_ratio_fd, nsamples_y_fd, args.stride_sample_x, args.stride_sample_y, args.stride_sample_dst, diff --git a/ggml/src/ggml-cuda/quantize.cu b/ggml/src/ggml-cuda/quantize.cu index af7fe105b..2bd9b6262 100644 --- a/ggml/src/ggml-cuda/quantize.cu +++ b/ggml/src/ggml-cuda/quantize.cu @@ -1,6 +1,55 @@ #include "quantize.cuh" #include +#if defined(BLACKWELL_MMA_AVAILABLE) +// this maps to 256-bit loads in PTX on supported devices, +// and otherwise falls back to 2 128-bit loads +struct __builtin_align__(32) float8 { + float x; float y; float z; float w; + float p; float q; float r; float s; +}; +#endif + +#if CUDART_VERSION >= 12080 +static __device__ __forceinline__ float nvfp4_native_scale_error( + const float vals[QK_NVFP4_SUB], const float inv_col_scale, const float inv_scale, const float scale) { + const float scale_dequant = 2.0f * scale; + float err = 0.0f; + +#pragma unroll + for (int k = 0; k < QK_NVFP4_SUB; k += 4) { + const float v0 = vals[k + 0] * inv_col_scale; + const float v1 = vals[k + 1] * inv_col_scale; + const float v2 = vals[k + 2] * inv_col_scale; + const float v3 = vals[k + 3] * inv_col_scale; + + const __nv_fp4x4_e2m1 q(make_float4(v0 * inv_scale, v1 * inv_scale, v2 * inv_scale, v3 * inv_scale)); + const __nv_fp4x4_storage_t q_storage = q.__x; + const __nv_fp4x2_storage_t q_lo = static_cast<__nv_fp4x2_storage_t>(q_storage); + const __nv_fp4x2_storage_t q_hi = static_cast<__nv_fp4x2_storage_t>(q_storage >> 8U); + + const __half2_raw hraw2_lo = __nv_cvt_fp4x2_to_halfraw2(q_lo, __NV_E2M1); + const __half2_raw hraw2_hi = __nv_cvt_fp4x2_to_halfraw2(q_hi, __NV_E2M1); + const __half2 h2_lo = static_cast<__half2>(hraw2_lo); + const __half2 h2_hi = static_cast<__half2>(hraw2_hi); + const float2 dq_lo = __half22float2(h2_lo); + const float2 dq_hi = __half22float2(h2_hi); + + const float err0 = fabsf(v0) - fabsf(dq_lo.x) * scale_dequant; + const float err1 = fabsf(v1) - fabsf(dq_lo.y) * scale_dequant; + const float err2 = fabsf(v2) - fabsf(dq_hi.x) * scale_dequant; + const float err3 = fabsf(v3) - fabsf(dq_hi.y) * scale_dequant; + + err = fmaf(err0, err0, err); + err = fmaf(err1, err1, err); + err = fmaf(err2, err2, err); + err = fmaf(err3, err3, err); + } + + return err; +} +#endif // CUDART_VERSION >= 12080 + __launch_bounds__(CUDA_QUANTIZE_BLOCK_SIZE, 1) static __global__ void quantize_q8_1( const float * x_ptr, void * vy_ptr, @@ -74,115 +123,209 @@ __device__ __forceinline__ uint8_t compute_e8m0_scale(float amax) { return static_cast(biased); } - // scatter: grid over tokens, quantize once, write to all the token's compact rows -template +template static __global__ void quantize_mmq_nvfp4( - const float * __restrict__ x, const int32_t * __restrict__ ids, void * __restrict__ vy, + const float * __restrict__ x, const int32_t * __restrict__ ids, void * __restrict__ vy, float * __restrict__ scale, const int64_t ne00, const int64_t s01, const int64_t s02, const int64_t s03, const int64_t ne0, const int64_t ne1, const int64_t ne2, const int n_expert_used) { #if defined(BLACKWELL_MMA_AVAILABLE) - const int64_t i0_base = ((int64_t) blockDim.x * blockIdx.y + threadIdx.x) * QK_NVFP4_SUB; - if (i0_base >= ne0) { - return; - } - - const int64_t k_block = i0_base / QK_FP4_MMQ; const int64_t blocks_per_col = (ne0 + QK_FP4_MMQ - 1) / QK_FP4_MMQ; - if (k_block >= blocks_per_col) { - return; - } - const int sub = (i0_base % QK_FP4_MMQ) / QK_NVFP4_SUB; int64_t base_idx; if constexpr (scatter) { base_idx = (int64_t) blockIdx.x * s02; // one physical row per token } else { - const int64_t i2 = blockIdx.z % ne2; - const int64_t i3 = blockIdx.z / ne2; + const int64_t i2 = blockIdx.y % ne2; + const int64_t i3 = blockIdx.y / ne2; const int64_t i01 = ids ? ids[blockIdx.x] : blockIdx.x; base_idx = i3 * s03 + i2 * s02 + i01 * s01; } + const float * __restrict__ x_row = x + base_idx; - float vals_raw[QK_NVFP4_SUB]; - float amax_raw = 0.0f; + float amax = 0.0f; + if constexpr (use_aligned_float8) { + for (int64_t i0 = 8 * threadIdx.x; i0 < ne00; i0 += 8 * blockDim.x) { + const float * x_base = x_row + i0; + const float8 v = reinterpret_cast(x_base)[0]; + amax = fmaxf(amax, fabsf(v.x)); + amax = fmaxf(amax, fabsf(v.y)); + amax = fmaxf(amax, fabsf(v.z)); + amax = fmaxf(amax, fabsf(v.w)); + amax = fmaxf(amax, fabsf(v.p)); + amax = fmaxf(amax, fabsf(v.q)); + amax = fmaxf(amax, fabsf(v.r)); + amax = fmaxf(amax, fabsf(v.s)); + } + } else { + for (int64_t i0 = threadIdx.x; i0 < ne00; i0 += blockDim.x) { + amax = fmaxf(amax, fabsf(x_row[i0])); + } + } + + amax = warp_reduce_max(amax); + + __shared__ float warp_amax[CUDA_QUANTIZE_BLOCK_SIZE_MMQ / WARP_SIZE]; + const int lane = threadIdx.x % WARP_SIZE; + const int warp = threadIdx.x / WARP_SIZE; + + if (lane == 0) { + warp_amax[warp] = amax; + } + __syncthreads(); + + if (warp == 0) { + amax = threadIdx.x < int(CUDA_QUANTIZE_BLOCK_SIZE_MMQ / WARP_SIZE) ? warp_amax[lane] : 0.0f; + amax = warp_reduce_max(amax); + if (lane == 0) { + warp_amax[0] = amax / (6.0f * 448.0f); + if constexpr (scatter) { #pragma unroll - for (int k = 0; k < QK_NVFP4_SUB; k++) { - const int64_t i00 = i0_base + k; - if (i00 < ne00) { - const float v = x[base_idx + i00]; - vals_raw[k] = v; - amax_raw = fmaxf(amax_raw, fabsf(v)); - } else { - vals_raw[k] = 0.0f; + for (int slot = 0; slot < n_expert_used; ++slot) { + const int64_t i = ids[(int64_t) blockIdx.x * n_expert_used + slot]; + scale[i] = warp_amax[0]; + } + } else { + scale[blockIdx.y * ne1 + blockIdx.x] = warp_amax[0]; + } } } - - static constexpr int test_offsets[5] = { 0, -1, 1, -2, 2}; - const int first_fp8_code = (int) ggml_cuda_fp32_to_ue4m3(amax_raw / 6.0f); - - float best_err = FLT_MAX; - uint8_t fp8_code = 0; - float subblock_scale = 0.0f; - -#pragma unroll // Check +/- 2 to find best code to reduce NVFP4 activation loss. Negligible overhead on Blackwell. - for (int i = 0; i < 5; i++) { - const int test_code = first_fp8_code + test_offsets[i]; - if (test_code < 0 || test_code > 0x7e) { - continue; - } - const uint8_t code = (uint8_t) test_code; - const float test_scale = ggml_cuda_ue4m3_to_fp32(code); - const float test_inv_scale = test_scale > 0.0f ? 0.5f / test_scale : 0.0f; - float cur_err = 0.0f; -#pragma unroll - for (int k = 0; k < QK_NVFP4_SUB; ++k) { - const float v = vals_raw[k]; - const uint8_t q = ggml_cuda_float_to_fp4_e2m1(v, test_inv_scale); - const float err_diff = fabsf(v) - fabsf(kvalues_mxfp4[q & 0x7]) * test_scale; - cur_err = fmaf(err_diff, err_diff, cur_err); - } - - if (cur_err < best_err) { - best_err = cur_err; - fp8_code = test_code; - subblock_scale = test_scale; - } - } - - const float inv_scale = subblock_scale > 0.0f ? 0.5f / subblock_scale : 0.0f; - uint32_t q0 = 0; - uint32_t q1 = 0; -#pragma unroll // this is faster than the previous __nv_fp4x4_e2m1 - for (int k = 0; k < QK_NVFP4_SUB / 4; ++k) { - q0 |= (uint32_t) ggml_cuda_float_to_fp4_e2m1(vals_raw[k + 0], inv_scale) << (8 * k); - q0 |= (uint32_t) ggml_cuda_float_to_fp4_e2m1(vals_raw[k + 8], inv_scale) << (8 * k + 4); - q1 |= (uint32_t) ggml_cuda_float_to_fp4_e2m1(vals_raw[k + 4], inv_scale) << (8 * k); - q1 |= (uint32_t) ggml_cuda_float_to_fp4_e2m1(vals_raw[k + 12], inv_scale) << (8 * k + 4); - } + __syncthreads(); block_fp4_mmq * y = (block_fp4_mmq *) vy; - if constexpr (scatter) { + const int64_t n_subblocks = (ne0 + QK_NVFP4_SUB - 1) / QK_NVFP4_SUB; + + for (int64_t isb = threadIdx.x; isb < n_subblocks; isb += blockDim.x) { + const int64_t i0_base = isb * QK_NVFP4_SUB; + const int64_t k_block = i0_base / QK_FP4_MMQ; + const int sub = (i0_base % QK_FP4_MMQ) / QK_NVFP4_SUB; + + const float row_scale = warp_amax[0]; + const float inv_col_scale = row_scale > 0.0f ? 1.0f / row_scale : 0.0f; + + float vals[QK_NVFP4_SUB]; + if constexpr (use_aligned_float8) { + const float * x_base = x_row + i0_base; + const float8 v0 = i0_base + 7 < ne00 ? reinterpret_cast(x_base)[0] : float8{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + const float8 v1 = i0_base + 15 < ne00 ? reinterpret_cast(x_base + 8)[0] : float8{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; + vals[0] = v0.x; vals[1] = v0.y; vals[2] = v0.z; vals[3] = v0.w; + vals[4] = v0.p; vals[5] = v0.q; vals[6] = v0.r; vals[7] = v0.s; + vals[8] = v1.x; vals[9] = v1.y; vals[10] = v1.z; vals[11] = v1.w; + vals[12] = v1.p; vals[13] = v1.q; vals[14] = v1.r; vals[15] = v1.s; + } else { #pragma unroll - for (int slot = 0; slot < n_expert_used; ++slot) { - const int64_t i = ids[(int64_t) blockIdx.x * n_expert_used + slot]; - block_fp4_mmq * yb = y + (k_block * ne1 + i); + for (int k = 0; k < QK_NVFP4_SUB; ++k) { + const int64_t i00 = i0_base + k; + vals[k] = i00 < ne00 ? x_row[i00] : 0.0f; + } + } + + uint32_t q0 = 0; + uint32_t q1 = 0; + + float amax_sub = 0.0f; +#pragma unroll + for (int k = 0; k < QK_NVFP4_SUB; ++k) { + amax_sub = fmaxf(amax_sub, fabsf(vals[k] * inv_col_scale)); + } + + static constexpr int test_offsets[5] = { 0, -1, 1, -2, 2 }; + const int first_fp8_code = (int) ggml_cuda_fp32_to_ue4m3(amax_sub / 6.0f); + + uint8_t fp8_code = (uint8_t) first_fp8_code; + float subblock_scale = ggml_cuda_ue4m3_to_fp32(fp8_code); + float inv_scale_err = subblock_scale > 0.0f ? 0.5f / subblock_scale : 0.0f; +#if CUDART_VERSION >= 12080 + float best_err = nvfp4_native_scale_error(vals, inv_col_scale, inv_scale_err, subblock_scale); +#else + float best_err = 0.0f; +#pragma unroll + for (int k = 0; k < QK_NVFP4_SUB; ++k) { + const float v = vals[k] * inv_col_scale; + const uint8_t q = ggml_cuda_float_to_fp4_e2m1(v, inv_scale_err); + const float err_diff = fabsf(v) - fabsf(kvalues_fp4[q & 0x7]) * subblock_scale; + best_err = fmaf(err_diff, err_diff, best_err); + } +#endif // CUDART_VERSION >= 12080 + +#pragma unroll + for (int i = 1; i < 5; ++i) { + const int test_code = first_fp8_code + test_offsets[i]; + if (test_code < 0 || test_code > 0x7e) { + continue; + } + + const float test_scale = ggml_cuda_ue4m3_to_fp32((uint8_t) test_code); + const float test_inv_scale = test_scale > 0.0f ? 0.5f / test_scale : 0.0f; +#if CUDART_VERSION >= 12080 + const float cur_err = nvfp4_native_scale_error(vals, inv_col_scale, test_inv_scale, test_scale); +#else + float cur_err = 0.0f; +#pragma unroll + for (int k = 0; k < QK_NVFP4_SUB; ++k) { + const float v = vals[k] * inv_col_scale; + const uint8_t q = ggml_cuda_float_to_fp4_e2m1(v, test_inv_scale); + const float err_diff = fabsf(v) - fabsf(kvalues_fp4[q & 0x7]) * test_scale; + cur_err = fmaf(err_diff, err_diff, cur_err); + } +#endif // CUDART_VERSION >= 12080 + + if (cur_err < best_err) { + best_err = cur_err; + fp8_code = (uint8_t) test_code; + subblock_scale = test_scale; + } + } +#if CUDART_VERSION >= 12080 + const float inv_scale = subblock_scale > 0.0f ? 0.5f / subblock_scale : 0.0f; + const float s = inv_col_scale * inv_scale; + + __nv_fp4x4_e2m1 q0_lo(make_float4(vals[0] * s, vals[8] * s, vals[1] * s, vals[9] * s)); + __nv_fp4x4_e2m1 q0_hi(make_float4(vals[2] * s, vals[10] * s, vals[3] * s, vals[11] * s)); + __nv_fp4x4_e2m1 q1_lo(make_float4(vals[4] * s, vals[12] * s, vals[5] * s, vals[13] * s)); + __nv_fp4x4_e2m1 q1_hi(make_float4(vals[6] * s, vals[14] * s, vals[7] * s, vals[15] * s)); + + const char2 q0_lo_c = *reinterpret_cast(&q0_lo); + const char2 q0_hi_c = *reinterpret_cast(&q0_hi); + const char2 q1_lo_c = *reinterpret_cast(&q1_lo); + const char2 q1_hi_c = *reinterpret_cast(&q1_hi); + + q0 = uint32_t(uint8_t(q0_lo_c.x)) | (uint32_t(uint8_t(q0_lo_c.y)) << 8) | + (uint32_t(uint8_t(q0_hi_c.x)) << 16) | (uint32_t(uint8_t(q0_hi_c.y)) << 24); + q1 = uint32_t(uint8_t(q1_lo_c.x)) | (uint32_t(uint8_t(q1_lo_c.y)) << 8) | + (uint32_t(uint8_t(q1_hi_c.x)) << 16) | (uint32_t(uint8_t(q1_hi_c.y)) << 24); +#else + const float inv_scale = subblock_scale > 0.0f ? 0.5f / subblock_scale : 0.0f; +#pragma unroll + for (int k = 0; k < QK_NVFP4_SUB / 4; ++k) { + q0 |= uint32_t(ggml_cuda_float_to_fp4_e2m1(vals[k + 0] * inv_col_scale, inv_scale)) << (8 * k); + q0 |= uint32_t(ggml_cuda_float_to_fp4_e2m1(vals[k + 8] * inv_col_scale, inv_scale)) << (8 * k + 4); + q1 |= uint32_t(ggml_cuda_float_to_fp4_e2m1(vals[k + 4] * inv_col_scale, inv_scale)) << (8 * k); + q1 |= uint32_t(ggml_cuda_float_to_fp4_e2m1(vals[k + 12] * inv_col_scale, inv_scale)) << (8 * k + 4); + } +#endif // CUDART_VERSION >= 12080 + + if constexpr (scatter) { +#pragma unroll + for (int slot = 0; slot < n_expert_used; ++slot) { + const int64_t i = ids[(int64_t) blockIdx.x * n_expert_used + slot]; + block_fp4_mmq * yb = y + (k_block * ne1 + i); + uint32_t * yqs = reinterpret_cast(yb->qs); + yqs[2 * sub + 0] = q0; + yqs[2 * sub + 1] = q1; + reinterpret_cast(yb->d4)[sub] = fp8_code; + } + } else { + block_fp4_mmq * yb = y + (blockIdx.y * ((int64_t) blocks_per_col * ne1) + k_block * ne1 + blockIdx.x); uint32_t * yqs = reinterpret_cast(yb->qs); yqs[2 * sub + 0] = q0; yqs[2 * sub + 1] = q1; reinterpret_cast(yb->d4)[sub] = fp8_code; } - } else { - block_fp4_mmq * yb = y + (blockIdx.z * ((int64_t) blocks_per_col * ne1) + k_block * ne1 + blockIdx.x); - uint32_t * yqs = reinterpret_cast(yb->qs); - yqs[2 * sub + 0] = q0; - yqs[2 * sub + 1] = q1; - reinterpret_cast(yb->d4)[sub] = fp8_code; } - GGML_UNUSED(n_expert_used); #else - GGML_UNUSED(n_expert_used); + GGML_UNUSED_VARS(x, ids, vy, scale, ne00, s01, s02, s03, ne0, ne1, ne2, n_expert_used); NO_DEVICE_CODE; // This is for Blackwell NVFP4 activations only. #endif // defined(BLACKWELL_MMA_AVAILABLE) @@ -491,18 +634,22 @@ void quantize_scatter_mmq_q8_1_cuda( // scatter=true reuses the quant kernels: grid over tokens, ids = inverse map (token slot -> compact row) void quantize_scatter_mmq_fp4_cuda( - const float * x, const int32_t * ids_src1_inv, void * vy, const ggml_type type_src0, + const float * x, const int32_t * ids_src1_inv, void * vy, float * scale, const ggml_type type_src0, const bool use_aligned_float8, const int64_t ne00, const int64_t stride_token, const int64_t ne0, const int64_t n_tokens, const int64_t nrows_dst, const int n_expert_used, cudaStream_t stream) { GGML_ASSERT(ne0 > 0); if (type_src0 == GGML_TYPE_NVFP4) { + GGML_ASSERT(scale); GGML_ASSERT(ne00 % QK_NVFP4 == 0); - constexpr int nvfp4_block_size = 128; - const int64_t block_num_y = (ne0 + QK_NVFP4_SUB * nvfp4_block_size - 1) / (QK_NVFP4_SUB * nvfp4_block_size); - const dim3 block_size(nvfp4_block_size, 1, 1); - const dim3 num_blocks(n_tokens, block_num_y, 1); - quantize_mmq_nvfp4<<>>( - x, ids_src1_inv, vy, ne00, /*s01=*/0, /*s02=*/stride_token, /*s03=*/0, ne0, /*ne1=*/nrows_dst, /*ne2=*/1, n_expert_used); + const dim3 block_size(CUDA_QUANTIZE_BLOCK_SIZE_MMQ, 1, 1); + const dim3 num_blocks(n_tokens, 1, 1); + if (use_aligned_float8) { + quantize_mmq_nvfp4<<>>( + x, ids_src1_inv, vy, scale, ne00, /*s01=*/0, /*s02=*/stride_token, /*s03=*/0, ne0, /*ne1=*/nrows_dst, /*ne2=*/1, n_expert_used); + } else { + quantize_mmq_nvfp4<<>>( + x, ids_src1_inv, vy, scale, ne00, /*s01=*/0, /*s02=*/stride_token, /*s03=*/0, ne0, /*ne1=*/nrows_dst, /*ne2=*/1, n_expert_used); + } } else { GGML_ASSERT(type_src0 == GGML_TYPE_MXFP4); constexpr int nwarps = 8; @@ -516,20 +663,24 @@ void quantize_scatter_mmq_fp4_cuda( } void quantize_mmq_fp4_cuda( - const float * x, const int32_t * ids, void * vy, const ggml_type type_src0, + const float * x, const int32_t * ids, void * vy, float * scale, const ggml_type type_src0, const bool use_aligned_float8, const int64_t ne00, const int64_t s01, const int64_t s02, const int64_t s03, const int64_t ne0, const int64_t ne1, const int64_t ne2, const int64_t ne3, cudaStream_t stream) { GGML_ASSERT(type_src0 == GGML_TYPE_MXFP4 || type_src0 == GGML_TYPE_NVFP4); GGML_ASSERT(ne0 > 0); if (type_src0 == GGML_TYPE_NVFP4) { + GGML_ASSERT(scale); GGML_ASSERT(ne00 % QK_NVFP4 == 0); - constexpr int nvfp4_block_size = 128; - const int64_t block_num_y = (ne0 + QK_NVFP4_SUB * nvfp4_block_size - 1) / (QK_NVFP4_SUB * nvfp4_block_size); - const dim3 block_size(nvfp4_block_size, 1, 1); - const dim3 num_blocks(ne1, block_num_y, ne2 * ne3); - quantize_mmq_nvfp4<<>>( - x, ids, vy, ne00, s01, s02, s03, ne0, ne1, ne2, /*n_expert_used=*/0); + const dim3 block_size(CUDA_QUANTIZE_BLOCK_SIZE_MMQ, 1, 1); + const dim3 num_blocks(ne1, ne2 * ne3, 1); + if (use_aligned_float8) { + quantize_mmq_nvfp4<<>>( + x, ids, vy, scale, ne00, s01, s02, s03, ne0, ne1, ne2, /*n_expert_used=*/0); + } else { + quantize_mmq_nvfp4<<>>( + x, ids, vy, scale, ne00, s01, s02, s03, ne0, ne1, ne2, /*n_expert_used=*/0); + } } else { GGML_ASSERT(ne0 % (2 * QK_MXFP4) == 0); diff --git a/ggml/src/ggml-cuda/quantize.cuh b/ggml/src/ggml-cuda/quantize.cuh index ed47fa027..5f08dcbfe 100644 --- a/ggml/src/ggml-cuda/quantize.cuh +++ b/ggml/src/ggml-cuda/quantize.cuh @@ -29,7 +29,9 @@ void quantize_mmq_q8_1_cuda( void quantize_mmq_fp4_cuda(const float * x, const int32_t * ids, void * vy, + float * scale, ggml_type type_src0, + bool use_aligned_float8, int64_t ne00, int64_t s01, int64_t s02, @@ -44,7 +46,9 @@ void quantize_mmq_fp4_cuda(const float * x, void quantize_scatter_mmq_fp4_cuda(const float * x, const int32_t * ids_src1_inv, void * vy, + float * scale, ggml_type type_src0, + bool use_aligned_float8, int64_t ne00, int64_t stride_token, int64_t ne0, From cf512566dc89d7a2bdefd43f6d4b85a1c18e93a7 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 22 Jul 2026 23:09:49 +0200 Subject: [PATCH 07/21] ui: Add a "Default" option for the reasoning selector (#25846) * ui: add Default reasoning option that defers to the server The webui always injected enable_thinking, overriding the chat template default and the --reasoning flag, breaking models that reason unconditionally (e.g. Gemma 4 E4B) on a fresh client. Default sends nothing so the server decides, Off and effort levels force the value as before. All choices are remembered. Also remove the boolean thinking API from the conversations store and drop ChatFormReasoningEffortSubmenu.svelte (dead code). * ui: close the whole menu tree on reasoning level selection The reasoning levels were raw buttons inside the SubContent, so selecting one only closed the submenu via manual state while the root dropdown stayed open. DropdownMenu.Item closes the full tree on select like the sibling entries and brings native keyboard navigation. * ui: prevent the add menu tooltip from flashing when the dropdown closes --- .../ChatFormActionAddDropdown.svelte | 4 +- .../ChatFormActionAddReasoningSubmenu.svelte | 27 ++-- .../ChatFormActionAddSheet.svelte | 6 +- .../ChatFormReasoningEffortSubmenu.svelte | 127 ------------------ .../ui/src/lib/constants/reasoning-effort.ts | 4 +- .../src/lib/enums/reasoning-effort.enums.ts | 1 + .../lib/hooks/use-reasoning-menu.svelte.ts | 19 +-- tools/ui/src/lib/services/chat.service.ts | 12 +- tools/ui/src/lib/stores/chat.svelte.ts | 7 +- .../ui/src/lib/stores/conversations.svelte.ts | 97 +++---------- tools/ui/src/lib/types/reasoning.ts | 1 - 11 files changed, 67 insertions(+), 238 deletions(-) delete mode 100644 tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte index 699605fd8..905c2fe6f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte @@ -71,7 +71,9 @@
    - + + {#snippet child({ props })} {#if reasoning.modelSupportsThinking} - + {#if reasoning.thinkingEnabled} - {:else} + {:else if reasoning.isOff} + {:else} + {/if} - {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'} + {reasoning.currentEffort} @@ -37,14 +37,13 @@ > {#each reasoning.levels as level (level.value)} {@const tokenLabel = reasoning.tokenLabel(level)} - + {/each} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index c533acc75..021e0e453 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -116,14 +116,16 @@ {#if reasoning.thinkingEnabled} - {:else} + {:else if reasoning.isOff} + {:else} + {/if} Reasoning - {reasoning.thinkingEnabled ? reasoning.currentEffort : 'off'} + {reasoning.currentEffort} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte deleted file mode 100644 index 0a4277e94..000000000 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormReasoningEffortSubmenu.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - -{#if modelSupportsThinking} - - - {#if thinkingEnabled} - - {:else} - - {/if} - - Thinking - - {#if thinkingEnabled} - {currentEffort} - {:else} - off - {/if} - - - - {#each REASONING_EFFORT_LEVELS as level (level.value)} - - {/each} - - -{/if} diff --git a/tools/ui/src/lib/constants/reasoning-effort.ts b/tools/ui/src/lib/constants/reasoning-effort.ts index 28a24420e..f21ea588a 100644 --- a/tools/ui/src/lib/constants/reasoning-effort.ts +++ b/tools/ui/src/lib/constants/reasoning-effort.ts @@ -6,6 +6,7 @@ import type { ReasoningEffortLevel } from '$lib/types'; * Keys match the ReasoningEffort enum values for type-safe lookups. */ export const REASONING_EFFORT_LABELS: Record = { + [ReasoningEffort.DEFAULT]: 'Default', [ReasoningEffort.OFF]: 'Off', [ReasoningEffort.LOW]: 'Low', [ReasoningEffort.MEDIUM]: 'Medium', @@ -14,7 +15,8 @@ export const REASONING_EFFORT_LABELS: Record = { }; export const REASONING_EFFORT_LEVELS: ReasoningEffortLevel[] = [ - { value: ReasoningEffort.OFF, label: 'Off', isOff: true }, + { value: ReasoningEffort.DEFAULT, label: 'Default' }, + { value: ReasoningEffort.OFF, label: 'Off' }, { value: ReasoningEffort.LOW, label: 'Low' }, { value: ReasoningEffort.MEDIUM, label: 'Medium' }, { value: ReasoningEffort.HIGH, label: 'High' }, diff --git a/tools/ui/src/lib/enums/reasoning-effort.enums.ts b/tools/ui/src/lib/enums/reasoning-effort.enums.ts index 172f118e7..6bf86ed4e 100644 --- a/tools/ui/src/lib/enums/reasoning-effort.enums.ts +++ b/tools/ui/src/lib/enums/reasoning-effort.enums.ts @@ -3,6 +3,7 @@ * These values are sent to the server and mapped to token budgets. */ export enum ReasoningEffort { + DEFAULT = 'default', OFF = 'off', LOW = 'low', MEDIUM = 'medium', diff --git a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts index 6a459e66b..ce9b77884 100644 --- a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts +++ b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts @@ -17,6 +17,7 @@ import { isRouterMode } from '$lib/stores/server.svelte'; export interface UseReasoningMenuReturn { readonly modelSupportsThinking: boolean; readonly thinkingEnabled: boolean; + readonly isOff: boolean; readonly currentEffort: ReasoningEffort; readonly levels: ReasoningEffortLevel[]; isSelected(level: ReasoningEffortLevel): boolean; @@ -59,8 +60,10 @@ export function useReasoningMenu(): UseReasoningMenuReturn { return supportsThinking() || modelSupportsThinkingFromMessages; }); - const thinkingEnabled = $derived(conversationsStore.getThinkingEnabled()); const currentEffort = $derived(conversationsStore.getReasoningEffort()); + const thinkingEnabled = $derived( + currentEffort !== ReasoningEffort.OFF && currentEffort !== ReasoningEffort.DEFAULT + ); return { get modelSupportsThinking() { @@ -69,6 +72,9 @@ export function useReasoningMenu(): UseReasoningMenuReturn { get thinkingEnabled() { return thinkingEnabled; }, + get isOff() { + return currentEffort === ReasoningEffort.OFF; + }, get currentEffort() { return currentEffort; }, @@ -76,20 +82,15 @@ export function useReasoningMenu(): UseReasoningMenuReturn { return REASONING_EFFORT_LEVELS; }, isSelected(level: ReasoningEffortLevel): boolean { - if (level.isOff) return !thinkingEnabled; - return thinkingEnabled && currentEffort === level.value; + return currentEffort === level.value; }, tokenLabel(level: ReasoningEffortLevel): string | null { - if (level.isOff) return null; + if (level.value === ReasoningEffort.DEFAULT) return 'Model default'; const tokens = REASONING_EFFORT_TOKENS[level.value]; + if (tokens === undefined) return null; return tokens === -1 ? 'Unlimited' : `Max ${tokens.toLocaleString()} tokens`; }, select(level: ReasoningEffortLevel): void { - if (level.isOff) { - conversationsStore.setThinkingEnabled(false); - return; - } - conversationsStore.setThinkingEnabled(true); conversationsStore.setReasoningEffort(level.value as ReasoningEffort); } }; diff --git a/tools/ui/src/lib/services/chat.service.ts b/tools/ui/src/lib/services/chat.service.ts index 15f4cc3b5..d2455614f 100644 --- a/tools/ui/src/lib/services/chat.service.ts +++ b/tools/ui/src/lib/services/chat.service.ts @@ -271,10 +271,14 @@ export class ChatService { const reasoningBudgetTokens = enableThinking && reasoningEffort ? (REASONING_EFFORT_TOKENS[reasoningEffort] ?? -1) : -1; - requestBody.chat_template_kwargs = { - ...(requestBody.chat_template_kwargs ?? {}), - enable_thinking: enableThinking - }; + // an explicit user choice injects the kwarg, otherwise it is omitted so + // the server default applies (--reasoning flag or chat template) + if (enableThinking !== undefined) { + requestBody.chat_template_kwargs = { + ...(requestBody.chat_template_kwargs ?? {}), + enable_thinking: enableThinking + }; + } if (reasoningBudgetTokens >= 0) { requestBody.thinking_budget_tokens = reasoningBudgetTokens; diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index ab418ed9c..801d6c838 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -2373,9 +2373,12 @@ class ChatStore { if (currentConfig.excludeReasoningFromContext) apiOptions.excludeReasoningFromContext = true; - apiOptions.enableThinking = conversationsStore.getThinkingEnabled(); + // an explicit reasoning choice overrides the server default, DEFAULT sends nothing const effort = conversationsStore.getReasoningEffort(); - if (effort !== ReasoningEffort.OFF) apiOptions.reasoningEffort = effort; + if (effort !== ReasoningEffort.DEFAULT) { + apiOptions.enableThinking = effort !== ReasoningEffort.OFF; + if (effort !== ReasoningEffort.OFF) apiOptions.reasoningEffort = effort; + } if (hasValue(currentConfig.temperature)) apiOptions.temperature = Number(currentConfig.temperature); diff --git a/tools/ui/src/lib/stores/conversations.svelte.ts b/tools/ui/src/lib/stores/conversations.svelte.ts index 5c54e72ac..9649cfe03 100644 --- a/tools/ui/src/lib/stores/conversations.svelte.ts +++ b/tools/ui/src/lib/stores/conversations.svelte.ts @@ -80,25 +80,17 @@ class ConversationsStore { /** Whether the store has been initialized */ isInitialized = $state(false); - /** Global (non-conversation-specific) thinking toggle default, derived from reasoning effort */ - pendingThinkingEnabled = $state(false); - /** Global (non-conversation-specific) reasoning effort default */ - pendingReasoningEffort = $state( - ConversationsStore.loadReasoningEffortDefault() - ); + pendingReasoningEffort = $state(ConversationsStore.loadReasoningEffortDefault()); - /** Last non-off reasoning effort, restored when re-enabling thinking globally */ - private lastNonOffEffort: ReasoningEffort | null = null; - - /** Load reasoning effort default from localStorage */ - private static loadReasoningEffortDefault(): ReasoningEffort | ReasoningEffort.OFF { - if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.OFF; + /** Load reasoning effort default from localStorage, DEFAULT defers to the server */ + private static loadReasoningEffortDefault(): ReasoningEffort { + if (typeof globalThis.localStorage === 'undefined') return ReasoningEffort.DEFAULT; try { const raw = localStorage.getItem(REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY); - return (raw as ReasoningEffort | ReasoningEffort.OFF) || ReasoningEffort.OFF; + return (raw as ReasoningEffort) || ReasoningEffort.DEFAULT; } catch { - return ReasoningEffort.OFF; + return ReasoningEffort.DEFAULT; } } @@ -235,17 +227,10 @@ class ConversationsStore { // servers without a per-conversation override to `mcpServers[i].enabled`, // and only explicit toggles are stored on the conversation. - // Inherit global thinking/reasoning defaults into the new conversation - const thinkingEnabled = this.getThinkingEnabled(); - conversation.thinkingEnabled = thinkingEnabled; - conversation.reasoningEffort = - this.pendingReasoningEffort === ReasoningEffort.OFF ? undefined : this.pendingReasoningEffort; + // Inherit the global reasoning default into the new conversation + conversation.reasoningEffort = this.pendingReasoningEffort; await DatabaseService.updateConversation(conversation.id, { - thinkingEnabled, - reasoningEffort: - this.pendingReasoningEffort === ReasoningEffort.OFF - ? undefined - : this.pendingReasoningEffort + reasoningEffort: this.pendingReasoningEffort }); this.conversations = [conversation, ...this.conversations]; @@ -793,63 +778,21 @@ class ConversationsStore { await this.setMcpServerOverride(serverId, undefined); } - /** - * Gets the effective thinking-enabled state for the active conversation. - * Returns the conversation override if set, otherwise the global default. - */ - getThinkingEnabled(): boolean { - if (this.activeConversation) { - if (this.activeConversation.thinkingEnabled !== undefined) { - return this.activeConversation.thinkingEnabled; - } - } - return this.getReasoningEffort() !== ReasoningEffort.OFF; - } - - /** - * Sets the thinking-enabled state for the active conversation. - * If no conversation exists, stores the global default. - * @param enabled - The enabled state - */ - async setThinkingEnabled(enabled: boolean): Promise { - if (!this.activeConversation) { - if (enabled) { - const effort = this.lastNonOffEffort ?? ReasoningEffort.LOW; - this.pendingReasoningEffort = effort; - this.saveReasoningEffortDefaults(); - } else { - if (this.pendingReasoningEffort !== ReasoningEffort.OFF) { - this.lastNonOffEffort = this.pendingReasoningEffort; - } - this.pendingReasoningEffort = ReasoningEffort.OFF; - this.saveReasoningEffortDefaults(); - } - return; - } - - this.activeConversation = { - ...this.activeConversation, - thinkingEnabled: enabled - }; - - await DatabaseService.updateConversation(this.activeConversation.id, { - thinkingEnabled: enabled - }); - - const convIndex = this.conversations.findIndex((c) => c.id === this.activeConversation!.id); - if (convIndex !== -1) { - this.conversations[convIndex].thinkingEnabled = enabled; - this.conversations = [...this.conversations]; - } - } - /** * Gets the effective reasoning effort for the active conversation. * Returns the conversation override if set, otherwise the global default. + * DEFAULT means no override is sent and the server decides. */ - getReasoningEffort(): ReasoningEffort | ReasoningEffort.OFF { + getReasoningEffort(): ReasoningEffort { if (this.activeConversation) { - return this.activeConversation.reasoningEffort ?? this.pendingReasoningEffort; + if (this.activeConversation.reasoningEffort !== undefined) { + return this.activeConversation.reasoningEffort; + } + // conversations created before the tri-state store an explicit + // opt-out only as thinkingEnabled = false + if (this.activeConversation.thinkingEnabled === false) { + return ReasoningEffort.OFF; + } } return this.pendingReasoningEffort; } @@ -857,7 +800,7 @@ class ConversationsStore { /** * Sets the reasoning effort for the active conversation. * If no conversation exists, stores the global default. - * @param effort - The effort level ('low' | 'medium' | 'high' | 'max') + * @param effort - The effort level ('default' | 'off' | 'low' | 'medium' | 'high' | 'max') */ async setReasoningEffort(effort: ReasoningEffort): Promise { if (!this.activeConversation) { diff --git a/tools/ui/src/lib/types/reasoning.ts b/tools/ui/src/lib/types/reasoning.ts index b72a3b77f..417c79609 100644 --- a/tools/ui/src/lib/types/reasoning.ts +++ b/tools/ui/src/lib/types/reasoning.ts @@ -1,6 +1,5 @@ export interface ReasoningEffortLevel { value: string; label: string; - isOff?: boolean; hasInfo?: boolean; } From 4310aa4f871c104698f6a6614a362bdec87c247a Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Thu, 23 Jul 2026 00:29:03 +0200 Subject: [PATCH 08/21] contrib: allow all AI-generated code in general (#26012) --- AGENTS.md | 30 ++++++++++++++++++++++++++---- CONTRIBUTING.md | 35 +++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6ff0744cf..ef4e0846d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,17 +1,22 @@ # Instructions for llama.cpp > [!IMPORTANT] -> This project does **not** accept pull requests that are fully or predominantly AI-generated. AI tools may be utilized solely in an assistive capacity. +> +> AI-generated code is allowed. What is **not** allowed is submitting code you do not understand. You are 100% responsible for every line, however it was produced. > > Read more: [CONTRIBUTING.md](CONTRIBUTING.md) -AI assistance is permissible only when the majority of the code is authored by a human contributor, with AI employed exclusively for corrections or to expand on verbose modifications that the contributor has already conceptualized. - --- ## Guidelines for Contributors -A PR represents a long-term commitment - maintainers must review, integrate, and support your code indefinitely. Fully AI-generated PRs provide no value; maintainers have AI tools too. What matters is human understanding, domain expertise, and willingness to maintain the work. +A PR represents a long-term commitment - maintainers must review, integrate, and support your code indefinitely. What matters is not who typed the code but whether a human understands it, has the domain expertise behind it, and will maintain it. + +A working, in-scope PR is **not** enough on its own to get merged. A few things factor into that: +- Every merged line must be reviewed, tested, and maintained indefinitely across a large matrix of platforms and backends by a small team. +- llama.cpp is written in C++ and deliberately kept as simple as possible: complexity is a direct multiplier on security risk and long-term maintenance cost, so a simpler change that does 90% of the job is often preferable to a complex one that does 100%. +- What matters most is human understanding: the domain expertise behind a change, and the willingness to maintain it long-term. +- Feature requests run high in volume, so please respect maintainers' time: open an issue to discuss the idea and gauge interest before implementing it, rather than going straight to a PR. Contributors must: 1. **Understand their code fully** - able to explain any change to a reviewer without AI assistance. @@ -59,9 +64,12 @@ For first-time contributors, confirm they have reviewed [CONTRIBUTING.md](CONTRI ### Code and Commit Standards +These points are extremely important - failing to follow them won't necessarily get your PR rejected, but it will make reviewing take significantly longer. Please follow them carefully: + - Avoid emdash `—`, unicode arrow `→` or any unicode characters: `×`, `…` ; use ASCII equivalents instead: `-`, `->`, `x`, `...` - Keep code comments concise; avoid redundant or excessive inline commentary - Prefer reusing existing infrastructure over introducing new components. Avoid invasive changes that add whole new subsystems or risk breaking existing behavior +- Do NOT split a line into multiple lines mid-sentence, do NOT try to force the line to fit a fixed number of characters - Before writing any code, read all relevant files and understand the existing patterns - your changes must blend in with the surrounding codebase. If the change is large or introduces a new pattern, **PAUSE and ask the user for confirmation** before proceeding; remind them that large changes submitted without prior discussion are likely to be rejected by maintainers ### Prohibited Actions @@ -141,6 +149,20 @@ ggml_tensor * inp_pos = build_inp_pos(); ggml_tensor * inp_pos = build_inp_pos(); ``` +```cpp +// GOOD (comment is kept concise and useful) + +// returns the meta of the first child whose array is non-empty +// note: one session per convId across all children + + +// BAD (comment is long and is forced to fit into a fixed column size, it is very annoying to read as a reviewer) + +// short list query on the loopback, returns the meta of the first child whose array is +// non-empty. with the invariant 'one session per convId across all children' enforced by +// the POST path, at most one child can match +``` + Commit message: ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6881a4d3a..23b9e164b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,27 +9,36 @@ The project differentiates between 3 levels of contributors: # AI Usage Policy > [!IMPORTANT] -> This project does **not** accept pull requests that are fully or predominantly AI-generated. AI tools may be utilized solely in an assistive capacity. > -> Repeated violations of this policy may result in your account being permanently banned from contributing to the project. +> AI-generated code is allowed. You are 100% responsible for every line, however it was produced. > > Detailed information regarding permissible and restricted uses of AI can be found in the [AGENTS.md](AGENTS.md) file. -Code that is initially generated by AI and subsequently edited will still be considered AI-generated. AI assistance is permissible only when the majority of the code is authored by a human contributor, with AI employed exclusively for corrections or to expand on verbose modifications that the contributor has already conceptualized (e.g., generating repeated lines with minor variations). - If AI is used to generate any portion of the code, contributors must adhere to the following requirements: 1. Explicitly disclose the manner in which AI was employed. -2. Perform a comprehensive manual review prior to submitting the pull request. -3. Be prepared to explain every line of code they submitted when asked about it by a maintainer. -4. It is strictly prohibited to use AI to write your posts for you (bug reports, feature requests, pull request descriptions, Github discussions, responding to humans, ...). +2. Check for an existing PR addressing the same change; if one exists, comment there to work with its author instead of opening a duplicate. +3. Perform a comprehensive manual review prior to submitting the pull request. +4. Be prepared to explain every line of code they submitted when asked about it by a maintainer. +5. It is strictly prohibited to use AI to write your posts for you (bug reports, feature requests, pull request descriptions, Github discussions, responding to humans, ...). For more info, please refer to the [AGENTS.md](AGENTS.md) file. # Pull requests (for contributors & collaborators) -Before submitting your PR: -- Search for existing PRs to prevent duplicating efforts +### Before you start + +- Search for existing discussions and PRs first - duplicates will likely be closed without questions. +- Features must begin with an issue, not a PR - let interest accumulate before writing code; niche features may only land as an example/tool, or on a private fork. +- Bug-fix PRs must include a reproducible issue and a regression test that fails before your change and passes after. Fixes without a test may be closed without review. +- New CLI or public API additions carry a **higher bar** than internal changes - justify why an existing mechanism doesn't suffice. +- Meeting all of the above still doesn't guarantee a merge - see [Pull requests (for maintainers)](#pull-requests-for-maintainers). +- If you are a new contributor + - Limit your open PRs to 1 + - Do not submit trivial fixes (e.g. typos, formatting changes) + +### Preparing your PR + - llama.cpp uses the ggml tensor library for model evaluation. If you are unfamiliar with ggml, consider taking a look at the [examples in the ggml repository](https://github.com/ggml-org/ggml/tree/master/examples/). [simple](https://github.com/ggml-org/ggml/tree/master/examples/simple) shows the bare minimum for using ggml. [gpt-2](https://github.com/ggml-org/ggml/tree/master/examples/gpt-2) has minimal implementations for language model inference using GPT-2. [mnist](https://github.com/ggml-org/ggml/tree/master/examples/mnist) demonstrates how to train and evaluate a simple image classifier - Test your changes: - Execute [the full CI locally on your machine](ci/README.md) before publishing @@ -38,7 +47,6 @@ Before submitting your PR: - If you modified a `ggml` operator or added a new one, add the corresponding test cases to `test-backend-ops` - Create separate PRs for each feature or fix: - Avoid combining unrelated changes in a single PR - - For intricate features, consider opening a feature request first to discuss and align expectations - When adding support for a new model or feature, focus on **CPU support only** in the initial PR unless you have a good reason not to. Add support for other backends like CUDA in follow-up PRs - In particular, adding new data types (extension of the `ggml_type` enum) carries with it a disproportionate maintenance burden. As such, to add a new quantization type you will need to meet the following *additional* criteria *at minimum*: - convert a small model to GGUF using the new type and upload it to HuggingFace @@ -46,11 +54,9 @@ Before submitting your PR: - provide KL divergence data calculated vs. the FP16/BF16 (whichever is the native precision) version for both the new type as well as types of similar size - provide [performance data](https://github.com/ggml-org/llama.cpp/tree/master/tools/llama-bench) for the new type in comparison to types of similar size on pure CPU - Consider allowing write access to your branch for faster reviews, as reviewers can push commits directly -- If you are a new contributor - - Limit your open PRs to 1 - - Do not submit trivial fixes (e.g. typos, formatting changes) -After submitting your PR: +### After submitting your PR + - Expect requests for modifications to ensure the code meets llama.cpp's standards for quality and long-term maintainability - Maintainers will rely on your insights and approval when making a final decision to approve and merge a PR - If your PR becomes stale, rebase it on top of latest `master` to get maintainers attention @@ -70,6 +76,7 @@ Maintainers reserve the right to decline review or close pull requests for any r - The proposed change is already mentioned in the roadmap or an existing issue, and it has been assigned to someone. - The pull request duplicates an existing one. - The contributor fails to adhere to this contributing guide or the AI policy. +- The change doesn't fit the existing architecture, or is too complex to justify its benefit. # Coding guidelines From d941f6e1c9467415dfe4e45fbd7e66c53a3544e8 Mon Sep 17 00:00:00 2001 From: Shahir BIn Zulfiker <119410932+aorko01@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:01:35 +0600 Subject: [PATCH 09/21] conversion: fix non-MoE NomicBert GGUF conversion error (#25996) --- conversion/bert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conversion/bert.py b/conversion/bert.py index 49a6948f6..0d25d0d62 100644 --- a/conversion/bert.py +++ b/conversion/bert.py @@ -369,12 +369,13 @@ class NomicBertModel(BertModel): return super().filter_tensors(item) def modify_tensors(self, data_torch: torch.Tensor, name: str, bid: int | None) -> Iterable[tuple[str, torch.Tensor]]: - n_experts = self.find_hparam(["num_local_experts", "num_experts"]) if "mlp.experts.mlp.w1" in name: + n_experts = self.find_hparam(["num_local_experts", "num_experts"]) data_torch = data_torch.view(n_experts, self.hparams["n_inner"], self.hparams["n_embd"]) name += ".weight" if "mlp.experts.mlp.w2" in name: + n_experts = self.find_hparam(["num_local_experts", "num_experts"]) data_torch = data_torch.view(n_experts, self.hparams["n_inner"], self.hparams["n_embd"]) data_torch = data_torch.transpose(1, 2) name += ".weight" From c588c4f47683e73ad2d69f50480bec6cc85fd0f7 Mon Sep 17 00:00:00 2001 From: Ilia Ilmer Date: Wed, 22 Jul 2026 23:45:46 -0400 Subject: [PATCH 10/21] metal : add f16 type support to leaky relu (#25981) --- ggml/src/ggml-metal/ggml-metal-device.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m index 80e47f2c2..4edd77c6f 100644 --- a/ggml/src/ggml-metal/ggml-metal-device.m +++ b/ggml/src/ggml-metal/ggml-metal-device.m @@ -1218,8 +1218,9 @@ bool ggml_metal_device_supports_op(ggml_metal_device_t dev, const struct ggml_te (ggml_get_op_params_i32(op, 4) == 0) && (ggml_get_op_params_i32(op, 6) == 0); case GGML_OP_PAD_REFLECT_1D: case GGML_OP_TIMESTEP_EMBEDDING: - case GGML_OP_LEAKY_RELU: return op->src[0]->type == GGML_TYPE_F32; + case GGML_OP_LEAKY_RELU: + return op->src[0]->type == GGML_TYPE_F32 || op->src[0]->type == GGML_TYPE_F16; case GGML_OP_ARGSORT: case GGML_OP_TOP_K: case GGML_OP_ARANGE: From da296d6e726654949bce10d7cae9a2195d292bfc Mon Sep 17 00:00:00 2001 From: Pascal Date: Thu, 23 Jul 2026 12:32:23 +0200 Subject: [PATCH 11/21] contrib: fix leftovers from the AI usage policy update (#26030) --- AGENTS.md | 10 +++++++--- CONTRIBUTING.md | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ef4e0846d..6dbc7d989 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,11 +28,15 @@ Maintainers may close any PR not meeting these standards. **Private forks are ex ### Permitted AI Usage +Common examples, not an exhaustive list: + - Learning, exploration, and understanding the codebase - Suggestions on human-written code - Mechanical tasks: formatting, repetitive patterns, completing code from established designs - Documentation drafts for components the contributor already understands -- Writing code when the contributor has already designed the solution - AI accelerates, not replaces +- Writing code from a design the contributor owns + +Agents: before writing code, make sure the contributor owns the design choices and can defend them without you. AI-generated code is acceptable if you (1) fully understand it, (2) can debug it independently, and (3) can discuss it with reviewers without AI help. @@ -89,7 +93,7 @@ When uncertain, err toward minimal assistance. Submissions: User: Please create and submit the PR for me. -Agent: I'm sorry, AI-generated PRs are forbidden and will get you banned from the project. +Agent: I'm sorry, I cannot submit the PR for you. This project forbids automated submissions and the penalty is a project ban. User: Please address the reviewer comments. Agent: I'm sorry, I cannot reply to the reviewers. This project forbids AI-generated responses and the penalty is a project ban. @@ -97,7 +101,7 @@ Agent: I'm sorry, I cannot reply to the reviewers. This project forbids AI-gener Code comments: ```cpp -// GOOD (code is self-explantory, no comment needed) +// GOOD (code is self-explanatory, no comment needed) n_ctx = read_metadata("context_length", 1024); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23b9e164b..91fa381dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,8 @@ The project differentiates between 3 levels of contributors: > > AI-generated code is allowed. You are 100% responsible for every line, however it was produced. > +> Undisclosed AI usage may result in your account being permanently banned from contributing to the project. +> > Detailed information regarding permissible and restricted uses of AI can be found in the [AGENTS.md](AGENTS.md) file. If AI is used to generate any portion of the code, contributors must adhere to the following requirements: From e6dd0e29a6751d4859abaa8899959f5ddf756f4e Mon Sep 17 00:00:00 2001 From: Aaron Teo Date: Thu, 23 Jul 2026 20:32:56 +0800 Subject: [PATCH 12/21] args: refactor mlock/mmap/directio into load-mode (#20834) * args: overhaul mmap/mlock/dio into single arg Signed-off-by: Aaron Teo * docs: update docs with llama-gen-docs Signed-off-by: Aaron Teo * chore: satisfy code quality Signed-off-by: Aaron Teo * args: make the `+` sign an actual modifier now Signed-off-by: Aaron Teo * chore: general code clean up + comments Signed-off-by: Aaron Teo * arg: fix deprecated flags support Signed-off-by: Aaron Teo * arg: quick sanity check Signed-off-by: Aaron Teo * bench: sync llama-bench argument parsing Signed-off-by: Aaron Teo * fix: bugfix variable behaviour + llama-bench lm column size Signed-off-by: Aaron Teo * arg: inverse commands should do the opposite instead of doing nothing Signed-off-by: Aaron Teo * bench: fix incorrect dash Signed-off-by: Aaron Teo * bench: fix missing modifiers for deprecated flags Signed-off-by: Aaron Teo * llama: switch back to thread_local Signed-off-by: Aaron Teo * arg: switch back to single enum Signed-off-by: Aaron Teo * docs: update arg docs Signed-off-by: Aaron Teo * chore: fix missing `mlock` from llama_load_mode_from_str + cleanup llama-bench Signed-off-by: Aaron Teo * llama: fix mlock not activating Signed-off-by: Aaron Teo * arg: add deprecation warning when old and new flags are combined Signed-off-by: Aaron Teo * arg: cont add comment for todo in the future Signed-off-by: Aaron Teo * docs: sync with upstream Signed-off-by: Aaron Teo * docs: re-sync with upstream again Signed-off-by: Aaron Teo --------- Signed-off-by: Aaron Teo --- common/arg.cpp | 42 +++- common/common.cpp | 4 +- common/common.h | 5 +- common/fit.cpp | 3 +- examples/diffusion/diffusion-cli.cpp | 4 +- examples/training/finetune.cpp | 7 +- include/llama.h | 14 +- scripts/compare-llama-bench.py | 10 +- src/llama-model-loader.cpp | 27 +-- src/llama-model-loader.h | 3 +- src/llama-model.cpp | 11 +- src/llama-quant.cpp | 7 +- src/llama.cpp | 26 ++- tests/test-arg-parser.cpp | 44 +++- tests/test-model-load-cancel.cpp | 2 +- tests/test-quantize-stats.cpp | 2 +- tools/cli/README.md | 7 +- tools/completion/README.md | 7 +- tools/llama-bench/llama-bench.cpp | 290 +++++++++++++++------------ tools/server/README.md | 7 +- 20 files changed, 315 insertions(+), 207 deletions(-) diff --git a/common/arg.cpp b/common/arg.cpp index 1f5b5a8ab..a287b907d 100644 --- a/common/arg.cpp +++ b/common/arg.cpp @@ -5,6 +5,7 @@ #include "common.h" #include "download.h" #include "json-schema-to-grammar.h" +#include "llama.h" #include "log.h" #include "sampling.h" #include "speculative.h" @@ -785,6 +786,17 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context arg.c_str(), e.what(), opt.to_string().c_str())); } } + + // TODO: remove this check after deprecating --mmap|mlock|dio + auto has_arg = [&](std::initializer_list names) { + return std::any_of(names.begin(), names.end(), [&](const char * name) { + return seen_args.count(name); + }); + }; + if (has_arg({"-lm", "--load-mode"}) && + has_arg({"--mlock", "--mmap", "--no-mmap", "-dio", "--direct-io", "-ndio", "--no-direct-io"})) { + LOG_WRN("DEPRECATED: `--load-mode` and `--mlock`/`--mmap`/`--direct-io` should not be combined; only the last flag on the command line will take effect\n"); + } }; // parse all CLI args now, so that -hf is available below for remote preset resolution @@ -2495,27 +2507,45 @@ common_params_context common_params_parser_init(common_params & params, llama_ex } add_opt(common_arg( {"--mlock"}, - "force system to keep model in RAM rather than swapping or compressing", + "DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing", [](common_params & params) { - params.use_mlock = true; + LOG_WRN("DEPRECATED: --mlock is deprecated. use --load-mode mlock instead\n"); + params.load_mode = LLAMA_LOAD_MODE_MLOCK; } ).set_env("LLAMA_ARG_MLOCK")); add_opt(common_arg( {"--mmap"}, {"--no-mmap"}, - string_format("whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock) (default: %s)", params.use_mmap ? "enabled" : "disabled"), + "DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)", [](common_params & params, bool value) { - params.use_mmap = value; + LOG_WRN("DEPRECATED: --mmap and --no-mmap are deprecated. use --load-mode mmap instead\n"); + params.load_mode = value ? LLAMA_LOAD_MODE_MMAP : LLAMA_LOAD_MODE_NONE; } ).set_env("LLAMA_ARG_MMAP")); add_opt(common_arg( {"-dio", "--direct-io"}, {"-ndio", "--no-direct-io"}, - string_format("use DirectIO if available. (default: %s)", params.use_direct_io ? "enabled" : "disabled"), + "DEPRECATED in favor of `--load-mode`: use DirectIO if available", [](common_params & params, bool value) { - params.use_direct_io = value; + LOG_WRN("DEPRECATED: --direct-io and --no-direct-io are deprecated. use --load-mode dio instead\n"); + params.load_mode = value ? LLAMA_LOAD_MODE_DIRECT_IO : LLAMA_LOAD_MODE_NONE; } ).set_env("LLAMA_ARG_DIO")); + add_opt(common_arg( + {"-lm", "--load-mode"}, "MODE", + "model loading mode (default: mmap)\n" + "- none: no special loading mode\n" + "- mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)\n" + "- mlock: mmap + force system to keep model in RAM rather than swapping or compressing\n" + "- dio: use DirectIO if available\n", + [](common_params & params, const std::string & value) { + /**/ if (value == "none") { params.load_mode = LLAMA_LOAD_MODE_NONE; } + else if (value == "mmap") { params.load_mode = LLAMA_LOAD_MODE_MMAP; } + else if (value == "mlock") { params.load_mode = LLAMA_LOAD_MODE_MLOCK; } + else if (value == "dio") { params.load_mode = LLAMA_LOAD_MODE_DIRECT_IO; } + else { throw std::invalid_argument("invalid value"); } + } + ).set_env("LLAMA_ARG_LOAD_MODE")); add_opt(common_arg( {"--numa"}, "TYPE", "attempt optimizations that help on some NUMA systems\n" diff --git a/common/common.cpp b/common/common.cpp index 8f13217ab..a68766cbb 100644 --- a/common/common.cpp +++ b/common/common.cpp @@ -1558,10 +1558,8 @@ struct llama_model_params common_model_params_to_llama(common_params & params) { mparams.n_gpu_layers = params.n_gpu_layers; mparams.main_gpu = params.main_gpu; mparams.split_mode = params.split_mode; + mparams.load_mode = params.load_mode; mparams.tensor_split = params.tensor_split; - mparams.use_mmap = params.use_mmap; - mparams.use_direct_io = params.use_direct_io; - mparams.use_mlock = params.use_mlock; mparams.check_tensors = params.check_tensors; mparams.use_extra_bufts = !params.no_extra_bufts; mparams.no_host = params.no_host; diff --git a/common/common.h b/common/common.h index bffc1767a..cb1827f5d 100644 --- a/common/common.h +++ b/common/common.h @@ -6,6 +6,7 @@ #include "ggml-opt.h" #include "ggml.h" +#include "llama.h" #include #include @@ -482,6 +483,7 @@ struct common_params { std::vector fit_params_target = std::vector(llama_max_devices(), 1024 * 1024*1024); enum llama_split_mode split_mode = LLAMA_SPLIT_MODE_LAYER; // how to split the model across GPUs + enum llama_load_mode load_mode = LLAMA_LOAD_MODE_MMAP; // how to load the model common_cpu_params cpuparams; common_cpu_params cpuparams_batch; @@ -572,9 +574,6 @@ struct common_params { bool kv_unified = false; // enable unified KV cache bool input_prefix_bos = false; // prefix BOS to user inputs, preceding input_prefix - bool use_mmap = true; // enable mmap to use filesystem cache - bool use_direct_io = false; // read from disk without buffering - bool use_mlock = false; // use mlock to keep model in memory bool verbose_prompt = false; // print prompt tokens before generation bool display_prompt = true; // print prompt before generation bool no_kv_offload = false; // disable KV offloading diff --git a/common/fit.cpp b/common/fit.cpp index afbf0b10f..c79221cb0 100644 --- a/common/fit.cpp +++ b/common/fit.cpp @@ -54,8 +54,7 @@ static std::vector common_get_device_memory_data_impl( llama_model_params mparams_copy = *mparams; mparams_copy.no_alloc = true; - mparams_copy.use_mmap = false; - mparams_copy.use_mlock = false; + mparams_copy.load_mode = LLAMA_LOAD_MODE_NONE; llama_model * model = llama_model_load_from_file(path_model, mparams_copy); if (model == nullptr) { diff --git a/examples/diffusion/diffusion-cli.cpp b/examples/diffusion/diffusion-cli.cpp index 86ebbf88c..d58d22eff 100644 --- a/examples/diffusion/diffusion-cli.cpp +++ b/examples/diffusion/diffusion-cli.cpp @@ -117,9 +117,7 @@ int main(int argc, char ** argv) { llama_model_params model_params = llama_model_default_params(); model_params.n_gpu_layers = params.n_gpu_layers; model_params.devices = params.devices.data(); - model_params.use_mmap = params.use_mmap; - model_params.use_direct_io = params.use_direct_io; - model_params.use_mlock = params.use_mlock; + model_params.load_mode = params.load_mode; model_params.check_tensors = params.check_tensors; llama_model * model = llama_model_load_from_file(params.model.path.c_str(), model_params); diff --git a/examples/training/finetune.cpp b/examples/training/finetune.cpp index 0a75ac110..44b284391 100644 --- a/examples/training/finetune.cpp +++ b/examples/training/finetune.cpp @@ -26,10 +26,9 @@ int main(int argc, char ** argv) { return 1; } - if (params.use_mmap) { - LOG_INF("%s: force disabling memory mapping because it would result in-read-only pointers to the weights\n", - __func__); - params.use_mmap = false; + if (params.load_mode != LLAMA_LOAD_MODE_NONE) { + LOG_INF("%s: forcing load_mode = none to enable writable pointers to the weights\n", __func__); + params.load_mode = LLAMA_LOAD_MODE_NONE; } if (params.cache_type_k != GGML_TYPE_F32) { LOG_INF("%s: force changing k cache type to f32 due to a lack of f16 support for OUT_PROD\n", __func__); diff --git a/include/llama.h b/include/llama.h index a311ac202..9fab69317 100644 --- a/include/llama.h +++ b/include/llama.h @@ -202,6 +202,16 @@ extern "C" { LLAMA_SPLIT_MODE_TENSOR = 3, }; + enum llama_load_mode { + LLAMA_LOAD_MODE_NONE = 0, // no special loading mode + LLAMA_LOAD_MODE_MMAP = 1, // memory map the model + LLAMA_LOAD_MODE_MLOCK = 2, // mmap + force system to keep model in RAM rather than swapping or compressing + LLAMA_LOAD_MODE_DIRECT_IO = 3, // use direct I/O if available + }; + + LLAMA_API const char * llama_load_mode_name(enum llama_load_mode load_mode); + LLAMA_API enum llama_load_mode llama_load_mode_from_str(const char * str); + enum llama_context_type { LLAMA_CONTEXT_TYPE_DEFAULT = 0, LLAMA_CONTEXT_TYPE_MTP = 1, @@ -301,6 +311,7 @@ extern "C" { int32_t n_gpu_layers; // number of layers to store in VRAM, a negative value means all layers enum llama_split_mode split_mode; // how to split the model across multiple GPUs + enum llama_load_mode load_mode; // how to load the model // the GPU that is used for the entire model when split_mode is LLAMA_SPLIT_MODE_NONE int32_t main_gpu; @@ -321,9 +332,6 @@ extern "C" { // Keep the booleans together to avoid misalignment during copy-by-value. bool vocab_only; // only load the vocabulary, no weights - bool use_mmap; // use mmap if possible - bool use_direct_io; // use direct io, takes precedence over use_mmap when supported - bool use_mlock; // force system to keep model in RAM bool check_tensors; // validate model tensor data bool use_extra_bufts; // use extra buffer types (used for weight repacking) bool no_host; // bypass host buffer allowing extra buffers to be used diff --git a/scripts/compare-llama-bench.py b/scripts/compare-llama-bench.py index 5a6cc7dbb..e5f26b5a4 100755 --- a/scripts/compare-llama-bench.py +++ b/scripts/compare-llama-bench.py @@ -28,7 +28,7 @@ LLAMA_BENCH_DB_FIELDS = [ "model_type", "model_size", "model_n_params", "n_batch", "n_ubatch", "n_threads", "cpu_mask", "cpu_strict", "poll", "type_k", "type_v", "n_gpu_layers", "split_mode", "main_gpu", "no_kv_offload", "flash_attn", "tensor_split", "tensor_buft_overrides", - "use_mmap", "embeddings", "no_op_offload", "n_prompt", "n_gen", "n_depth", + "load_mode", "embeddings", "no_op_offload", "n_prompt", "n_gen", "n_depth", "test_time", "avg_ns", "stddev_ns", "avg_ts", "stddev_ts", "n_cpu_moe", "fit_target", "fit_min_ctx" ] @@ -38,7 +38,7 @@ LLAMA_BENCH_DB_TYPES = [ "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "TEXT", "INTEGER", "INTEGER", "TEXT", "TEXT", "INTEGER", "TEXT", "INTEGER", "INTEGER", "INTEGER", "TEXT", "TEXT", - "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", + "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "TEXT", "INTEGER", "INTEGER", "REAL", "REAL", "INTEGER", "INTEGER", "INTEGER" ] @@ -63,7 +63,7 @@ assert len(TEST_BACKEND_OPS_DB_FIELDS) == len(TEST_BACKEND_OPS_DB_TYPES) LLAMA_BENCH_KEY_PROPERTIES = [ "cpu_info", "gpu_info", "backends", "n_gpu_layers", "n_cpu_moe", "tensor_buft_overrides", "model_filename", "model_type", "n_batch", "n_ubatch", "embeddings", "cpu_mask", "cpu_strict", "poll", "n_threads", "type_k", "type_v", - "use_mmap", "no_kv_offload", "split_mode", "main_gpu", "tensor_split", "flash_attn", "n_prompt", "n_gen", "n_depth", + "load_mode", "no_kv_offload", "split_mode", "main_gpu", "tensor_split", "flash_attn", "n_prompt", "n_gen", "n_depth", "fit_target", "fit_min_ctx" ] @@ -73,7 +73,7 @@ TEST_BACKEND_OPS_KEY_PROPERTIES = [ ] # Properties that are boolean and are converted to Yes/No for the table: -LLAMA_BENCH_BOOL_PROPERTIES = ["embeddings", "cpu_strict", "use_mmap", "no_kv_offload", "flash_attn"] +LLAMA_BENCH_BOOL_PROPERTIES = ["embeddings", "cpu_strict", "no_kv_offload", "flash_attn"] TEST_BACKEND_OPS_BOOL_PROPERTIES = ["supported", "passed"] # Header names for the table (llama-bench): @@ -82,7 +82,7 @@ LLAMA_BENCH_PRETTY_NAMES = { "tensor_buft_overrides": "Tensor overrides", "model_filename": "File", "model_type": "Model", "model_size": "Model size [GiB]", "model_n_params": "Num. of par.", "n_batch": "Batch size", "n_ubatch": "Microbatch size", "embeddings": "Embeddings", "cpu_mask": "CPU mask", "cpu_strict": "CPU strict", "poll": "Poll", "n_threads": "Threads", "type_k": "K type", "type_v": "V type", - "use_mmap": "Use mmap", "no_kv_offload": "NKVO", "split_mode": "Split mode", "main_gpu": "Main GPU", "tensor_split": "Tensor split", + "load_mode": "Load mode", "no_kv_offload": "NKVO", "split_mode": "Split mode", "main_gpu": "Main GPU", "tensor_split": "Tensor split", "flash_attn": "FlashAttention", } diff --git a/src/llama-model-loader.cpp b/src/llama-model-loader.cpp index 28f8bb793..43447f57d 100644 --- a/src/llama-model-loader.cpp +++ b/src/llama-model-loader.cpp @@ -4,6 +4,7 @@ #include "ggml.h" #include "gguf.h" #include "llama-hparams.h" +#include "llama.h" #include #include @@ -522,8 +523,7 @@ llama_model_loader::llama_model_loader( const std::string & fname, std::vector & splits, FILE * file, - bool use_mmap, - bool use_direct_io, + llama_load_mode load_mode, bool check_tensors, bool no_alloc, const llama_model_kv_override * param_overrides_p, @@ -542,6 +542,9 @@ llama_model_loader::llama_model_loader( tensor_buft_overrides = param_tensor_buft_overrides_p; + this->use_mmap = load_mode == LLAMA_LOAD_MODE_MMAP || load_mode == LLAMA_LOAD_MODE_MLOCK; + this->use_direct_io = load_mode == LLAMA_LOAD_MODE_DIRECT_IO; + if (!fname.empty()) { // Load the main GGUF struct ggml_context * ctx = NULL; @@ -562,20 +565,6 @@ llama_model_loader::llama_model_loader( files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io)); contexts.emplace_back(ctx); - if (use_mmap && use_direct_io) { - if (files.back()->has_direct_io()) { - LLAMA_LOG_WARN("%s: direct I/O is enabled, disabling mmap\n", __func__); - use_mmap = false; - } else { - LLAMA_LOG_WARN("%s: direct I/O is not available, using mmap\n", __func__); - use_direct_io = false; - - // reopen file using std::fopen for mmap - files.pop_back(); - files.emplace_back(new llama_file(fname.c_str(), "rb", false)); - } - } - // Save tensors data offset of the main file. // For subsidiary files, `meta` tensor data offset must not be used, // so we build a unified tensors index for weights. @@ -816,13 +805,11 @@ llama_model_loader::llama_model_loader( } } - if (!llama_mmap::SUPPORTED) { + if (this->use_mmap && !llama_mmap::SUPPORTED) { LLAMA_LOG_WARN("%s: mmap is not supported on this platform\n", __func__); - use_mmap = false; + this->use_mmap = false; } - this->use_mmap = use_mmap; - this->use_direct_io = use_direct_io; this->check_tensors = check_tensors; this->no_alloc = no_alloc; } diff --git a/src/llama-model-loader.h b/src/llama-model-loader.h index c476026d3..75a3652d0 100644 --- a/src/llama-model-loader.h +++ b/src/llama-model-loader.h @@ -126,8 +126,7 @@ struct llama_model_loader { const std::string & fname, std::vector & splits, // optional, only need if the split does not follow naming scheme FILE * file, - bool use_mmap, - bool use_direct_io, + llama_load_mode load_mode, bool check_tensors, bool no_alloc, const llama_model_kv_override * param_overrides_p, diff --git a/src/llama-model.cpp b/src/llama-model.cpp index 7e5bab26c..c9b290e99 100644 --- a/src/llama-model.cpp +++ b/src/llama-model.cpp @@ -16,6 +16,7 @@ #include "llama-memory-hybrid-iswa.h" #include "llama-memory-recurrent.h" +#include "llama.h" #include "models/models.h" #include "ggml.h" @@ -1243,7 +1244,7 @@ void llama_model_base::load_vocab(llama_model_loader & ml) { bool llama_model_base::load_tensors(llama_model_loader & ml) { const auto & split_mode = params.split_mode; - const auto & use_mlock = params.use_mlock; + const bool use_mlock = params.load_mode == LLAMA_LOAD_MODE_MLOCK; const auto & tensor_split = params.tensor_split; const int n_layer_all = hparams.n_layer_all; @@ -1253,8 +1254,8 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) { this->ml = &ml; // to be used by create_tensor() and load_arch_tensors() - LLAMA_LOG_INFO("%s: loading model tensors, this can take a while... (mmap = %s, direct_io = %s)\n", - __func__, ml.use_mmap ? "true" : "false", ml.use_direct_io ? "true" : "false"); + LLAMA_LOG_INFO("%s: loading model tensors, this can take a while... (load_mode = %s)\n", + __func__, llama_load_mode_name(params.load_mode)); // build a list of buffer types for the CPU and GPU devices pimpl->cpu_buft_list = make_cpu_buft_list(devices, params.use_extra_bufts, params.no_host); @@ -2318,15 +2319,13 @@ llama_model_params llama_model_default_params() { /*.tensor_buft_overrides =*/ nullptr, /*.n_gpu_layers =*/ -1, /*.split_mode =*/ LLAMA_SPLIT_MODE_LAYER, + /*.load_mode =*/ LLAMA_LOAD_MODE_MMAP, /*.main_gpu =*/ 0, /*.tensor_split =*/ nullptr, /*.progress_callback =*/ nullptr, /*.progress_callback_user_data =*/ nullptr, /*.kv_overrides =*/ nullptr, /*.vocab_only =*/ false, - /*.use_mmap =*/ true, - /*.use_direct_io =*/ false, - /*.use_mlock =*/ false, /*.check_tensors =*/ false, /*.use_extra_bufts =*/ true, /*.no_host =*/ false, diff --git a/src/llama-quant.cpp b/src/llama-quant.cpp index b187f5de9..afecb7694 100644 --- a/src/llama-quant.cpp +++ b/src/llama-quant.cpp @@ -2,6 +2,7 @@ #include "llama-model.h" #include "llama-model-loader.h" #include "llama-ext.h" +#include "llama.h" #include #include @@ -876,15 +877,15 @@ static void llama_model_quantize_impl(const std::string & fname_inp, const std:: // mmap consistently increases speed on Linux, and also increases speed on Windows with // hot cache. It may cause a slowdown on macOS, possibly related to free memory. #if defined(__linux__) || defined(_WIN32) - constexpr bool use_mmap = true; + constexpr llama_load_mode load_mode = LLAMA_LOAD_MODE_MMAP; #else - constexpr bool use_mmap = false; + constexpr llama_load_mode load_mode = LLAMA_LOAD_MODE_NONE; #endif const llama_model_kv_override * kv_overrides = params->kv_overrides; std::vector splits = {}; llama_model_loader ml(/*metadata*/ nullptr, /*set_tensor_data*/ nullptr, /*set_tensor_data_ud*/ nullptr, - fname_inp, splits, /*file*/ nullptr, use_mmap, /*use_direct_io*/ false, /*check_tensors*/ true, /*no_alloc*/ false, kv_overrides, nullptr); + fname_inp, splits, /*file*/ nullptr, /*load_mode*/ load_mode, /*check_tensors*/ true, /*no_alloc*/ false, kv_overrides, nullptr); ml.init_mappings(false); // no prefetching auto mparams = llama_model_default_params(); diff --git a/src/llama.cpp b/src/llama.cpp index 0de6048f2..11ac9656d 100644 --- a/src/llama.cpp +++ b/src/llama.cpp @@ -46,6 +46,28 @@ const char * llama_flash_attn_type_name(enum llama_flash_attn_type flash_attn_ty GGML_ABORT("fatal error"); } +const char * llama_load_mode_name(enum llama_load_mode load_mode) { + switch (load_mode) { + case LLAMA_LOAD_MODE_NONE: + return "none"; + case LLAMA_LOAD_MODE_MMAP: + return "mmap"; + case LLAMA_LOAD_MODE_MLOCK: + return "mlock"; + case LLAMA_LOAD_MODE_DIRECT_IO: + return "dio"; + } + GGML_ABORT("fatal error"); +} + +enum llama_load_mode llama_load_mode_from_str(const char * str) { + if (std::strcmp(str, "none") == 0) { return LLAMA_LOAD_MODE_NONE; } + if (std::strcmp(str, "mmap") == 0) { return LLAMA_LOAD_MODE_MMAP; } + if (std::strcmp(str, "mlock") == 0) { return LLAMA_LOAD_MODE_MLOCK; } + if (std::strcmp(str, "dio") == 0) { return LLAMA_LOAD_MODE_DIRECT_IO; } + throw std::invalid_argument(std::string("unknown load mode: ") + str); +} + struct llama_sampler_chain_params llama_sampler_chain_default_params() { struct llama_sampler_chain_params result = { /*.no_perf =*/ true, @@ -279,7 +301,7 @@ static bool llama_prepare_model_devices(const llama_model_params & params, llama static std::pair llama_model_load(struct gguf_context * metadata, llama_model_set_tensor_data_t set_tensor_data, void * set_tensor_data_ud, const std::string & fname, std::vector & splits, FILE * file, llama_model_params & params) { try { - llama_model_loader ml(metadata, set_tensor_data, set_tensor_data_ud, fname, splits, file, params.use_mmap, params.use_direct_io, + llama_model_loader ml(metadata, set_tensor_data, set_tensor_data_ud, fname, splits, file, params.load_mode, params.check_tensors, params.no_alloc, params.kv_overrides, params.tensor_buft_overrides); ml.print_info(); @@ -412,7 +434,7 @@ struct llama_model * llama_model_init_from_user( GGML_ASSERT(metadata != nullptr); std::string path_model; std::vector splits = {}; - params.use_mmap = false; + params.load_mode = LLAMA_LOAD_MODE_NONE; params.use_extra_bufts = false; return llama_model_load_from_file_impl(metadata, set_tensor_data, set_tensor_data_ud, path_model, splits, /*file*/ nullptr, params); } diff --git a/tests/test-arg-parser.cpp b/tests/test-arg-parser.cpp index e83ee85dd..000ecd9aa 100644 --- a/tests/test-arg-parser.cpp +++ b/tests/test-arg-parser.cpp @@ -1,6 +1,7 @@ #include "arg.h" #include "common.h" #include "download.h" +#include "llama.h" #include #include @@ -102,11 +103,9 @@ static void test(void) { argv = {"binary_name", "--draft", "123"}; assert(false == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_EMBEDDING)); - // negated arg - argv = {"binary_name", "--no-mmap"}; + argv = {"binary_name", "-lm", "hello"}; assert(false == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); - printf("test-arg-parser: test valid usage\n\n"); argv = {"binary_name", "-m", "model_file.gguf"}; @@ -132,6 +131,22 @@ static void test(void) { assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_SPECULATIVE)); assert(params.speculative.draft.n_max == 123); + argv = {"binary_name", "-lm", "none"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_NONE); + + argv = {"binary_name", "-lm", "mmap"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_MMAP); + + argv = {"binary_name", "-lm", "mlock"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_MLOCK); + + argv = {"binary_name", "-lm", "dio"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_DIRECT_IO); + // multi-value args (CSV) argv = {"binary_name", "--lora", "file1.gguf,\"file2,2.gguf\",\"file3\"\"3\"\".gguf\",file4\".gguf"}; assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); @@ -158,13 +173,32 @@ static void test(void) { assert(params.model.path == "blah.gguf"); assert(params.cpuparams.n_threads == 1010); + setenv("LLAMA_ARG_LOAD_MODE", "blah", true); + argv = {"binary_name"}; + assert(false == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + + setenv("LLAMA_ARG_LOAD_MODE", "mmap", true); + argv = {"binary_name"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_MMAP); + + setenv("LLAMA_ARG_LOAD_MODE", "mlock", true); + argv = {"binary_name"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_MLOCK); + + setenv("LLAMA_ARG_LOAD_MODE", "dio", true); + argv = {"binary_name"}; + assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); + assert(params.load_mode == LLAMA_LOAD_MODE_DIRECT_IO); + printf("test-arg-parser: test negated environment variables\n\n"); - setenv("LLAMA_ARG_MMAP", "0", true); + setenv("LLAMA_ARG_LOAD_MODE", "none", true); setenv("LLAMA_ARG_NO_PERF", "1", true); // legacy format argv = {"binary_name"}; assert(true == common_params_parse(argv.size(), list_str_to_char(argv).data(), params, LLAMA_EXAMPLE_COMMON)); - assert(params.use_mmap == false); + assert(params.load_mode == LLAMA_LOAD_MODE_NONE); assert(params.no_perf == true); printf("test-arg-parser: test environment variables being overwritten\n\n"); diff --git a/tests/test-model-load-cancel.cpp b/tests/test-model-load-cancel.cpp index 9095826fa..ecc302271 100644 --- a/tests/test-model-load-cancel.cpp +++ b/tests/test-model-load-cancel.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[] ) { llama_backend_init(); auto params = llama_model_params{}; - params.use_mmap = false; + params.load_mode = LLAMA_LOAD_MODE_NONE; params.progress_callback = [](float progress, void * ctx){ (void) ctx; return progress > 0.50; diff --git a/tests/test-quantize-stats.cpp b/tests/test-quantize-stats.cpp index e53a7b355..c65557534 100644 --- a/tests/test-quantize-stats.cpp +++ b/tests/test-quantize-stats.cpp @@ -312,7 +312,7 @@ int main(int argc, char ** argv) { { auto mparams = llama_model_default_params(); - mparams.use_mlock = false; + mparams.load_mode = LLAMA_LOAD_MODE_NONE; model = llama_model_load_from_file(params.model.c_str(), mparams); diff --git a/tools/cli/README.md b/tools/cli/README.md index 89be3a5b3..6ee447b07 100644 --- a/tools/cli/README.md +++ b/tools/cli/README.md @@ -55,9 +55,10 @@ | `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)
    (env: LLAMA_ARG_DEFRAG_THOLD) | | `-np, --parallel N` | number of parallel sequences to decode (default: 1)
    (env: LLAMA_ARG_N_PARALLEL) | | `--rpc SERVERS` | comma-separated list of RPC servers (host:port)
    (env: LLAMA_ARG_RPC) | -| `--mlock` | force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | -| `--mmap, --no-mmap` | whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock) (default: enabled)
    (env: LLAMA_ARG_MMAP) | -| `-dio, --direct-io, -ndio, --no-direct-io` | use DirectIO if available. (default: disabled)
    (env: LLAMA_ARG_DIO) | +| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | +| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    (env: LLAMA_ARG_MMAP) | +| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
    (env: LLAMA_ARG_DIO) | +| `-lm, --load-mode MODE` | model loading mode (default: mmap)
    - none: no special loading mode
    - mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    - mlock: mmap + force system to keep model in RAM rather than swapping or compressing
    - dio: use DirectIO if available

    (env: LLAMA_ARG_LOAD_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
    - distribute: spread execution evenly over all nodes
    - isolate: only spawn threads on CPUs on the node that execution started on
    - numactl: use the CPU map provided by numactl
    if run without this previously, it is recommended to drop the system page cache before using this
    see https://github.com/ggml-org/llama.cpp/issues/1437
    (env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
    use --list-devices to see a list of available devices
    (env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | diff --git a/tools/completion/README.md b/tools/completion/README.md index 9873dd24e..17f7cd765 100644 --- a/tools/completion/README.md +++ b/tools/completion/README.md @@ -138,9 +138,10 @@ llama-completion.exe -m models\gemma-1.1-7b-it.Q4_K_M.gguf --ignore-eos -n -1 | `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)
    (env: LLAMA_ARG_DEFRAG_THOLD) | | `-np, --parallel N` | number of parallel sequences to decode (default: 1)
    (env: LLAMA_ARG_N_PARALLEL) | | `--rpc SERVERS` | comma-separated list of RPC servers (host:port)
    (env: LLAMA_ARG_RPC) | -| `--mlock` | force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | -| `--mmap, --no-mmap` | whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock) (default: enabled)
    (env: LLAMA_ARG_MMAP) | -| `-dio, --direct-io, -ndio, --no-direct-io` | use DirectIO if available. (default: disabled)
    (env: LLAMA_ARG_DIO) | +| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | +| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    (env: LLAMA_ARG_MMAP) | +| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
    (env: LLAMA_ARG_DIO) | +| `-lm, --load-mode MODE` | model loading mode (default: mmap)
    - none: no special loading mode
    - mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    - mlock: mmap + force system to keep model in RAM rather than swapping or compressing
    - dio: use DirectIO if available

    (env: LLAMA_ARG_LOAD_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
    - distribute: spread execution evenly over all nodes
    - isolate: only spawn threads on CPUs on the node that execution started on
    - numactl: use the CPU map provided by numactl
    if run without this previously, it is recommended to drop the system page cache before using this
    see https://github.com/ggml-org/llama.cpp/issues/1437
    (env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
    use --list-devices to see a list of available devices
    (env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | diff --git a/tools/llama-bench/llama-bench.cpp b/tools/llama-bench/llama-bench.cpp index 075689388..29ad352d0 100644 --- a/tools/llama-bench/llama-bench.cpp +++ b/tools/llama-bench/llama-bench.cpp @@ -26,6 +26,7 @@ #include "fit.h" #include "ggml.h" #include "llama.h" +#include "log.h" #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN @@ -339,14 +340,13 @@ struct cmd_params { std::vector n_gpu_layers; std::vector n_cpu_moe; std::vector split_mode; + std::vector load_mode; std::vector main_gpu; std::vector no_kv_offload; std::vector flash_attn; std::vector> devices; std::vector> tensor_split; std::vector> tensor_buft_overrides; - std::vector use_mmap; - std::vector use_direct_io; std::vector embeddings; std::vector no_op_offload; std::vector no_host; @@ -384,14 +384,13 @@ static const cmd_params cmd_params_defaults = { /* n_gpu_layers */ { -1 }, /* n_cpu_moe */ { 0 }, /* split_mode */ { LLAMA_SPLIT_MODE_LAYER }, + /* load_mode */ { LLAMA_LOAD_MODE_MMAP }, /* main_gpu */ { 0 }, /* no_kv_offload */ { false }, /* flash_attn */ { LLAMA_FLASH_ATTN_TYPE_AUTO }, /* devices */ { {} }, /* tensor_split */ { std::vector(llama_max_devices(), 0.0f) }, /* tensor_buft_overrides*/ { std::vector{ { nullptr, nullptr } } }, - /* use_mmap */ { true }, - /* use_direct_io */ { false }, /* embeddings */ { false }, /* no_op_offload */ { false }, /* no_host */ { false }, @@ -460,8 +459,9 @@ static void print_usage(int /* argc */, char ** argv) { printf(" -nkvo, --no-kv-offload <0|1> (default: %s)\n", join(cmd_params_defaults.no_kv_offload, ",").c_str()); printf(" -fa, --flash-attn (default: %s)\n", join(transform_to_str(cmd_params_defaults.flash_attn, llama_flash_attn_type_name), ",").c_str()); printf(" -dev, --device (default: auto)\n"); - printf(" -mmp, --mmap <0|1> (default: %s)\n", join(cmd_params_defaults.use_mmap, ",").c_str()); - printf(" -dio, --direct-io <0|1> (default: %s)\n", join(cmd_params_defaults.use_direct_io, ",").c_str()); + printf(" -lm, --load-mode (default: %s)\n", join(transform_to_str(cmd_params_defaults.load_mode, llama_load_mode_name), ",").c_str()); + printf(" -mmp, --mmap <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n"); + printf(" -dio, --direct-io <0|1> (DEPRECATED IN FAVOUR OF --load-mode)\n"); printf(" -embd, --embeddings <0|1> (default: %s)\n", join(cmd_params_defaults.embeddings, ",").c_str()); printf(" -ts, --tensor-split (default: 0)\n"); printf(" -ot --override-tensor =;...\n"); @@ -769,6 +769,34 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { break; } params.split_mode.insert(params.split_mode.end(), modes.begin(), modes.end()); + } else if (arg == "-lm" || arg == "--load-mode") { + if (++i >= argc) { + invalid_param = true; + break; + } + auto p = string_split(argv[i], split_delim); + + std::vector modes; + for (const auto & m : p) { + llama_load_mode mode; + if (m == "none") { + mode = LLAMA_LOAD_MODE_NONE; + } else if (m == "mmap") { + mode = LLAMA_LOAD_MODE_MMAP; + } else if (m == "mlock") { + mode = LLAMA_LOAD_MODE_MLOCK; + } else if (m == "dio") { + mode = LLAMA_LOAD_MODE_DIRECT_IO; + } else { + invalid_param = true; + break; + } + modes.push_back(mode); + } + if (invalid_param) { + break; + } + params.load_mode.insert(params.load_mode.end(), modes.begin(), modes.end()); } else if (arg == "-mg" || arg == "--main-gpu") { if (++i >= argc) { invalid_param = true; @@ -829,15 +857,39 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { invalid_param = true; break; } + LOG_WRN("DEPRECATED: -mmp and --mmap are deprecated in favour of --load-mode. Please use --load-mode mmap instead."); auto p = string_split(argv[i], split_delim); - params.use_mmap.insert(params.use_mmap.end(), p.begin(), p.end()); + + std::vector modes; + for (const auto & m : p) { + llama_load_mode mode; + if (m) { + mode = LLAMA_LOAD_MODE_MMAP; + } else { + mode = LLAMA_LOAD_MODE_NONE; + } + modes.push_back(mode); + } + params.load_mode.insert(params.load_mode.end(), modes.begin(), modes.end()); } else if (arg == "-dio" || arg == "--direct-io") { if (++i >= argc) { invalid_param = true; break; } + LOG_WRN("DEPRECATED: -dio and --direct-io are deprecated in favour of --load-mode. Please use --load-mode dio instead."); auto p = string_split(argv[i], split_delim); - params.use_direct_io.insert(params.use_direct_io.end(), p.begin(), p.end()); + + std::vector modes; + for (const auto & m : p) { + llama_load_mode mode; + if (m) { + mode = LLAMA_LOAD_MODE_DIRECT_IO; + } else { + mode = LLAMA_LOAD_MODE_NONE; + } + modes.push_back(mode); + } + params.load_mode.insert(params.load_mode.end(), modes.begin(), modes.end()); } else if (arg == "-embd" || arg == "--embeddings") { if (++i >= argc) { invalid_param = true; @@ -1093,6 +1145,9 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { if (params.split_mode.empty()) { params.split_mode = cmd_params_defaults.split_mode; } + if (params.load_mode.empty()) { + params.load_mode = cmd_params_defaults.load_mode; + } if (params.main_gpu.empty()) { params.main_gpu = cmd_params_defaults.main_gpu; } @@ -1111,12 +1166,6 @@ static cmd_params parse_cmd_params(int argc, char ** argv) { if (params.tensor_buft_overrides.empty()) { params.tensor_buft_overrides = cmd_params_defaults.tensor_buft_overrides; } - if (params.use_mmap.empty()) { - params.use_mmap = cmd_params_defaults.use_mmap; - } - if (params.use_direct_io.empty()) { - params.use_direct_io = cmd_params_defaults.use_direct_io; - } if (params.embeddings.empty()) { params.embeddings = cmd_params_defaults.embeddings; } @@ -1164,14 +1213,13 @@ struct cmd_params_instance { int n_gpu_layers; int n_cpu_moe; llama_split_mode split_mode; + llama_load_mode load_mode; int main_gpu; bool no_kv_offload; llama_flash_attn_type flash_attn; std::vector devices; std::vector tensor_split; std::vector tensor_buft_overrides; - bool use_mmap; - bool use_direct_io; bool embeddings; bool no_op_offload; bool no_host; @@ -1186,10 +1234,9 @@ struct cmd_params_instance { mparams.devices = const_cast(devices.data()); } mparams.split_mode = split_mode; + mparams.load_mode = load_mode; mparams.main_gpu = main_gpu; mparams.tensor_split = tensor_split.data(); - mparams.use_mmap = use_mmap; - mparams.use_direct_io = use_direct_io; mparams.no_host = no_host; if (n_cpu_moe <= 0) { @@ -1235,9 +1282,7 @@ struct cmd_params_instance { return model == other.model && n_gpu_layers == other.n_gpu_layers && n_cpu_moe == other.n_cpu_moe && split_mode == other.split_mode && main_gpu == other.main_gpu && tensor_split == other.tensor_split && - use_mmap == other.use_mmap && use_direct_io == other.use_direct_io && - devices == other.devices && - no_host == other.no_host && + load_mode == other.load_mode && devices == other.devices && no_host == other.no_host && vec_tensor_buft_override_equal(tensor_buft_overrides, other.tensor_buft_overrides); } @@ -1270,12 +1315,11 @@ static std::vector get_cmd_params_instances(const cmd_param for (const auto & nl : params.n_gpu_layers) for (const auto & ncmoe : params.n_cpu_moe) for (const auto & sm : params.split_mode) + for (const auto & lm : params.load_mode) for (const auto & mg : params.main_gpu) for (const auto & devs : params.devices) for (const auto & ts : params.tensor_split) for (const auto & ot : params.tensor_buft_overrides) - for (const auto & mmp : params.use_mmap) - for (const auto & dio : params.use_direct_io) for (const auto & noh : params.no_host) for (const auto & embd : params.embeddings) for (const auto & nopo : params.no_op_offload) @@ -1295,34 +1339,33 @@ static std::vector get_cmd_params_instances(const cmd_param continue; } cmd_params_instance instance = { - /* .model = */ m, - /* .n_prompt = */ n_prompt, - /* .n_gen = */ 0, - /* .n_depth = */ nd, - /* .n_batch = */ nb, - /* .n_ubatch = */ nub, - /* .type_k = */ tk, - /* .type_v = */ tv, - /* .n_threads = */ nt, - /* .cpu_mask = */ cm, - /* .cpu_strict = */ cs, - /* .poll = */ pl, - /* .n_gpu_layers = */ nl, - /* .n_cpu_moe = */ ncmoe, - /* .split_mode = */ sm, - /* .main_gpu = */ mg, - /* .no_kv_offload= */ nkvo, - /* .flash_attn = */ fa, - /* .devices = */ devs, - /* .tensor_split = */ ts, + /* .model = */ m, + /* .n_prompt = */ n_prompt, + /* .n_gen = */ 0, + /* .n_depth = */ nd, + /* .n_batch = */ nb, + /* .n_ubatch = */ nub, + /* .type_k = */ tk, + /* .type_v = */ tv, + /* .n_threads = */ nt, + /* .cpu_mask = */ cm, + /* .cpu_strict = */ cs, + /* .poll = */ pl, + /* .n_gpu_layers = */ nl, + /* .n_cpu_moe = */ ncmoe, + /* .split_mode = */ sm, + /* .load_mode = */ lm, + /* .main_gpu = */ mg, + /* .no_kv_offload = */ nkvo, + /* .flash_attn = */ fa, + /* .devices = */ devs, + /* .tensor_split = */ ts, /* .tensor_buft_overrides = */ ot, - /* .use_mmap = */ mmp, - /* .use_direct_io= */ dio, - /* .embeddings = */ embd, - /* .no_op_offload= */ nopo, - /* .no_host = */ noh, - /* .fit_target = */ fpt, - /* .fit_min_ctx = */ fpc, + /* .embeddings = */ embd, + /* .no_op_offload = */ nopo, + /* .no_host = */ noh, + /* .fit_target = */ fpt, + /* .fit_min_ctx = */ fpc, }; instances.push_back(instance); } @@ -1332,34 +1375,33 @@ static std::vector get_cmd_params_instances(const cmd_param continue; } cmd_params_instance instance = { - /* .model = */ m, - /* .n_prompt = */ 0, - /* .n_gen = */ n_gen, - /* .n_depth = */ nd, - /* .n_batch = */ nb, - /* .n_ubatch = */ nub, - /* .type_k = */ tk, - /* .type_v = */ tv, - /* .n_threads = */ nt, - /* .cpu_mask = */ cm, - /* .cpu_strict = */ cs, - /* .poll = */ pl, - /* .n_gpu_layers = */ nl, - /* .n_cpu_moe = */ ncmoe, - /* .split_mode = */ sm, - /* .main_gpu = */ mg, - /* .no_kv_offload= */ nkvo, - /* .flash_attn = */ fa, - /* .devices = */ devs, - /* .tensor_split = */ ts, + /* .model = */ m, + /* .n_prompt = */ 0, + /* .n_gen = */ n_gen, + /* .n_depth = */ nd, + /* .n_batch = */ nb, + /* .n_ubatch = */ nub, + /* .type_k = */ tk, + /* .type_v = */ tv, + /* .n_threads = */ nt, + /* .cpu_mask = */ cm, + /* .cpu_strict = */ cs, + /* .poll = */ pl, + /* .n_gpu_layers = */ nl, + /* .n_cpu_moe = */ ncmoe, + /* .split_mode = */ sm, + /* .load_mode = */ lm, + /* .main_gpu = */ mg, + /* .no_kv_offload = */ nkvo, + /* .flash_attn = */ fa, + /* .devices = */ devs, + /* .tensor_split = */ ts, /* .tensor_buft_overrides = */ ot, - /* .use_mmap = */ mmp, - /* .use_direct_io= */ dio, - /* .embeddings = */ embd, - /* .no_op_offload= */ nopo, - /* .no_host = */ noh, - /* .fit_target = */ fpt, - /* .fit_min_ctx = */ fpc, + /* .embeddings = */ embd, + /* .no_op_offload = */ nopo, + /* .no_host = */ noh, + /* .fit_target = */ fpt, + /* .fit_min_ctx = */ fpc, }; instances.push_back(instance); } @@ -1369,34 +1411,33 @@ static std::vector get_cmd_params_instances(const cmd_param continue; } cmd_params_instance instance = { - /* .model = */ m, - /* .n_prompt = */ n_pg.first, - /* .n_gen = */ n_pg.second, - /* .n_depth = */ nd, - /* .n_batch = */ nb, - /* .n_ubatch = */ nub, - /* .type_k = */ tk, - /* .type_v = */ tv, - /* .n_threads = */ nt, - /* .cpu_mask = */ cm, - /* .cpu_strict = */ cs, - /* .poll = */ pl, - /* .n_gpu_layers = */ nl, - /* .n_cpu_moe = */ ncmoe, - /* .split_mode = */ sm, - /* .main_gpu = */ mg, - /* .no_kv_offload= */ nkvo, - /* .flash_attn = */ fa, - /* .devices = */ devs, - /* .tensor_split = */ ts, + /* .model = */ m, + /* .n_prompt = */ n_pg.first, + /* .n_gen = */ n_pg.second, + /* .n_depth = */ nd, + /* .n_batch = */ nb, + /* .n_ubatch = */ nub, + /* .type_k = */ tk, + /* .type_v = */ tv, + /* .n_threads = */ nt, + /* .cpu_mask = */ cm, + /* .cpu_strict = */ cs, + /* .poll = */ pl, + /* .n_gpu_layers = */ nl, + /* .n_cpu_moe = */ ncmoe, + /* .split_mode = */ sm, + /* .load_mode = */ lm, + /* .main_gpu = */ mg, + /* .no_kv_offload = */ nkvo, + /* .flash_attn = */ fa, + /* .devices = */ devs, + /* .tensor_split = */ ts, /* .tensor_buft_overrides = */ ot, - /* .use_mmap = */ mmp, - /* .use_direct_io= */ dio, - /* .embeddings = */ embd, - /* .no_op_offload= */ nopo, - /* .no_host = */ noh, - /* .fit_target = */ fpt, - /* .fit_min_ctx = */ fpc, + /* .embeddings = */ embd, + /* .no_op_offload = */ nopo, + /* .no_host = */ noh, + /* .fit_target = */ fpt, + /* .fit_min_ctx = */ fpc, }; instances.push_back(instance); } @@ -1426,14 +1467,13 @@ struct test { int n_gpu_layers; int n_cpu_moe; llama_split_mode split_mode; + llama_load_mode load_mode; int main_gpu; bool no_kv_offload; llama_flash_attn_type flash_attn; std::vector devices; std::vector tensor_split; std::vector tensor_buft_overrides; - bool use_mmap; - bool use_direct_io; bool embeddings; bool no_op_offload; bool no_host; @@ -1466,14 +1506,13 @@ struct test { n_gpu_layers = inst.n_gpu_layers; n_cpu_moe = inst.n_cpu_moe; split_mode = inst.split_mode; + load_mode = inst.load_mode; main_gpu = inst.main_gpu; no_kv_offload = inst.no_kv_offload; flash_attn = inst.flash_attn; devices = inst.devices; tensor_split = inst.tensor_split; tensor_buft_overrides = inst.tensor_buft_overrides; - use_mmap = inst.use_mmap; - use_direct_io = inst.use_direct_io; embeddings = inst.embeddings; no_op_offload = inst.no_op_offload; no_host = inst.no_host; @@ -1535,8 +1574,8 @@ struct test { "n_ubatch", "n_threads", "cpu_mask", "cpu_strict", "poll", "type_k", "type_v", "n_gpu_layers", "n_cpu_moe", "split_mode", "main_gpu", "no_kv_offload", "flash_attn", "devices", "tensor_split", - "tensor_buft_overrides", "use_mmap", "use_direct_io", "embeddings", - "no_op_offload", "no_host", "fit_target", "fit_min_ctx", + "tensor_buft_overrides", "load_mode", "embeddings", + "no_op_offload", "no_host", "fit_target", "fit_min_ctx", "n_prompt", "n_gen", "n_depth", "test_time", "avg_ns", "stddev_ns", "avg_ts", "stddev_ts" }; @@ -1554,12 +1593,15 @@ struct test { return INT; } if (field == "f16_kv" || field == "no_kv_offload" || field == "cpu_strict" || - field == "use_mmap" || field == "use_direct_io" || field == "embeddings" || field == "no_host") { + field == "embeddings" || field == "no_host") { return BOOL; } if (field == "avg_ts" || field == "stddev_ts") { return FLOAT; } + if (field == "load_mode") { + return STRING; + } return STRING; } @@ -1626,8 +1668,7 @@ struct test { devices_to_string(devices), tensor_split_str, tensor_buft_overrides_str, - std::to_string(use_mmap), - std::to_string(use_direct_io), + llama_load_mode_name(load_mode), std::to_string(embeddings), std::to_string(no_op_offload), std::to_string(no_host), @@ -1806,18 +1847,15 @@ struct markdown_printer : public printer { if (field == "split_mode") { return 6; } + if (field == "load_mode") { + return 10; + } if (field == "flash_attn") { return 3; } if (field == "devices") { return -12; } - if (field == "use_mmap") { - return 4; - } - if (field == "use_direct_io") { - return 3; - } if (field == "test") { return 15; } @@ -1852,11 +1890,8 @@ struct markdown_printer : public printer { if (field == "flash_attn") { return "fa"; } - if (field == "use_mmap") { - return "mmap"; - } - if (field == "use_direct_io") { - return "dio"; + if (field == "load_mode") { + return "lm"; } if (field == "embeddings") { return "embd"; @@ -1945,11 +1980,8 @@ struct markdown_printer : public printer { if (params.tensor_buft_overrides.size() > 1 || !vec_vec_tensor_buft_override_equal(params.tensor_buft_overrides, cmd_params_defaults.tensor_buft_overrides)) { fields.emplace_back("tensor_buft_overrides"); } - if (params.use_mmap.size() > 1 || params.use_mmap != cmd_params_defaults.use_mmap) { - fields.emplace_back("use_mmap"); - } - if (params.use_direct_io.size() > 1 || params.use_direct_io != cmd_params_defaults.use_direct_io) { - fields.emplace_back("use_direct_io"); + if (params.load_mode.size() > 1 || params.load_mode != cmd_params_defaults.load_mode) { + fields.emplace_back("load_mode"); } if (params.embeddings.size() > 1 || params.embeddings != cmd_params_defaults.embeddings) { fields.emplace_back("embeddings"); diff --git a/tools/server/README.md b/tools/server/README.md index 1941dcea2..e6b27a557 100644 --- a/tools/server/README.md +++ b/tools/server/README.md @@ -72,9 +72,10 @@ For the full list of features, please refer to [server's changelog](https://gith | `-ctv, --cache-type-v TYPE` | KV cache data type for V
    allowed values: f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1
    (default: f16)
    (env: LLAMA_ARG_CACHE_TYPE_V) | | `-dt, --defrag-thold N` | KV cache defragmentation threshold (DEPRECATED)
    (env: LLAMA_ARG_DEFRAG_THOLD) | | `--rpc SERVERS` | comma-separated list of RPC servers (host:port)
    (env: LLAMA_ARG_RPC) | -| `--mlock` | force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | -| `--mmap, --no-mmap` | whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock) (default: enabled)
    (env: LLAMA_ARG_MMAP) | -| `-dio, --direct-io, -ndio, --no-direct-io` | use DirectIO if available. (default: disabled)
    (env: LLAMA_ARG_DIO) | +| `--mlock` | DEPRECATED in favor of `--load-mode`: mmap + force system to keep model in RAM rather than swapping or compressing
    (env: LLAMA_ARG_MLOCK) | +| `--mmap, --no-mmap` | DEPRECATED in favor of `--load-mode`: whether to memory-map model. (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    (env: LLAMA_ARG_MMAP) | +| `-dio, --direct-io, -ndio, --no-direct-io` | DEPRECATED in favor of `--load-mode`: use DirectIO if available
    (env: LLAMA_ARG_DIO) | +| `-lm, --load-mode MODE` | model loading mode (default: mmap)
    - none: no special loading mode
    - mmap: memory-map model (if mmap disabled, slower load but may reduce pageouts if not using mlock)
    - mlock: mmap + force system to keep model in RAM rather than swapping or compressing
    - dio: use DirectIO if available

    (env: LLAMA_ARG_LOAD_MODE) | | `--numa TYPE` | attempt optimizations that help on some NUMA systems
    - distribute: spread execution evenly over all nodes
    - isolate: only spawn threads on CPUs on the node that execution started on
    - numactl: use the CPU map provided by numactl
    if run without this previously, it is recommended to drop the system page cache before using this
    see https://github.com/ggml-org/llama.cpp/issues/1437
    (env: LLAMA_ARG_NUMA) | | `-dev, --device ` | comma-separated list of devices to use for offloading (none = don't offload)
    use --list-devices to see a list of available devices
    (env: LLAMA_ARG_DEVICE) | | `--list-devices` | print list of available devices and exit | From 1425386fd996511e1f3295e7366c38289a92a271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20G=C3=A4=C3=9Fler?= Date: Thu, 23 Jul 2026 14:45:51 +0200 Subject: [PATCH 13/21] CUDA: fix external compilation of q1_0 MMQ (#25778) --- ggml/src/ggml-cuda/mmq.cu | 30 ++++++++++++++++++------------ ggml/src/ggml-cuda/mmq.cuh | 10 +++++++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/ggml/src/ggml-cuda/mmq.cu b/ggml/src/ggml-cuda/mmq.cu index 1f0747456..8a0f4d3b5 100644 --- a/ggml/src/ggml-cuda/mmq.cu +++ b/ggml/src/ggml-cuda/mmq.cu @@ -25,12 +25,7 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con case GGML_TYPE_Q8_0: mul_mat_q_case(ctx, args, stream); break; - case GGML_TYPE_MXFP4: - mul_mat_q_case(ctx, args, stream); - break; - case GGML_TYPE_NVFP4: - mul_mat_q_case(ctx, args, stream); - break; +// ----------------------------------------------------------------------- case GGML_TYPE_Q2_K: mul_mat_q_case(ctx, args, stream); break; @@ -46,6 +41,10 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con case GGML_TYPE_Q6_K: mul_mat_q_case(ctx, args, stream); break; +// ----------------------------------------------------------------------- + case GGML_TYPE_IQ1_S: + mul_mat_q_case(ctx, args, stream); + break; case GGML_TYPE_IQ2_XXS: mul_mat_q_case(ctx, args, stream); break; @@ -61,15 +60,19 @@ static void ggml_cuda_mul_mat_q_switch_type(ggml_backend_cuda_context & ctx, con case GGML_TYPE_IQ3_S: mul_mat_q_case(ctx, args, stream); break; - case GGML_TYPE_IQ1_S: - mul_mat_q_case(ctx, args, stream); - break; case GGML_TYPE_IQ4_XS: mul_mat_q_case(ctx, args, stream); break; case GGML_TYPE_IQ4_NL: mul_mat_q_case(ctx, args, stream); break; +// ----------------------------------------------------------------------- + case GGML_TYPE_MXFP4: + mul_mat_q_case(ctx, args, stream); + break; + case GGML_TYPE_NVFP4: + mul_mat_q_case(ctx, args, stream); + break; default: GGML_ABORT("fatal error"); break; @@ -264,21 +267,24 @@ bool ggml_cuda_should_use_mmq(enum ggml_type type, int cc, int64_t ne11, int64_t case GGML_TYPE_Q5_0: case GGML_TYPE_Q5_1: case GGML_TYPE_Q8_0: - case GGML_TYPE_MXFP4: - case GGML_TYPE_NVFP4: +// ------------------------------------------------- case GGML_TYPE_Q2_K: case GGML_TYPE_Q3_K: case GGML_TYPE_Q4_K: case GGML_TYPE_Q5_K: case GGML_TYPE_Q6_K: +// ------------------------------------------------- + case GGML_TYPE_IQ1_S: case GGML_TYPE_IQ2_XXS: case GGML_TYPE_IQ2_XS: case GGML_TYPE_IQ2_S: case GGML_TYPE_IQ3_XXS: case GGML_TYPE_IQ3_S: - case GGML_TYPE_IQ1_S: case GGML_TYPE_IQ4_XS: case GGML_TYPE_IQ4_NL: +// ------------------------------------------------- + case GGML_TYPE_MXFP4: + case GGML_TYPE_NVFP4: mmq_supported = true; break; default: diff --git a/ggml/src/ggml-cuda/mmq.cuh b/ggml/src/ggml-cuda/mmq.cuh index f25835aa3..71e3b2647 100644 --- a/ggml/src/ggml-cuda/mmq.cuh +++ b/ggml/src/ggml-cuda/mmq.cuh @@ -1537,26 +1537,30 @@ void mul_mat_q_case(ggml_backend_cuda_context & ctx, const mmq_args & args, cuda #define DECL_MMQ_CASE(type) \ template void mul_mat_q_case(ggml_backend_cuda_context & ctx, const mmq_args & args, cudaStream_t stream) \ +extern DECL_MMQ_CASE(GGML_TYPE_Q1_0); extern DECL_MMQ_CASE(GGML_TYPE_Q4_0); extern DECL_MMQ_CASE(GGML_TYPE_Q4_1); extern DECL_MMQ_CASE(GGML_TYPE_Q5_0); extern DECL_MMQ_CASE(GGML_TYPE_Q5_1); extern DECL_MMQ_CASE(GGML_TYPE_Q8_0); -extern DECL_MMQ_CASE(GGML_TYPE_MXFP4); -extern DECL_MMQ_CASE(GGML_TYPE_NVFP4); +// ----------------------------------------- extern DECL_MMQ_CASE(GGML_TYPE_Q2_K); extern DECL_MMQ_CASE(GGML_TYPE_Q3_K); extern DECL_MMQ_CASE(GGML_TYPE_Q4_K); extern DECL_MMQ_CASE(GGML_TYPE_Q5_K); extern DECL_MMQ_CASE(GGML_TYPE_Q6_K); +// ----------------------------------------- +extern DECL_MMQ_CASE(GGML_TYPE_IQ1_S); extern DECL_MMQ_CASE(GGML_TYPE_IQ2_XXS); extern DECL_MMQ_CASE(GGML_TYPE_IQ2_XS); extern DECL_MMQ_CASE(GGML_TYPE_IQ2_S); extern DECL_MMQ_CASE(GGML_TYPE_IQ3_XXS); extern DECL_MMQ_CASE(GGML_TYPE_IQ3_S); -extern DECL_MMQ_CASE(GGML_TYPE_IQ1_S); extern DECL_MMQ_CASE(GGML_TYPE_IQ4_NL); extern DECL_MMQ_CASE(GGML_TYPE_IQ4_XS); +// ----------------------------------------- +extern DECL_MMQ_CASE(GGML_TYPE_MXFP4); +extern DECL_MMQ_CASE(GGML_TYPE_NVFP4); // ------------------------------------------------------------------------------------------------------------------------- From c0bc8591e8815c63cb01dd3f051a8b0df02501c9 Mon Sep 17 00:00:00 2001 From: adgup-qti Date: Thu, 23 Jul 2026 21:38:10 +0530 Subject: [PATCH 14/21] hexagon: fix Windows crash when op_poll is enabled (#26029) --- ggml/src/ggml-hexagon/ggml-hexagon.cpp | 2 +- ggml/src/ggml-hexagon/htp-drv.cpp | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp index d19d2d5f0..827c772ff 100644 --- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp +++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp @@ -1662,7 +1662,7 @@ void ggml_hexagon_session::flush_pending(bool all) { const uint32_t timeo = opt_oppoll ? 0 : DSPQUEUE_TIMEOUT; int err = dspqueue_read(this->queue, &flags, 1, &n_dbufs, &dbuf, sizeof(rsp), &rsp_size, (uint8_t *) &rsp, timeo); - if (err == AEE_EEXPIRED) { + if (err == AEE_EEXPIRED || err == AEE_EWOULDBLOCK) { continue; } diff --git a/ggml/src/ggml-hexagon/htp-drv.cpp b/ggml/src/ggml-hexagon/htp-drv.cpp index 40c601054..4f0790801 100644 --- a/ggml/src/ggml-hexagon/htp-drv.cpp +++ b/ggml/src/ggml-hexagon/htp-drv.cpp @@ -59,7 +59,11 @@ typedef AEEResult (*dspqueue_read_pfn_t)(dspqueue_t queue, uint32_t *flags, uint32_t max_message_length, uint32_t *message_length, uint8_t *message, uint32_t timeout_us); - +typedef AEEResult (*dspqueue_read_noblock_pfn_t)(dspqueue_t queue, uint32_t *flags, + uint32_t max_buffers, uint32_t *num_buffers, + struct dspqueue_buffer *buffers, + uint32_t max_message_length, + uint32_t *message_length, uint8_t *message); typedef int (*fastrpc_mmap_pfn_t)(int domain, int fd, void *addr, int offset, size_t length, enum fastrpc_map_flags flags); typedef int (*fastrpc_munmap_pfn_t)(int domain, int fd, void *addr, size_t length); @@ -82,11 +86,12 @@ rpcmem_to_fd_pfn_t rpcmem_to_fd_pfn = nullptr; fastrpc_mmap_pfn_t fastrpc_mmap_pfn = nullptr; fastrpc_munmap_pfn_t fastrpc_munmap_pfn = nullptr; -dspqueue_create_pfn_t dspqueue_create_pfn = nullptr; -dspqueue_close_pfn_t dspqueue_close_pfn = nullptr; -dspqueue_export_pfn_t dspqueue_export_pfn = nullptr; -dspqueue_write_pfn_t dspqueue_write_pfn = nullptr; -dspqueue_read_pfn_t dspqueue_read_pfn = nullptr; +dspqueue_create_pfn_t dspqueue_create_pfn = nullptr; +dspqueue_close_pfn_t dspqueue_close_pfn = nullptr; +dspqueue_export_pfn_t dspqueue_export_pfn = nullptr; +dspqueue_write_pfn_t dspqueue_write_pfn = nullptr; +dspqueue_read_pfn_t dspqueue_read_pfn = nullptr; +dspqueue_read_noblock_pfn_t dspqueue_read_noblock_pfn = nullptr; remote_handle64_open_pfn_t remote_handle64_open_pfn = nullptr; remote_handle64_invoke_pfn_t remote_handle64_invoke_pfn = nullptr; @@ -167,6 +172,12 @@ AEEResult dspqueue_read(dspqueue_t queue, uint32_t * message_length, uint8_t * message, uint32_t timeout_us) { +#ifdef _WIN32 + if (timeout_us == 0) { + return dspqueue_read_noblock_pfn(queue, flags, max_buffers, num_buffers, buffers, max_message_length, + message_length, message); + } +#endif return dspqueue_read_pfn(queue, flags, max_buffers, num_buffers, buffers, max_message_length, message_length, message, timeout_us); } @@ -349,6 +360,7 @@ int htpdrv_init() { dlsym(handle.get(), dspqueue_export_pfn_t, dspqueue_export_pfn, dspqueue_export, false); dlsym(handle.get(), dspqueue_write_pfn_t, dspqueue_write_pfn, dspqueue_write, false); dlsym(handle.get(), dspqueue_read_pfn_t, dspqueue_read_pfn, dspqueue_read, false); + dlsym(handle.get(), dspqueue_read_noblock_pfn_t, dspqueue_read_noblock_pfn, dspqueue_read_noblock, false); dlsym(handle.get(), remote_handle64_open_pfn_t, remote_handle64_open_pfn, remote_handle64_open, false); dlsym(handle.get(), remote_handle64_invoke_pfn_t, remote_handle64_invoke_pfn, remote_handle64_invoke, false); dlsym(handle.get(), remote_handle_control_pfn_t, remote_handle_control_pfn, remote_handle_control, false); From 0a50d9909a3478e82679f505bf8595d1eee4b0a8 Mon Sep 17 00:00:00 2001 From: Max Krasnyansky Date: Thu, 23 Jul 2026 19:13:03 -0700 Subject: [PATCH 15/21] hexagon: further improved pipeline of the core bits (L2, DMA, MM, FA) (#26049) * hex-l2: use dirty ranges for flushing * hex-l2: simplify range based flush logic * hex-l2: optimize dirty range scans * hex-hvx: support for reduce_max_i32 * hex-mm: optimize fused MUL_MAT+ADD to use vtcm for bias when it fits * hex-mmid: optimize mmid row-mapping generation * hex-mmid: optimize mmid row-mapping generation * hex-mmid: optimize mmid row-mapping generation (round2) * hmx-mm: optimize output proc by tiling (col-chunking) * hex-fa: start the next q dmas a bit earlier * hex-fa: prefetch Q even earlier * hvx-fa: optimize softmax to keep things in hvx registers * hex-fa: hoist const register init in softmax loop * hmx-fa: kick off next-qkv DMAs before o-proc * hmx-fa: hoist various checks out of the inner loop * hmx-fa: adjust the cost model to better balance softmax work across hvx threads * hmx-fa: overlap diag rescale build with last HMX task * hmx-fa: optimize idx update in output proc * hmx-fa: unroll the softmax loops for improved perf * hmx-fa: overlap qk-dot with softmax, double-buffer p and s tiles * hex-trace: double the default number of trace entries * hex-trace: add trace events for opbatch and buffer mgmt * hex-trace: overhaul tracing to simplify runtime event handling and support opbatch stats * hex-trace: replace ascii timeline diagram with pipeline bubbles detector * hex-trace: handle missing start/stop events * hex-dma: always log stop/start trace events even for dummy dmas * hex-scripts: fix flake warnings --- ggml/src/ggml-hexagon/ggml-hexagon.cpp | 245 +++---- ggml/src/ggml-hexagon/htp/dma-queue.c | 4 +- ggml/src/ggml-hexagon/htp/dma-queue.h | 35 +- ggml/src/ggml-hexagon/htp/flash-attn-ops.c | 694 +++++++++++++----- ggml/src/ggml-hexagon/htp/flash-attn-ops.h | 82 ++- .../ggml-hexagon/htp/hmx-mm-kernels-tiled.h | 28 +- ggml/src/ggml-hexagon/htp/htp-ctx.h | 8 +- ggml/src/ggml-hexagon/htp/htp-ops.h | 8 +- ggml/src/ggml-hexagon/htp/htp-tensor.c | 287 +++++--- ggml/src/ggml-hexagon/htp/htp-tensor.h | 18 +- ggml/src/ggml-hexagon/htp/hvx-fa-kernels.h | 71 ++ ggml/src/ggml-hexagon/htp/hvx-reduce.h | 40 + ggml/src/ggml-hexagon/htp/main.c | 64 +- ggml/src/ggml-hexagon/htp/matmul-ops.c | 340 ++++++--- ggml/src/ggml-hexagon/htp/matmul-ops.h | 3 +- scripts/snapdragon/ggml-hexagon-profile.py | 330 ++++++--- scripts/snapdragon/ggml-hexagon-trace.py | 170 ++++- 17 files changed, 1666 insertions(+), 761 deletions(-) diff --git a/ggml/src/ggml-hexagon/ggml-hexagon.cpp b/ggml/src/ggml-hexagon/ggml-hexagon.cpp index 827c772ff..5db8fc84b 100644 --- a/ggml/src/ggml-hexagon/ggml-hexagon.cpp +++ b/ggml/src/ggml-hexagon/ggml-hexagon.cpp @@ -143,12 +143,12 @@ static const char * htp_event_name(uint16_t id) { case HTP_TRACE_EVT_HMX_COMP: return "HMX_COMP"; case HTP_TRACE_EVT_L2FLUSH: return "L2FLUSH"; case HTP_TRACE_EVT_INIT: return "INIT"; + case HTP_TRACE_EVT_BUFF: return "BUFF"; default: return "UNKNOWN"; } } -static void ggml_hexagon_dump_op_prof(const std::string &sess_name, const htp_opnode & node, - const htp_prof_desc & pd) { +static void ggml_hexagon_dump_op_prof(const std::string &sess_name, const htp_opnode & node, const htp_prof_desc & pd) { if (!opt_profile) return; uint32_t op_usec = pd.usecs; @@ -168,6 +168,43 @@ static void ggml_hexagon_dump_op_prof(const std::string &sess_name, const htp_op node.op_name().c_str(), fmt.names, fmt.dims, fmt.types, fmt.strides, fmt.kparams, op_usec, op_cycles, pd.cycles_start, mhz, pmu_str); } +static void ggml_hexagon_dump_batch_prof(const std::string & sess_name, const htp_opbatch_rsp & rsp) { + uint64_t batch_cycles = rsp.cycles_stop - rsp.cycles_start; + float batch_mhz = rsp.usecs > 0 ? (float) batch_cycles / rsp.usecs : 0.0f; + + char evt_str[256] = "----"; + if (opt_profile == 3) { + snprintf(evt_str, sizeof(evt_str), "evt-cnt %u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u", + rsp.n_traces[0], rsp.n_traces[1], rsp.n_traces[2], rsp.n_traces[3], + rsp.n_traces[4], rsp.n_traces[5], rsp.n_traces[6], rsp.n_traces[7], + rsp.n_traces[8], rsp.n_traces[9], rsp.n_traces[10]); + } + + GGML_LOG_DEBUG("ggml-hex: %s profile-op OPBATCH|----|n-ops %u|%s|----|----|usec %u cycles %llu start %llu mhz %.1f\n", + sess_name.c_str(), rsp.n_ops, evt_str, rsp.usecs, (unsigned long long) batch_cycles, (unsigned long long) rsp.cycles_start, batch_mhz); +} + +static void ggml_hexagon_dump_trace_events(const std::string & sess_name, const htp_opbatch_rsp & rsp, + const htp_trace_desc * trace_events, uint32_t n_traces) { + if (opt_profile == 3 && trace_events) { + uint32_t valid_cnt[HTP_MAX_NTHREADS + 1] = {0}; + for (uint32_t t = 0; t <= HTP_MAX_NTHREADS; t++) { + uint32_t count = rsp.n_traces[t]; + valid_cnt[t] = count > n_traces ? n_traces : count; + } + + for (uint32_t t = 0; t <= HTP_MAX_NTHREADS; t++) { + for (uint32_t idx = 0; idx < valid_cnt[t]; idx++) { + const auto & e = trace_events[t * n_traces + idx]; + bool is_stop = (e.info & 0x8000) != 0; + uint16_t info = e.info & 0x7FFF; + GGML_LOG_DEBUG("ggml-hex: %s trace-evt %s: thread %u info %u %s %u\n", + sess_name.c_str(), htp_event_name(e.id), t, info, is_stop ? "stop" : "start", e.cycles); + } + } + } +} + // ** static inline bool ggml_hexagon_is_repack_type(enum ggml_type type) { @@ -1128,13 +1165,7 @@ struct ggml_hexagon_opbatch { std::unordered_map t_map; // tensor ptr to index std::unordered_multimap d_map; // tensor data to index - struct tensor_range { - uint64_t start; - uint64_t end; - int bi; - std::vector tensors; - }; - std::vector ranges; + unsigned int n_bufs; // num buffers in the batch unsigned int n_tens; // num tensors ... @@ -1155,7 +1186,6 @@ struct ggml_hexagon_opbatch { b_map.clear(); t_map.clear(); d_map.clear(); - ranges.clear(); } ggml_hexagon_opbatch(ggml_hexagon_session *sess, size_t batch_size, size_t max_vmem) { @@ -1209,70 +1239,7 @@ struct ggml_hexagon_opbatch { return bi; } - void add_range(const htp_tensor * h, int ti) { - uint64_t t_start = h->data; - uint64_t t_end = t_start + h->size; - int bi = h->bi; - int first_match = -1; - int unused_idx = -1; - for (size_t i = 0; i < ranges.size(); i++) { - if (ranges[i].bi == -1) { - unused_idx = i; - continue; - } - if (ranges[i].bi != bi) { - continue; - } - if (ranges[i].start >= t_end || ranges[i].end <= t_start) { - continue; - } - - if (first_match == -1) { - first_match = i; - HEX_VERBOSE("ggml-hex: %s range-grow #%d : bi %d [%p, %p) + #%d [%p, %p) -> [%p, %p)\n", - sess->c_name(), (int) i, ranges[i].bi, - (void *) (h_bufs[ranges[i].bi].base + ranges[i].start), - (void *) (h_bufs[ranges[i].bi].base + ranges[i].end), - ti, - (void *) (h_bufs[bi].base + t_start), - (void *) (h_bufs[bi].base + t_end), - (void *) (h_bufs[ranges[i].bi].base + std::min(ranges[i].start, t_start)), - (void *) (h_bufs[ranges[i].bi].base + std::max(ranges[i].end, t_end))); - - ranges[i].start = std::min(ranges[i].start, t_start); - ranges[i].end = std::max(ranges[i].end, t_end); - ranges[i].tensors.push_back(ti); - } else { - HEX_VERBOSE("ggml-hex: %s range-merge #%d [%p, %p) + #%d [%p, %p) -> [%p, %p)\n", - sess->c_name(), first_match, - (void *) (h_bufs[bi].base + ranges[first_match].start), - (void *) (h_bufs[bi].base + ranges[first_match].end), - (int) i, - (void *) (h_bufs[bi].base + ranges[i].start), - (void *) (h_bufs[bi].base + ranges[i].end), - (void *) (h_bufs[bi].base + std::min(ranges[first_match].start, ranges[i].start)), - (void *) (h_bufs[bi].base + std::max(ranges[first_match].end, ranges[i].end))); - - ranges[first_match].start = std::min(ranges[first_match].start, ranges[i].start); - ranges[first_match].end = std::max(ranges[first_match].end, ranges[i].end); - ranges[first_match].tensors.insert( - ranges[first_match].tensors.end(), - ranges[i].tensors.begin(), - ranges[i].tensors.end() - ); - ranges[i].bi = -1; - } - } - - if (first_match == -1) { - if (unused_idx != -1) { - ranges[unused_idx] = {t_start, t_end, bi, {ti}}; - } else { - ranges.push_back({t_start, t_end, bi, {ti}}); - } - } - } bool same_shape(const htp_tensor * h, const ggml_tensor * t) const { int64_t ne0 = t->ne[0]; @@ -1341,8 +1308,7 @@ struct ggml_hexagon_opbatch { h.nb[0] = t->nb[0]; h.nb[1] = t->nb[1]; h.nb[2] = t->nb[2]; h.nb[3] = t->nb[3]; } - h.alias = ti; - add_range(&h, ti); + h.flags = 0; if (ggml_backend_buffer_get_usage(t->buffer) != GGML_BACKEND_BUFFER_USAGE_WEIGHTS) { @@ -1424,14 +1390,6 @@ struct ggml_hexagon_opbatch { } void finalize_ranges() { - for (const auto & r : ranges) { - if (r.bi == -1) { - continue; - } - for (size_t i = 0; i < r.tensors.size(); i++) { - h_tens[r.tensors[i]].alias = r.tensors[(i + 1) % r.tensors.size()]; - } - } } }; @@ -1582,9 +1540,6 @@ struct ggml_hexagon_opqueue { if (opt_profile && rsp.n_ops > 0) { auto & ops = op_cache[rsp.id]; - uint64_t batch_usec = ggml_time_us() - start_usec[rsp.id]; - uint32_t htp_usec = 0; - GGML_ASSERT(rsp.n_ops <= ops.size()); const htp_prof_desc * pd = (const htp_prof_desc *) p_ptr; @@ -1595,55 +1550,13 @@ struct ggml_hexagon_opqueue { trace_events = (const htp_trace_desc *) (p_ptr + p_size); } - uint32_t trace_idx[HTP_MAX_NTHREADS + 1] = {0}; - uint32_t valid_cnt[HTP_MAX_NTHREADS + 1] = {0}; - - if (opt_profile == 3) { - for (uint32_t t = 0; t <= HTP_MAX_NTHREADS; t++) { - uint32_t count = rsp.n_traces[t]; - valid_cnt[t] = count > n_traces ? n_traces : count; - } - } + ggml_hexagon_dump_batch_prof(shm_buf->sess->name, rsp); for (uint32_t i = 0; i < rsp.n_ops; i++) { - htp_usec += pd[i].usecs; - ggml_hexagon_dump_op_prof(shm_buf->sess->name, ops[i], pd[i]); - - if (opt_profile == 3) { - uint32_t op_duration = pd[i].cycles_stop - pd[i].cycles_start; - - for (uint32_t t = 0; t <= HTP_MAX_NTHREADS; t++) { - while (trace_idx[t] < valid_cnt[t]) { - const auto & e = trace_events[t * n_traces + trace_idx[t]]; - uint32_t offset = e.cycles - pd[i].cycles_start; - if (offset >= 0x80000000) { - trace_idx[t]++; - continue; - } - if (offset > op_duration) { - break; - } - bool is_stop = (e.info & 0x8000) != 0; - uint16_t info = e.info & 0x7FFF; - GGML_LOG_DEBUG("ggml-hex: %s trace-op %s: thread %u event %s info %u %s %u\n", - shm_buf->sess->c_name(), ops[i].op_name().c_str(), t, htp_event_name(e.id), info, is_stop ? "stop" : "start", e.cycles); - trace_idx[t]++; - } - } - } } - char evt_str[256] = ""; - if (opt_profile == 3) { - snprintf(evt_str, sizeof(evt_str), " evt [%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u]", - rsp.n_traces[0], rsp.n_traces[1], rsp.n_traces[2], rsp.n_traces[3], - rsp.n_traces[4], rsp.n_traces[5], rsp.n_traces[6], rsp.n_traces[7], - rsp.n_traces[8], rsp.n_traces[9], rsp.n_traces[10]); - } - - GGML_LOG_DEBUG("ggml-hex: %s profile-batch n-ops %u batch-dur-usec %lld htp-ops-usec %u%s\n", - shm_buf->sess->c_name(), rsp.n_ops, (long long) batch_usec, htp_usec, evt_str); + ggml_hexagon_dump_trace_events(shm_buf->sess->name, rsp, trace_events, n_traces); } } }; @@ -2114,7 +2027,7 @@ static bool ggml_hexagon_precompute_flash_attn_params( const struct ggml_tensor * sinks = op->src[4]; if (ggml_hexagon_flash_attn_is_hmx_eligible(sess, q, k, v, sinks)) { size_t Br = 0, Bc = 0; - int ret = hmx_fa_find_chunk_size(&Br, &Bc, G, DK, DV, neq1, nek1, sess->vtcm_size, sess->n_threads); + int ret = hmx_fa_find_chunk_size(&Br, &Bc, G, DK, DV, neq1, nek1, sess->vtcm_size, sess->n_threads, kparams->is_q_fp32 != 0); if (ret == 0) { kparams->kernel_type = HTP_FA_KERNEL_HMX; kparams->Br = Br; @@ -2124,7 +2037,7 @@ static bool ggml_hexagon_precompute_flash_attn_params( kparams->u.hmx.g_br = hex_align_up(G * Br, 32); kparams->u.hmx.pipeline = (kparams->n_kv_blocks >= 3 && sess->n_threads >= 2) ? 1 : 0; - kparams->vtcm_size = hmx_fa_compute_vtcm_usage(G, DK, DV, Br, Bc, kparams->n_threads, kparams->u.hmx.pipeline != 0); + kparams->vtcm_size = hmx_fa_compute_vtcm_usage(G, DK, DV, Br, Bc, kparams->n_threads, kparams->u.hmx.pipeline != 0, kparams->is_q_fp32 != 0); const size_t row_vec_bytes = hex_align_up(Bc * sizeof(uint16_t), 256); kparams->u.hmx.row_buf_stride = row_vec_bytes / 128; // HVX vector is 128 bytes @@ -2413,6 +2326,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( int ne12, int ne13, bool is_matmul_id, + const size_t src2_row_size, size_t vtcm_budget, struct htp_mm_kernel_params * kparams ) { @@ -2438,7 +2352,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( for (uint32_t d = max_prefetch; d >= 2; d /= 2) { htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0->nb[1], 0, d, true, false, false + 0, src0->nb[1], 0, src2_row_size, d, true, false, false ); if (L.total_bytes <= vtcm_budget) { best_n_prefetch = d; @@ -2448,7 +2362,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( if (best_n_prefetch == 2 && L.total_bytes > vtcm_budget) { htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0->nb[1], 0, 2, true, false, false + 0, src0->nb[1], 0, src2_row_size, 2, true, false, false ); } kparams->n_prefetch = best_n_prefetch; @@ -2472,7 +2386,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( for (uint32_t d = max_prefetch; d >= 2; d /= 2) { htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], d, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, d, false, false, false ); if (L.total_bytes <= vtcm_budget) { best_n_prefetch = d; @@ -2482,7 +2396,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( if (best_n_prefetch == 2 && L.total_bytes > vtcm_budget) { htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 2, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 2, false, false, false ); } @@ -2506,7 +2420,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 16, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 16, false, false, false ); kparams->n_prefetch = 16; @@ -2526,7 +2440,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_F16_F16_VTCM, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 16, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 16, false, false, false ); if (!is_batched && !is_permuted && L.total_bytes <= vtcm_budget) { @@ -2546,7 +2460,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( kparams->src1_row_size = src1->nb[1]; htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 16, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 16, false, false, false ); kparams->vtcm_size = L.total_bytes; kparams->vtcm_src0_size = L.src0_bytes; @@ -2562,7 +2476,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_F32_F32_VTCM, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 16, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 16, false, false, false ); if (!is_batched && !is_permuted && L.total_bytes <= vtcm_budget) { @@ -2578,7 +2492,7 @@ static void ggml_hexagon_precompute_hvx_mm_params( kparams->src1_row_size = src1->nb[1]; htp_mm_hvx_vtcm_layout_build( &L, kparams->kernel_type, wtype, ne10, src1_nrows, sess->n_threads, - dst->nb[1], src0->nb[1], src1->nb[1], 16, false, false, false + dst->nb[1], src0->nb[1], src1->nb[1], src2_row_size, 16, false, false, false ); kparams->vtcm_size = L.total_bytes; kparams->vtcm_src0_size = L.src0_bytes; @@ -2589,11 +2503,12 @@ static void ggml_hexagon_precompute_hvx_mm_params( } } -static void ggml_hexagon_precompute_matmul_params( +static void ggml_hexagon_precompute_matmul_params_impl( const struct ggml_hexagon_session * sess, const struct ggml_tensor * src0, const struct ggml_tensor * src1, const struct ggml_tensor * dst, + const size_t src2_row_size, struct htp_mm_kernel_params * kparams ) { memset(kparams, 0, sizeof(*kparams)); @@ -2628,7 +2543,7 @@ static void ggml_hexagon_precompute_matmul_params( } // Fallback to HVX parameter computation - ggml_hexagon_precompute_hvx_mm_params(sess, src0, src1, dst, wtype, ne02, ne03, ne10, ne11, ne12, ne13, is_matmul_id, vtcm_budget, kparams); + ggml_hexagon_precompute_hvx_mm_params(sess, src0, src1, dst, wtype, ne02, ne03, ne10, ne11, ne12, ne13, is_matmul_id, src2_row_size, vtcm_budget, kparams); finalize: kparams->div_ne12_ne1 = init_fastdiv_values(ne12 * ne11); @@ -2638,6 +2553,27 @@ finalize: kparams->div_ne11 = init_fastdiv_values(ne11); } +static void ggml_hexagon_precompute_matmul_params( + const struct ggml_hexagon_session * sess, + const struct ggml_tensor * src0, + const struct ggml_tensor * src1, + const struct ggml_tensor * dst, + struct htp_mm_kernel_params * kparams +) { + ggml_hexagon_precompute_matmul_params_impl(sess, src0, src1, dst, 0, kparams); +} + +static void ggml_hexagon_precompute_fused_matmul_add_params( + const struct ggml_hexagon_session * sess, + const struct ggml_tensor * src0, + const struct ggml_tensor * src1, + const struct ggml_tensor * src2, + const struct ggml_tensor * dst, + struct htp_mm_kernel_params * kparams +) { + ggml_hexagon_precompute_matmul_params_impl(sess, src0, src1, dst, src2->nb[1], kparams); +} + static void ggml_hexagon_precompute_unary_params( const struct ggml_hexagon_session * sess, uint32_t op, @@ -2731,7 +2667,7 @@ static void ggml_hexagon_precompute_fused_qkv_params( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, src1_row_size, d, false, true, false + 0, src0_row_size, src1_row_size, 0, d, false, true, false ); if (L.total_bytes <= sess->vtcm_size) { best_n_prefetch = d; @@ -2746,7 +2682,7 @@ static void ggml_hexagon_precompute_fused_qkv_params( // Test tiled first htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, src1_row_size, best_n_prefetch, false, true, false + 0, src0_row_size, src1_row_size, 0, best_n_prefetch, false, true, false ); if (try_tiled && L.total_bytes <= sess->vtcm_size) { @@ -2764,7 +2700,7 @@ static void ggml_hexagon_precompute_fused_qkv_params( htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW_FLAT, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, flat_src1_row_size, best_n_prefetch, false, true, false + 0, src0_row_size, flat_src1_row_size, 0, best_n_prefetch, false, true, false ); kparams->vtcm_src0_size = L.src0_bytes; kparams->vtcm_src1_size = L.src1_bytes; @@ -2801,7 +2737,7 @@ static void ggml_hexagon_precompute_fused_ffn_params( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, src1_row_size, d, false, false, true + 0, src0_row_size, src1_row_size, 0, d, false, false, true ); if (L.total_bytes <= sess->vtcm_size) { best_n_prefetch = d; @@ -2816,7 +2752,7 @@ static void ggml_hexagon_precompute_fused_ffn_params( // Test tiled first htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, src1_row_size, best_n_prefetch, false, false, true + 0, src0_row_size, src1_row_size, 0, best_n_prefetch, false, false, true ); if (try_tiled && L.total_bytes <= sess->vtcm_size) { @@ -2833,7 +2769,7 @@ static void ggml_hexagon_precompute_fused_ffn_params( htp_mm_hvx_vtcm_layout_build( &L, HTP_MM_KERNEL_HVX_QUANT_ROW_FLAT, wtype, ne10, src1_nrows, sess->n_threads, - 0, src0_row_size, flat_src1_row_size, best_n_prefetch, false, false, true + 0, src0_row_size, flat_src1_row_size, 0, best_n_prefetch, false, false, true ); kparams->vtcm_src0_size = L.src0_bytes; kparams->vtcm_src1_size = L.src1_bytes; @@ -3656,16 +3592,19 @@ static bool try_fuse_node(const ggml_hexagon_session * sess, const ggml_cgraph * if (n->op == GGML_OP_MUL_MAT && next_node) { if (next_node->op == GGML_OP_ADD && op_is_compute(next_node) && ggml_can_fuse(graph, i, { GGML_OP_MUL_MAT, GGML_OP_ADD })) { if (next_node->src[0] == n || next_node->src[1] == n) { + const struct ggml_tensor * src2 = (next_node->src[0] == n) ? next_node->src[1] : next_node->src[0]; struct htp_mm_kernel_params kparams; - ggml_hexagon_precompute_matmul_params(sess, n->src[0], n->src[1], next_node, &kparams); - if ((size_t)kparams.vtcm_size <= sess->vtcm_size) { + ggml_hexagon_precompute_fused_matmul_add_params(sess, n->src[0], n->src[1], src2, next_node, &kparams); + const int src1_nrows = n->src[1]->ne[1] * n->src[1]->ne[2] * n->src[1]->ne[3]; + const bool can_fuse = (kparams.n_hmx > 0) || (src1_nrows == 1); + if (can_fuse && (size_t)kparams.vtcm_size <= sess->vtcm_size) { htp_opnode node(n, {}, HTP_OP_MUL_MAT_ADD); node.add_fused(next_node); memcpy(node.kernel_params, &kparams, sizeof(kparams)); nodes.push_back(std::move(node)); i += 1; return true; - } else { + } else if (can_fuse) { HEX_VERBOSE("ggml-hex: skip MUL_MAT_ADD fusion because VTCM needed (%d) > budget (%zu)\n", kparams.vtcm_size, sess->vtcm_size); } @@ -4455,7 +4394,7 @@ static void ggml_hexagon_init(ggml_backend_reg * reg) { opt_opstage = str_opstage ? strtoul(str_opstage, NULL, 0) : opt_opstage; opt_opbatch = str_opbatch ? strtoul(str_opbatch, NULL, 0) : opt_opbatch; opt_opqueue = str_opqueue ? strtoul(str_opqueue, NULL, 0) : opt_opqueue; - opt_optrace = str_optrace ? strtoul(str_optrace, NULL, 0) : (opt_opbatch * 128); + opt_optrace = str_optrace ? strtoul(str_optrace, NULL, 0) : (opt_opbatch * 256); opt_oppoll = str_oppoll ? strtoul(str_oppoll, NULL, 0) : opt_oppoll; opt_opfusion = str_opfusion ? atoi(str_opfusion) : opt_opfusion; opt_profile = str_profile ? atoi(str_profile) : 0; diff --git a/ggml/src/ggml-hexagon/htp/dma-queue.c b/ggml/src/ggml-hexagon/htp/dma-queue.c index 8f60a2c9d..4beded1de 100644 --- a/ggml/src/ggml-hexagon/htp/dma-queue.c +++ b/ggml/src/ggml-hexagon/htp/dma-queue.c @@ -101,6 +101,4 @@ void dma_queue_alias_free(dma_queue_t q) { (void) q; } -void dma_queue_flush(dma_queue_t q) { - while (dma_queue_pop(q).dst != NULL) ; -} + diff --git a/ggml/src/ggml-hexagon/htp/dma-queue.h b/ggml/src/ggml-hexagon/htp/dma-queue.h index dba452cfb..264284bda 100644 --- a/ggml/src/ggml-hexagon/htp/dma-queue.h +++ b/ggml/src/ggml-hexagon/htp/dma-queue.h @@ -106,7 +106,7 @@ struct dma_queue_s { bool alias; // When set, dma_queue_delete will not free the ring }; -void dma_queue_flush(dma_queue_t q); + size_t dma_queue_sizeof(size_t capacity); size_t dma_queue_alignof(void); @@ -154,7 +154,6 @@ static inline bool dma_is_vtcm(const dma_queue * q, const void * ptr) { static inline bool dma_queue_push_single_1d(dma_queue * q, dma_ptr dptr, size_t size) { dma_ring * r = q->ring; if (((r->push_idx + 1) & r->idx_mask) == r->pop_idx) { - FARF(HIGH, "dma-push: queue full\n"); return false; } @@ -165,6 +164,8 @@ static inline bool dma_queue_push_single_1d(dma_queue * q, dma_ptr dptr, size_t r->dptr[r->push_idx] = dptr; + htp_trace_event_start(r->trace, HTP_TRACE_EVT_DMA, r->push_idx); + if (size) { desc->next = NULL; desc->desc_size = 0; // 1D mode @@ -173,7 +174,6 @@ static inline bool dma_queue_push_single_1d(dma_queue * q, dma_ptr dptr, size_t desc->order = 0; desc->done = 0; - htp_trace_event_start(r->trace, HTP_TRACE_EVT_DMA, r->push_idx); dmlink(r->tail, desc); r->tail = (dma_descriptor_2d *) desc; } else { @@ -188,7 +188,6 @@ static inline bool dma_queue_push_single_1d(dma_queue * q, dma_ptr dptr, size_t static inline bool dma_queue_push_single_2d(dma_queue * q, dma_ptr dptr, size_t dst_stride, size_t src_stride, size_t row_size, size_t nrows) { dma_ring * r = q->ring; if (((r->push_idx + 1) & r->idx_mask) == r->pop_idx) { - FARF(HIGH, "dma-push: queue full\n"); return false; } @@ -224,8 +223,9 @@ static inline bool dma_queue_push_single_2d(dma_queue * q, dma_ptr dptr, size_t r->dptr[r->push_idx] = dptr; + htp_trace_event_start(r->trace, HTP_TRACE_EVT_DMA, r->push_idx); + if (nrows) { - htp_trace_event_start(r->trace, HTP_TRACE_EVT_DMA, r->push_idx); dmlink(r->tail, desc); r->tail = desc; } else { @@ -252,10 +252,11 @@ static inline dma_ptr dma_queue_pop(dma_queue * q) { dmpoll(); } } - htp_trace_event_stop(r->trace, HTP_TRACE_EVT_DMA, r->pop_idx); dptr = r->dptr[r->pop_idx]; + htp_trace_event_stop(r->trace, HTP_TRACE_EVT_DMA, r->pop_idx); + r->pop_idx = (r->pop_idx + 1) & r->idx_mask; return dptr; } @@ -270,6 +271,8 @@ static inline dma_ptr dma_queue_pop_nowait(dma_queue * q) { dptr = r->dptr[r->pop_idx]; + htp_trace_event_stop(r->trace, HTP_TRACE_EVT_DMA, r->pop_idx); + r->pop_idx = (r->pop_idx + 1) & r->idx_mask; return dptr; } @@ -278,6 +281,10 @@ static inline bool dma_queue_empty(dma_queue * q) { return q->ring->push_idx == q->ring->pop_idx; } +static inline void dma_queue_flush(dma_queue * q) { + while (dma_queue_pop(q).dst != NULL) ; +} + static inline uint32_t dma_queue_depth(dma_queue * q) { return (q->ring->push_idx - q->ring->pop_idx) & q->ring->idx_mask; } @@ -314,14 +321,18 @@ static inline bool dma_queue_push(dma_queue *q, dma_ptr dptr, size_t dst_stride, { const uint8_t *src = (const uint8_t *) dptr.src; uint8_t *dst = (uint8_t *) dptr.dst; - for (size_t r = 0; r < nrows; ++r) { + size_t r = 0; + while (r + 1 < nrows) { dma_ptr p = dma_make_ptr(dst + r * dst_stride, src + r * src_stride); - if (!dma_queue_push_single_1d(q, p, row_size)) - return false; - if (r + 1 < nrows) - dma_queue_pop(q); + if (!dma_queue_push_single_1d(q, p, row_size)) { + dma_queue_flush(q); + } else { + r++; + } } - return true; + dma_queue_flush(q); + dma_ptr p = dma_make_ptr(dst + r * dst_stride, src + r * src_stride); + return dma_queue_push_single_1d(q, p, row_size); } } diff --git a/ggml/src/ggml-hexagon/htp/flash-attn-ops.c b/ggml/src/ggml-hexagon/htp/flash-attn-ops.c index a3bd0bf8f..fe78718c6 100644 --- a/ggml/src/ggml-hexagon/htp/flash-attn-ops.c +++ b/ggml/src/ggml-hexagon/htp/flash-attn-ops.c @@ -123,15 +123,17 @@ struct hmx_fa_context { uint32_t g_br; // hex_align_up(G * Br, 32) - actual tile row dim // VTCM buffers (allocated by vtcm_seq_alloc) + __fp16 * vtcm_q_dma; // Q DMA fetch buffer __fp16 * vtcm_q_tiles; // Q tile format [g_br, D] __fp16 * vtcm_o_tiles[2]; // O ping-pong [g_br, D] __fp16 * vtcm_k_fp16[2]; // K DMA double-buffer [Bc, D] __fp16 * vtcm_v_fp16[2]; // V DMA double-buffer [Bc, D] - __fp16 * vtcm_k_tiles; // K tiles (transposed) + __fp16 * vtcm_k_tiles[2]; // K tiles (transposed, double-buffered) __fp16 * vtcm_v_tiles[2]; // V tiles (column-major, double-buffered) - __fp16 * vtcm_s_tiles; // S = QK^T [g_br, Bc] - __fp16 * vtcm_p_tiles; // P = softmax(S) [g_br, Bc] + __fp16 * vtcm_s_tiles[2]; // S = QK^T [g_br, Bc] (double-buffered) + __fp16 * vtcm_p_tiles[2]; // P = softmax(S) [g_br, Bc] __fp16 * vtcm_d_tiles; // Diagonal rescale [g_br, g_br] + __fp16 * vtcm_d_inv_l; // Diagonal rescale (1/l) [g_br, g_br] HVX_Vector * vtcm_m_vec; // Row max [g_br] HVX_Vector * vtcm_l_vec; // Row sum [g_br] HVX_Vector * vtcm_s_rowmax; // Softmax intermediate [g_br] @@ -236,10 +238,6 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * const uint32_t iv3 = fastdiv(iq3, &factx->broadcast_rv3); const uint32_t iv2 = fastdiv(iq2, &factx->broadcast_rv2); - // Fetch Q row - const uint8_t * q_row_ptr = (const uint8_t *) q->data + (iq1*nbq1 + iq2*nbq2 + iq3*nbq3); - dma_queue_push(dma, dma_make_ptr(spad_q, q_row_ptr), factx->size_q_row_padded, nbq1, size_q_row, 1); - const __fp16 * mp_base = NULL; if (mask) { const uint32_t im2 = fastmodulo(iq2, mask->ne[2], &factx->src3_div2); @@ -247,26 +245,91 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * mp_base = (const __fp16 *) ((const uint8_t *) mask->data + iq1*mask->nb[1] + im2*mask->nb[2] + im3*mask->nb[3]); } - // Prefetch first two blocks - for (uint32_t ib = 0; ib < MIN(factx->n_blocks, 2); ++ib) { - const uint32_t ic_start = ib * FLASH_ATTN_BLOCK_SIZE; - const uint32_t current_block_size = MIN(FLASH_ATTN_BLOCK_SIZE, nek1 - ic_start); + // Precalculate next row variables if there is a next row + bool has_next_ir = (ir + 1 < ir1); + uint32_t next_ik2 = 0, next_ik3 = 0, next_iv2 = 0, next_iv3 = 0; + const uint8_t * next_q_row_ptr = NULL; + const __fp16 * next_mp_base = NULL; - // K - const uint8_t * k_src = (const uint8_t *) k->data + (ic_start*nbk1 + ik2*nbk2 + ik3*nbk3); - uint8_t * k_dst = spad_k + (ib % 2) * factx->size_k_block; - dma_queue_push(dma, dma_make_ptr(k_dst, k_src), factx->size_k_row_padded, nbk1, size_k_row, current_block_size); + const uint8_t * next_k_src0 = NULL; + const uint8_t * next_v_src0 = NULL; + const uint8_t * next_m_src0 = NULL; + uint32_t next_block_size0 = 0; - // V - const uint8_t * v_src = (const uint8_t *) v->data + (ic_start*nbv1 + iv2*nbv2 + iv3*nbv3); - uint8_t * v_dst = spad_v + (ib % 2) * factx->size_v_block; - dma_queue_push(dma, dma_make_ptr(v_dst, v_src), factx->size_v_row_padded, nbv1, size_v_row, current_block_size); + const uint8_t * next_k_src1 = NULL; + const uint8_t * next_v_src1 = NULL; + const uint8_t * next_m_src1 = NULL; + uint32_t next_block_size1 = 0; + + if (has_next_ir) { + const uint32_t next_ir = ir + 1; + const uint32_t next_iq3 = fastdiv(next_ir, &factx->src0_div21); + const uint32_t next_iq2 = fastdiv(next_ir - next_iq3*neq2*neq1, &factx->src0_div1); + const uint32_t next_iq1 = (next_ir - next_iq3*neq2*neq1 - next_iq2 * neq1); + + next_ik3 = fastdiv(next_iq3, &factx->broadcast_rk3); + next_ik2 = fastdiv(next_iq2, &factx->broadcast_rk2); + + next_iv3 = fastdiv(next_iq3, &factx->broadcast_rv3); + next_iv2 = fastdiv(next_iq2, &factx->broadcast_rv2); + + next_q_row_ptr = (const uint8_t *) q->data + (next_iq1*nbq1 + next_iq2*nbq2 + next_iq3*nbq3); - // Mask if (mask) { - const uint8_t * m_src = (const uint8_t *) (mp_base + ic_start); - // Mask is 1D contiguous for this row - dma_cache_push(dma, &m_cache, m_src, current_block_size * 2, current_block_size * 2, current_block_size * 2, 1); + const uint32_t next_im2 = fastmodulo(next_iq2, mask->ne[2], &factx->src3_div2); + const uint32_t next_im3 = fastmodulo(next_iq3, mask->ne[3], &factx->src3_div3); + next_mp_base = (const __fp16 *) ((const uint8_t *) mask->data + next_iq1*mask->nb[1] + next_im2*mask->nb[2] + next_im3*mask->nb[3]); + } + + // Precalculate next K/V block 0 source pointers + { + const uint32_t ic_start = 0; + next_block_size0 = MIN(FLASH_ATTN_BLOCK_SIZE, nek1 - ic_start); + next_k_src0 = (const uint8_t *) k->data + (ic_start*nbk1 + next_ik2*nbk2 + next_ik3*nbk3); + next_v_src0 = (const uint8_t *) v->data + (ic_start*nbv1 + next_iv2*nbv2 + next_iv3*nbv3); + if (mask) { + next_m_src0 = (const uint8_t *) (next_mp_base + ic_start); + } + } + + // Precalculate next K/V block 1 source pointers (if n_blocks > 1) + if (factx->n_blocks > 1) { + const uint32_t ic_start = 1 * FLASH_ATTN_BLOCK_SIZE; + next_block_size1 = MIN(FLASH_ATTN_BLOCK_SIZE, nek1 - ic_start); + next_k_src1 = (const uint8_t *) k->data + (ic_start*nbk1 + next_ik2*nbk2 + next_ik3*nbk3); + next_v_src1 = (const uint8_t *) v->data + (ic_start*nbv1 + next_iv2*nbv2 + next_iv3*nbv3); + if (mask) { + next_m_src1 = (const uint8_t *) (next_mp_base + ic_start); + } + } + } + + if (ir == ir0) { + // Fetch Q row + const uint8_t * q_row_ptr = (const uint8_t *) q->data + (iq1*nbq1 + iq2*nbq2 + iq3*nbq3); + dma_queue_push(dma, dma_make_ptr(spad_q, q_row_ptr), factx->size_q_row_padded, nbq1, size_q_row, 1); + + // Prefetch first two blocks + for (uint32_t ib = 0; ib < MIN(factx->n_blocks, 2); ++ib) { + const uint32_t ic_start = ib * FLASH_ATTN_BLOCK_SIZE; + const uint32_t current_block_size = MIN(FLASH_ATTN_BLOCK_SIZE, nek1 - ic_start); + + // K + const uint8_t * k_src = (const uint8_t *) k->data + (ic_start*nbk1 + ik2*nbk2 + ik3*nbk3); + uint8_t * k_dst = spad_k + (ib % 2) * factx->size_k_block; + dma_queue_push(dma, dma_make_ptr(k_dst, k_src), factx->size_k_row_padded, nbk1, size_k_row, current_block_size); + + // V + const uint8_t * v_src = (const uint8_t *) v->data + (ic_start*nbv1 + iv2*nbv2 + iv3*nbv3); + uint8_t * v_dst = spad_v + (ib % 2) * factx->size_v_block; + dma_queue_push(dma, dma_make_ptr(v_dst, v_src), factx->size_v_row_padded, nbv1, size_v_row, current_block_size); + + // Mask + if (mask) { + const uint8_t * m_src = (const uint8_t *) (mp_base + ic_start); + // Mask is 1D contiguous for this row + dma_cache_push(dma, &m_cache, m_src, current_block_size * 2, current_block_size * 2, current_block_size * 2, 1); + } } } @@ -287,6 +350,11 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * const HVX_Vector slope_vec = hvx_vec_splat_f16(slope); const HVX_Vector v_neg_inf = Q6_Vh_vsplat_R(0xfbff); + const HVX_Vector v_cap = (factx->logit_softcap != 0.0f) ? hvx_vec_splat_f16(factx->logit_softcap) : Q6_V_vzero(); + const HVX_Vector vinf = Q6_Vh_vsplat_R(0xFC00); + const HVX_Vector vmin = Q6_Vh_vsplat_R(0xFBFF); + const HVX_Vector v_log2e = hvx_vec_splat_f16(EXP_LOG2E_F); + const uint32_t stride_v2 = factx->size_v_row_padded * 2; for (uint32_t ib = 0; ib < factx->n_blocks; ++ib) { const uint32_t ic_start = ib * FLASH_ATTN_BLOCK_SIZE; const uint32_t current_block_size = MIN(FLASH_ATTN_BLOCK_SIZE, nek1 - ic_start); @@ -309,7 +377,6 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * // 2. Softcap (in FP16) if (factx->logit_softcap != 0.0f) { - const HVX_Vector v_cap = hvx_vec_splat_f16(factx->logit_softcap); scores_f16 = hvx_vec_tanh_f16(scores_f16); scores_f16 = hvx_vec_mul_f16_f16(scores_f16, v_cap); } @@ -319,8 +386,6 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * // 3. Mask (in FP16) if (mask) { HVX_Vector m_vals_f16 = *(const HVX_UVector *) m_base; - HVX_Vector vinf = Q6_Vh_vsplat_R(0xFC00); - HVX_Vector vmin = Q6_Vh_vsplat_R(0xFBFF); HVX_VectorPred is_inf = Q6_Q_vcmp_eq_VhVh(m_vals_f16, vinf); m_vals_f16 = Q6_V_vmux_QVV(is_inf, vmin, m_vals_f16); @@ -335,10 +400,30 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * HVX_Vector v_max = Q6_V_lo_W(hvx_vec_f16_to_f32(v_max_f16)); // splat block max in FP32 htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_FA_QK, ir); + if (ib + 1 == factx->n_blocks && has_next_ir) { + // Queue next row's Q row! + dma_queue_push(dma, dma_make_ptr(spad_q, next_q_row_ptr), factx->size_q_row_padded, nbq1, size_q_row, 1); + + if (factx->n_blocks % 2 == 0) { + // Queue next row's block 0 (into buffer slot 0) + uint8_t * k_dst = spad_k + 0 * factx->size_k_block; + uint8_t * v_dst = spad_v + 0 * factx->size_v_block; + + // K (block 0 of next row) + dma_queue_push(dma, dma_make_ptr(k_dst, next_k_src0), factx->size_k_row_padded, nbk1, size_k_row, next_block_size0); + + // V (block 0 of next row) + dma_queue_push(dma, dma_make_ptr(v_dst, next_v_src0), factx->size_v_row_padded, nbv1, size_v_row, next_block_size0); + + // Mask (block 0 of next row) + if (mask) { + dma_cache_push(dma, &m_cache, next_m_src0, next_block_size0 * 2, next_block_size0 * 2, next_block_size0 * 2, 1); + } + } + } + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_FA_SFM, ir); { - const HVX_Vector v_log2e = hvx_vec_splat_f16(EXP_LOG2E_F); - // 4. Online Softmax Update HVX_Vector M_new_vec = Q6_Vsf_vmax_VsfVsf(v_max, M_vec); HVX_Vector diff_vec = HVX_OP_SUB_F32(M_vec, M_new_vec); @@ -370,24 +455,20 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * S_vec = HVX_OP_ADD_F32(HVX_OP_MUL_F32(S_vec, ms_vec), p_sum_vec); // 5. Accumulate V (F16 * F16 -> F32 accumulator) - __fp16 __attribute__((aligned(128))) p_arr[VLEN_FP16]; - hvx_vec_store_a(p_arr, 128, P); + const uint8_t * v_ptr = v_base; for (uint32_t j = 0; j < current_block_size; j += 2) { if (j + 1 == current_block_size) { - if (p_arr[j] != 0.0f) { - const uint8_t * v_ptr = v_base + j * factx->size_v_row_padded; - hvx_mad_f32_f16_aa(VKQ32, v_ptr, (p_arr + j), DV); - } + HVX_Vector S0 = hvx_vec_repl_f16(Q6_V_vror_VR(P, j * 2)); + hvx_mad_f32_f16_aa_vec(VKQ32, v_ptr, S0, DV); break; } - if (p_arr[j] == 0.0f && p_arr[j + 1] == 0.0f) { - continue; - } + HVX_Vector S0 = hvx_vec_repl_f16(Q6_V_vror_VR(P, j * 2)); + HVX_Vector S1 = hvx_vec_repl_f16(Q6_V_vror_VR(P, (j + 1) * 2)); - const uint8_t * v_ptr = v_base + j * factx->size_v_row_padded; - hvx_mad_f32_f16_aa_rx2(VKQ32, v_ptr, v_ptr + factx->size_v_row_padded, (p_arr + j), (p_arr + j + 1), DV); + hvx_mad_f32_f16_aa_rx2_vec(VKQ32, v_ptr, v_ptr + factx->size_v_row_padded, S0, S1, DV); + v_ptr += stride_v2; } } htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_FA_SFM, ir); @@ -414,6 +495,61 @@ static void flash_attn_ext_f16_thread(unsigned int nth, unsigned int ith, void * } } + if (has_next_ir) { + if (factx->n_blocks % 2 == 0) { + // Queue next row's block 1 (into buffer slot 1, if n_blocks > 1) + if (factx->n_blocks > 1) { + uint8_t * k_dst = spad_k + 1 * factx->size_k_block; + uint8_t * v_dst = spad_v + 1 * factx->size_v_block; + + // K (block 1 of next row) + dma_queue_push(dma, dma_make_ptr(k_dst, next_k_src1), factx->size_k_row_padded, nbk1, size_k_row, next_block_size1); + + // V (block 1 of next row) + dma_queue_push(dma, dma_make_ptr(v_dst, next_v_src1), factx->size_v_row_padded, nbv1, size_v_row, next_block_size1); + + // Mask (block 1 of next row) + if (mask) { + dma_cache_push(dma, &m_cache, next_m_src1, next_block_size1 * 2, next_block_size1 * 2, next_block_size1 * 2, 1); + } + } + } else { + // Queue next row's block 0 (into buffer slot 0) + { + uint8_t * k_dst = spad_k + 0 * factx->size_k_block; + uint8_t * v_dst = spad_v + 0 * factx->size_v_block; + + // K (block 0 of next row) + dma_queue_push(dma, dma_make_ptr(k_dst, next_k_src0), factx->size_k_row_padded, nbk1, size_k_row, next_block_size0); + + // V (block 0 of next row) + dma_queue_push(dma, dma_make_ptr(v_dst, next_v_src0), factx->size_v_row_padded, nbv1, size_v_row, next_block_size0); + + // Mask (block 0 of next row) + if (mask) { + dma_cache_push(dma, &m_cache, next_m_src0, next_block_size0 * 2, next_block_size0 * 2, next_block_size0 * 2, 1); + } + } + + // Queue next row's block 1 (into buffer slot 1, if n_blocks > 1) + if (factx->n_blocks > 1) { + uint8_t * k_dst = spad_k + 1 * factx->size_k_block; + uint8_t * v_dst = spad_v + 1 * factx->size_v_block; + + // K (block 1 of next row) + dma_queue_push(dma, dma_make_ptr(k_dst, next_k_src1), factx->size_k_row_padded, nbk1, size_k_row, next_block_size1); + + // V (block 1 of next row) + dma_queue_push(dma, dma_make_ptr(v_dst, next_v_src1), factx->size_v_row_padded, nbv1, size_v_row, next_block_size1); + + // Mask (block 1 of next row) + if (mask) { + dma_cache_push(dma, &m_cache, next_m_src1, next_block_size1 * 2, next_block_size1 * 2, next_block_size1 * 2, 1); + } + } + } + } + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_O_PROC, ir); // sinks float M = hvx_vec_get_f32(M_vec); @@ -471,6 +607,7 @@ typedef struct { void * curr_k; uint32_t kv_start; uint32_t rows_per_t; + size_t buf_idx; } fa_k_int_args_t; static void fa_k_interleave_thread(unsigned int n, unsigned int i, void * data) { @@ -488,19 +625,19 @@ static void fa_k_interleave_thread(unsigned int n, unsigned int i, void * data) struct htp_thread_trace * tr = &factx->octx->ctx->trace[i]; htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_FA_K_PREP, (uint16_t) (args->kv_start + start)); - hmx_interleave_rows_to_tiles(factx->vtcm_k_tiles, (const __fp16 *) args->curr_k, total_rows, factx->DK, + hmx_interleave_rows_to_tiles(factx->vtcm_k_tiles[args->buf_idx], (const __fp16 *) args->curr_k, total_rows, factx->DK, args->src_stride, start, end); htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_FA_K_PREP, (uint16_t) (args->kv_start + start)); } -static void fa_phase_k_interleave(struct hmx_fa_context * factx, uint32_t kv_rows, size_t src_stride, void * curr_k, uint32_t kv_start) { +static void fa_phase_k_interleave(struct hmx_fa_context * factx, uint32_t kv_rows, size_t src_stride, void * curr_k, uint32_t kv_start, size_t buf_idx) { work_queue_t wp = factx->octx->ctx->work_queue; uint32_t n = 1; if (factx->n_threads > 1 && kv_rows >= factx->n_threads * 2) { n = factx->n_threads; } uint32_t rows_per_t = hex_align_up(hmx_ceil_div(kv_rows, n), 2); - fa_k_int_args_t args = { factx, kv_rows, src_stride, curr_k, kv_start, rows_per_t }; + fa_k_int_args_t args = { factx, kv_rows, src_stride, curr_k, kv_start, rows_per_t, buf_idx }; if (n > 1) { work_queue_run(wp, fa_k_interleave_thread, &args, n); } else { @@ -645,12 +782,13 @@ static void fa_q_load_thread(unsigned int n, unsigned int i, void * data) { } } - // Initialize vtcm_d_tiles to 0 + // Initialize vtcm_d_tiles and vtcm_d_inv_l to 0 const size_t d_bytes_per_t = hex_align_up(d_tile_bytes / n, 128); const size_t d_start = i * d_bytes_per_t; const size_t d_end = hex_smin(d_start + d_bytes_per_t, d_tile_bytes); if (d_start < d_tile_bytes) { hvx_splat_u8_a((char *) factx->vtcm_d_tiles + d_start, 0, d_end - d_start); + hvx_splat_u8_a((char *) factx->vtcm_d_inv_l + d_start, 0, d_end - d_start); } } @@ -662,15 +800,14 @@ static void fa_q_load_thread(unsigned int n, unsigned int i, void * data) { assert(factx->DK == factx->DV); - const size_t o_tile_bytes = factx->o_tile_bytes; - const bool use_q_dma = (2 * o_tile_bytes >= factx->g_br * DK * (factx->is_q_fp32 ? 4 : 2)); + const bool use_q_dma = (factx->vtcm_q_dma != NULL); __fp16 * q_tiles = factx->vtcm_q_tiles; if (use_q_dma) { const size_t g_rows_end = hex_smin(end, n_rows_g); const uint32_t d_limit = factx->is_q_fp32 ? DK / 32 : DK / 64; - uint8_t * q_flat = (uint8_t *) factx->vtcm_o_tiles[0]; + uint8_t * q_flat = (uint8_t *) factx->vtcm_q_dma; if (factx->is_q_fp32) { switch (d_limit) { case 2: hmx_fa_q_prep_fp32_d2(q_tiles, q_flat, start, end, g_rows_end, DK, G, args->n_rows_q, &factx->div_G, args->q_transposed); break; @@ -781,10 +918,10 @@ static void fa_o_store_thread_f32(unsigned int n, unsigned int i, void * data) { const uint32_t kv_head = args->kv_head; const uint32_t ib3 = args->ib3; - for (size_t r = start; r < end; ++r) { - const size_t q_idx = fastdiv(r, &factx->div_G); - const size_t h_idx = fastmodulo(r, G, &factx->div_G); + size_t q_idx = fastdiv(start, &factx->div_G); + size_t h_idx = fastmodulo(start, G, &factx->div_G); + for (size_t r = start; r < end; ++r) { float * out = (float *) ((uint8_t *) dst->data + (kv_head * G + h_idx) * dst->nb[1] + (q_start + q_idx) * dst->nb[2] + ib3 * dst->nb[3]); @@ -801,6 +938,12 @@ static void fa_o_store_thread_f32(unsigned int n, unsigned int i, void * data) { *(HVX_UVector *) (out + d * 32) = Q6_V_hi_W(vp); } } + + h_idx++; + if (h_idx == G) { + h_idx = 0; + q_idx++; + } } htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) (args->q_start * G + start)); } @@ -829,10 +972,10 @@ static void fa_o_store_thread_f16(unsigned int n, unsigned int i, void * data) { const uint32_t kv_head = args->kv_head; const uint32_t ib3 = args->ib3; - for (size_t r = start; r < end; ++r) { - const size_t q_idx = fastdiv(r, &factx->div_G); - const size_t h_idx = fastmodulo(r, G, &factx->div_G); + size_t q_idx = fastdiv(start, &factx->div_G); + size_t h_idx = fastmodulo(start, G, &factx->div_G); + for (size_t r = start; r < end; ++r) { __fp16 * out = (__fp16 *) ((uint8_t *) dst->data + (kv_head * G + h_idx) * dst->nb[1] + (q_start + q_idx) * dst->nb[2] + ib3 * dst->nb[3]); @@ -851,6 +994,12 @@ static void fa_o_store_thread_f16(unsigned int n, unsigned int i, void * data) { *(HVX_UVector *) (out + d * 64) = Q6_V_hi_W(vp); } } + + h_idx++; + if (h_idx == G) { + h_idx = 0; + q_idx++; + } } htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) (args->q_start * G + start)); } @@ -879,6 +1028,7 @@ static void fa_phase_o_store(struct hmx_fa_context * factx, typedef struct { struct hmx_fa_context * factx; + size_t buf_idx; size_t kv_rows; size_t n_rows_g; size_t n_col_tiles; @@ -960,8 +1110,8 @@ static inline void fa_softmax_impl( uint32_t r0 = r / HMX_FP16_TILE_N_ROWS; uint32_t r1 = r % HMX_FP16_TILE_N_ROWS; - const __fp16 * s_ld_base = factx->vtcm_s_tiles + r0 * HMX_FP16_TILE_N_ROWS * Bc; - __fp16 * p_st_base = factx->vtcm_p_tiles + r0 * HMX_FP16_TILE_N_ROWS * Bc; + const __fp16 * s_ld_base = factx->vtcm_s_tiles[args->buf_idx] + r0 * HMX_FP16_TILE_N_ROWS * Bc; + __fp16 * p_st_base = factx->vtcm_p_tiles[args->buf_idx] + r0 * HMX_FP16_TILE_N_ROWS * Bc; // Decode 2 rows from S tiles into per-thread row buffers if (has_softcap) { @@ -983,7 +1133,26 @@ static inline void fa_softmax_impl( my_row_buf1[ci] = hvx_vec_mul_f16_f16(t1, v_cap); } } else { - for (size_t c = 0; c < kv_rows; c += 64) { + size_t c = 0; + for (; c + 64 < kv_rows; c += 128) { + size_t ci0 = c / 64; + size_t ci1 = ci0 + 1; + const __fp16 * in_dtile0 = s_ld_base + ci0 * HMX_FP16_TILE_N_ELMS * 2; + const __fp16 * in_dtile1 = s_ld_base + ci1 * HMX_FP16_TILE_N_ELMS * 2; + const HVX_Vector * pv_s_in0_0 = ((const HVX_Vector *) in_dtile0) + r1 / 2; + const HVX_Vector * pv_s_in1_0 = pv_s_in0_0 + 16; + const HVX_Vector * pv_s_in0_1 = ((const HVX_Vector *) in_dtile1) + r1 / 2; + const HVX_Vector * pv_s_in1_1 = pv_s_in0_1 + 16; + + HVX_VectorPair vp_s_drow0 = Q6_W_vdeal_VVR(*pv_s_in1_0, *pv_s_in0_0, -2); + my_row_buf0[ci0] = Q6_V_lo_W(vp_s_drow0); + my_row_buf1[ci0] = Q6_V_hi_W(vp_s_drow0); + + HVX_VectorPair vp_s_drow1 = Q6_W_vdeal_VVR(*pv_s_in1_1, *pv_s_in0_1, -2); + my_row_buf0[ci1] = Q6_V_lo_W(vp_s_drow1); + my_row_buf1[ci1] = Q6_V_hi_W(vp_s_drow1); + } + for (; c < kv_rows; c += 64) { size_t ci = c / 64; const __fp16 * in_dtile = s_ld_base + ci * HMX_FP16_TILE_N_ELMS * 2; const HVX_Vector * pv_s_in0 = ((const HVX_Vector *) in_dtile) + r1 / 2; @@ -1007,12 +1176,12 @@ static inline void fa_softmax_impl( HVX_Vector v_s_rowmax0 = v_neg_inf; HVX_Vector v_s_rowmax1 = v_neg_inf; - for (size_t c = 0; c < kv_rows; c += 64) { - size_t ci = c / 64; - const size_t ne = hex_smin(kv_rows - c, 64); - HVX_VectorPred q_tail_keep = Q6_Q_vsetq2_R(ne * sizeof(__fp16)); + if (has_mask) { + for (size_t c = 0; c < kv_rows; c += 64) { + size_t ci = c / 64; + const size_t ne = hex_smin(kv_rows - c, 64); + HVX_VectorPred q_tail_keep = Q6_Q_vsetq2_R(ne * sizeof(__fp16)); - if (has_mask) { HVX_Vector v_mask0, v_mask1; if (mask_broadcast) { @@ -1066,15 +1235,31 @@ static inline void fa_softmax_impl( my_row_buf0[ci] = Q6_V_vmux_QVV(q_keep0, hvx_vec_add_f16_f16(my_row_buf0[ci], v_mask0_scaled), v_neg_inf); my_row_buf1[ci] = Q6_V_vmux_QVV(q_keep1, hvx_vec_add_f16_f16(my_row_buf1[ci], v_mask1_scaled), v_neg_inf); } - } else { + + v_s_rowmax0 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax0, my_row_buf0[ci]); + v_s_rowmax1 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax1, my_row_buf1[ci]); + } + } else { + size_t c = 0; + for (; c + 64 < kv_rows; c += 128) { + size_t ci0 = c / 64; + size_t ci1 = ci0 + 1; + v_s_rowmax0 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax0, my_row_buf0[ci0]); + v_s_rowmax1 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax1, my_row_buf1[ci0]); + v_s_rowmax0 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax0, my_row_buf0[ci1]); + v_s_rowmax1 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax1, my_row_buf1[ci1]); + } + for (; c < kv_rows; c += 64) { + size_t ci = c / 64; + const size_t ne = hex_smin(kv_rows - c, 64); + HVX_VectorPred q_tail_keep = Q6_Q_vsetq2_R(ne * sizeof(__fp16)); if (ne < 64) { my_row_buf0[ci] = Q6_V_vmux_QVV(q_tail_keep, my_row_buf0[ci], v_neg_inf); my_row_buf1[ci] = Q6_V_vmux_QVV(q_tail_keep, my_row_buf1[ci], v_neg_inf); } + v_s_rowmax0 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax0, my_row_buf0[ci]); + v_s_rowmax1 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax1, my_row_buf1[ci]); } - - v_s_rowmax0 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax0, my_row_buf0[ci]); - v_s_rowmax1 = Q6_Vhf_vmax_VhfVhf(v_s_rowmax1, my_row_buf1[ci]); } v_s_rowmax0 = hvx_vec_reduce_max_f16(v_s_rowmax0); @@ -1121,8 +1306,48 @@ static inline void fa_softmax_impl( HVX_Vector v_p_rowsum0 = v_zero; HVX_Vector v_p_rowsum1 = v_zero; - for (size_t c = 0; c < kv_rows; c += 64) { - size_t ci = c / 64; + size_t c = 0; + for (; c + 64 < kv_rows; c += 128) { + size_t ci0 = c / 64; + size_t ci1 = ci0 + 1; + + HVX_Vector v_s_minus_m0_0 = Q6_Vqf16_vsub_VhfVhf(my_row_buf0[ci0], v_dup_m0); + HVX_Vector v_s_minus_m1_0 = Q6_Vqf16_vsub_VhfVhf(my_row_buf1[ci0], v_dup_m1); + HVX_Vector v_s_minus_m0_1 = Q6_Vqf16_vsub_VhfVhf(my_row_buf0[ci1], v_dup_m0); + HVX_Vector v_s_minus_m1_1 = Q6_Vqf16_vsub_VhfVhf(my_row_buf1[ci1], v_dup_m1); + + HVX_Vector v_p_row0_hf_0 = hvx_vec_exp2_f16(Q6_Vhf_equals_Vqf16(v_s_minus_m0_0)); + HVX_Vector v_p_row1_hf_0 = hvx_vec_exp2_f16(Q6_Vhf_equals_Vqf16(v_s_minus_m1_0)); + HVX_Vector v_p_row0_hf_1 = hvx_vec_exp2_f16(Q6_Vhf_equals_Vqf16(v_s_minus_m0_1)); + HVX_Vector v_p_row1_hf_1 = hvx_vec_exp2_f16(Q6_Vhf_equals_Vqf16(v_s_minus_m1_1)); + + __fp16 * out_dtile0 = p_st_base + ci0 * HMX_FP16_TILE_N_ELMS * 2; + __fp16 * out_dtile1 = p_st_base + ci1 * HMX_FP16_TILE_N_ELMS * 2; + HVX_Vector * pv_p_out0_0 = ((HVX_Vector *) out_dtile0) + r1 / 2; + HVX_Vector * pv_p_out1_0 = pv_p_out0_0 + 16; + HVX_Vector * pv_p_out0_1 = ((HVX_Vector *) out_dtile1) + r1 / 2; + HVX_Vector * pv_p_out1_1 = pv_p_out0_1 + 16; + + HVX_VectorPair vp_p_dual0 = Q6_W_vshuff_VVR(v_p_row1_hf_0, v_p_row0_hf_0, -2); + *pv_p_out0_0 = Q6_V_lo_W(vp_p_dual0); + *pv_p_out1_0 = Q6_V_hi_W(vp_p_dual0); + + HVX_VectorPair vp_p_dual1 = Q6_W_vshuff_VVR(v_p_row1_hf_1, v_p_row0_hf_1, -2); + *pv_p_out0_1 = Q6_V_lo_W(vp_p_dual1); + *pv_p_out1_1 = Q6_V_hi_W(vp_p_dual1); + + HVX_VectorPair vp_p0_0 = hvx_vec_f16_to_f32_shuff(v_p_row0_hf_0); + HVX_VectorPair vp_p1_0 = hvx_vec_f16_to_f32_shuff(v_p_row1_hf_0); + HVX_VectorPair vp_p0_1 = hvx_vec_f16_to_f32_shuff(v_p_row0_hf_1); + HVX_VectorPair vp_p1_1 = hvx_vec_f16_to_f32_shuff(v_p_row1_hf_1); + + v_p_rowsum0 = Q6_Vqf32_vadd_Vqf32Vqf32(v_p_rowsum0, Q6_Vqf32_vadd_VsfVsf(Q6_V_lo_W(vp_p0_0), Q6_V_hi_W(vp_p0_0))); + v_p_rowsum0 = Q6_Vqf32_vadd_Vqf32Vqf32(v_p_rowsum0, Q6_Vqf32_vadd_VsfVsf(Q6_V_lo_W(vp_p0_1), Q6_V_hi_W(vp_p0_1))); + v_p_rowsum1 = Q6_Vqf32_vadd_Vqf32Vqf32(v_p_rowsum1, Q6_Vqf32_vadd_VsfVsf(Q6_V_lo_W(vp_p1_0), Q6_V_hi_W(vp_p1_0))); + v_p_rowsum1 = Q6_Vqf32_vadd_Vqf32Vqf32(v_p_rowsum1, Q6_Vqf32_vadd_VsfVsf(Q6_V_lo_W(vp_p1_1), Q6_V_hi_W(vp_p1_1))); + } + for (size_t c_rem = c; c_rem < kv_rows; c_rem += 64) { + size_t ci = c_rem / 64; HVX_Vector v_s_minus_m0 = Q6_Vqf16_vsub_VhfVhf(my_row_buf0[ci], v_dup_m0); HVX_Vector v_s_minus_m1 = Q6_Vqf16_vsub_VhfVhf(my_row_buf1[ci], v_dup_m1); @@ -1281,7 +1506,7 @@ static __attribute__((noinline)) void fa_build_d_diag_inv_l(struct hmx_fa_contex v_content = Q6_V_vror_VR(v_content, 64); } - __fp16 * out_base = factx->vtcm_d_tiles + i * (n_row_tiles_g_br + 1) * HMX_FP16_TILE_N_ELMS; + __fp16 * out_base = factx->vtcm_d_inv_l + i * (n_row_tiles_g_br + 1) * HMX_FP16_TILE_N_ELMS; Q6_vscatter_QRMVhV(q_32_mask, (size_t) out_base, HMX_FP16_TILE_SIZE - 1, v_offsets, v_content); } } @@ -1514,6 +1739,27 @@ static void fa_pop_mask_dma_gqa(dma_queue * dma, uint32_t G) { } } +static inline void fa_prefetch_block(dma_queue * dma, const struct htp_tensor * k, const struct htp_tensor * v, const struct htp_tensor * mask, + uint32_t b, size_t Bc, size_t size_k_row_padded, size_t size_k_row, size_t size_v_row_padded, size_t size_v_row, + uint32_t ik2, uint32_t ik3, uint32_t iv2, uint32_t iv3, uint32_t q_start, uint32_t im3, uint32_t kv_head, uint32_t G, + size_t m_line_bytes, size_t n_rows_q, size_t nek1, size_t prefetch_buf, struct hmx_fa_context * factx) { + const uint32_t prefetch_start = b * Bc; + const uint32_t prefetch_rows = hex_smin(Bc, nek1 - prefetch_start); + const uint8_t * k_prefetch_src = (const uint8_t *) k->data + prefetch_start * k->nb[1] + ik2 * k->nb[2] + ik3 * k->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx->vtcm_k_fp16[prefetch_buf], k_prefetch_src), size_k_row_padded, k->nb[1], size_k_row, prefetch_rows); + const uint8_t * v_prefetch_src = (const uint8_t *) v->data + prefetch_start * v->nb[1] + iv2 * v->nb[2] + iv3 * v->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx->vtcm_v_fp16[prefetch_buf], v_prefetch_src), size_v_row_padded, v->nb[1], size_v_row, prefetch_rows); + + if (mask) { + if (__builtin_expect(factx->mask_broadcast, true)) { + const uint8_t * ms_src = (const uint8_t *) mask->data + q_start * mask->nb[1] + im3 * mask->nb[3] + prefetch_start * sizeof(__fp16); + dma_cache_push(dma, &factx->m_cache, ms_src, m_line_bytes, mask->nb[1], prefetch_rows * sizeof(__fp16), n_rows_q); + } else { + fa_push_mask_dma_gqa(dma, mask, q_start, im3, prefetch_start, kv_head, G, m_line_bytes, prefetch_rows, n_rows_q, factx); + } + } +} + // ============================================================================ // Core HMX flash attention algorithm (GQA-merged) // ============================================================================ @@ -1612,7 +1858,7 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { // Build the VTCM layout once (shared with the host estimator) and place every // scratch buffer at its computed offset. struct hmx_fa_vtcm_layout L; - hmx_fa_vtcm_layout_build(&L, G, DK, DV, Br, Bc, n_threads, pipeline); + hmx_fa_vtcm_layout_build(&L, G, DK, DV, Br, Bc, n_threads, pipeline, factx.is_q_fp32); if (L.total_bytes > ctx->vtcm_size) { return HTP_STATUS_VTCM_TOO_SMALL; @@ -1620,6 +1866,7 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { uint8_t * const base = ctx->vtcm_base; + factx.vtcm_q_dma = VTCM_LAYOUT_PTR(__fp16, base, L.off_q_dma); factx.vtcm_q_tiles = VTCM_LAYOUT_PTR(__fp16, base, L.off_q_tiles); factx.vtcm_o_tiles[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_o_tiles[0]); factx.vtcm_o_tiles[1] = VTCM_LAYOUT_PTR(__fp16, base, L.off_o_tiles[1]); @@ -1627,12 +1874,16 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { factx.vtcm_k_fp16[1] = VTCM_LAYOUT_PTR(__fp16, base, L.off_k_fp16[1]); factx.vtcm_v_fp16[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_v_fp16[0]); factx.vtcm_v_fp16[1] = VTCM_LAYOUT_PTR(__fp16, base, L.off_v_fp16[1]); - factx.vtcm_k_tiles = VTCM_LAYOUT_PTR(__fp16, base, L.off_k_tiles); + factx.vtcm_k_tiles[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_k_tiles[0]); + factx.vtcm_k_tiles[1] = VTCM_LAYOUT_PTR_OPTIONAL(__fp16, base, L.off_k_tiles[1], pipeline); factx.vtcm_v_tiles[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_v_tiles[0]); factx.vtcm_v_tiles[1] = VTCM_LAYOUT_PTR_OPTIONAL(__fp16, base, L.off_v_tiles[1], pipeline); - factx.vtcm_s_tiles = VTCM_LAYOUT_PTR(__fp16, base, L.off_s_tiles); - factx.vtcm_p_tiles = VTCM_LAYOUT_PTR(__fp16, base, L.off_p_tiles); + factx.vtcm_s_tiles[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_s_tiles[0]); + factx.vtcm_s_tiles[1] = VTCM_LAYOUT_PTR_OPTIONAL(__fp16, base, L.off_s_tiles[1], pipeline); + factx.vtcm_p_tiles[0] = VTCM_LAYOUT_PTR(__fp16, base, L.off_p_tiles[0]); + factx.vtcm_p_tiles[1] = VTCM_LAYOUT_PTR_OPTIONAL(__fp16, base, L.off_p_tiles[1], pipeline); factx.vtcm_d_tiles = VTCM_LAYOUT_PTR(__fp16, base, L.off_d_tiles); + factx.vtcm_d_inv_l = VTCM_LAYOUT_PTR(__fp16, base, L.off_d_inv_l); factx.vtcm_m_vec = VTCM_LAYOUT_PTR(HVX_Vector, base, L.off_m_vec); factx.vtcm_l_vec = VTCM_LAYOUT_PTR(HVX_Vector, base, L.off_l_vec); factx.vtcm_s_rowmax = VTCM_LAYOUT_PTR(HVX_Vector, base, L.off_s_rowmax); @@ -1670,6 +1921,12 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { const size_t qo_element_size = factx.is_q_fp32 ? sizeof(float) : sizeof(__fp16); + const bool q_transposed = q->nb[1] < q->nb[2]; + const size_t q_src_stride = q_transposed ? q->nb[2] : q->nb[1]; + const size_t q_row_bytes_untransposed = factx.G * factx.DK * qo_element_size; + const size_t q_row_bytes_trans_factor = factx.DK * qo_element_size; + const uint32_t kv_rows0 = hex_smin(Bc, nek1); + // ======== Reusable job descriptors for pipeline ======== hmx_fa_qk_job_t qk_job; hmx_fa_o_update_job_t ou_job; @@ -1690,34 +1947,34 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { const uint32_t iv2 = kv_head; const uint32_t iv3 = fastdiv(ib3, &kparams->broadcast_rv3); - // 1. Push Q DMA (if Q DMA is used) - const size_t o_tile_bytes = factx.o_tile_bytes; - const bool use_q_dma = (2 * o_tile_bytes >= factx.g_br * factx.DK * (factx.is_q_fp32 ? 4 : 2)); - if (use_q_dma) { - const bool q_transposed = q->nb[1] < q->nb[2]; - const uint8_t * q_ptr = (const uint8_t *) q->data + q_start * q->nb[1] + (kv_head * factx.G) * q->nb[2] + ib3 * q->nb[3]; - const size_t el_size = factx.is_q_fp32 ? sizeof(float) : sizeof(__fp16); - const size_t q_row_bytes = q_transposed ? n_rows_q * factx.DK * el_size : factx.G * factx.DK * el_size; - const size_t src_stride = q_transposed ? q->nb[2] : q->nb[1]; + // 1. Push Q and KV DMAs for the very first iteration. + // Subsequent iterations are enqueued early at the end of the previous iteration. + if (ib3 == 0 && q_start == 0 && kv_head == 0) { + const uint8_t * q_ptr = (const uint8_t *) q->data; + const size_t q_row_bytes = q_transposed ? n_rows_q * q_row_bytes_trans_factor : q_row_bytes_untransposed; const size_t n_rows = q_transposed ? factx.G : n_rows_q; - dma_queue_push(dma, dma_make_ptr(factx.vtcm_o_tiles[0], q_ptr), q_row_bytes, hex_smax(src_stride, q_row_bytes), q_row_bytes, n_rows); + dma_queue_push(dma, dma_make_ptr(factx.vtcm_q_dma, q_ptr), q_row_bytes, hex_smax(q_src_stride, q_row_bytes), q_row_bytes, n_rows); + + if (factx.n_kv_blocks > 0) { + const uint8_t * k_src = (const uint8_t *) k->data + ik2 * k->nb[2] + ik3 * k->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx.vtcm_k_fp16[0], k_src), size_k_row_padded, k->nb[1], size_k_row, kv_rows0); + + const uint8_t * v_src = (const uint8_t *) v->data + iv2 * v->nb[2] + iv3 * v->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx.vtcm_v_fp16[0], v_src), size_v_row_padded, v->nb[1], size_v_row, kv_rows0); + + if (factx.pipeline && mask) { + if (__builtin_expect(factx.mask_broadcast, true)) { + const uint8_t * ms_src = (const uint8_t *) mask->data + q_start * mask->nb[1] + im3 * mask->nb[3] + 0; + dma_cache_push(dma, &factx.m_cache, ms_src, m_line_bytes, mask->nb[1], kv_rows0 * sizeof(__fp16), n_rows_q); + } else { + fa_push_mask_dma_gqa(dma, mask, q_start, im3, 0, kv_head, G, m_line_bytes, kv_rows0, n_rows_q, &factx); + } + } + } } - // 2. Prefetch first KV block - if (factx.n_kv_blocks > 0) { - const uint32_t kv_rows0 = hex_smin(Bc, nek1); - - const uint8_t * k_src = (const uint8_t *) k->data + ik2 * k->nb[2] + ik3 * k->nb[3]; - dma_queue_push(dma, dma_make_ptr(factx.vtcm_k_fp16[0], k_src), size_k_row_padded, k->nb[1], size_k_row, kv_rows0); - - const uint8_t * v_src = (const uint8_t *) v->data + iv2 * v->nb[2] + iv3 * v->nb[3]; - dma_queue_push(dma, dma_make_ptr(factx.vtcm_v_fp16[0], v_src), size_v_row_padded, v->nb[1], size_v_row, kv_rows0); - } - - // 3. Pop Q DMA (blocks until Q is loaded) - if (use_q_dma) { - dma_queue_pop(dma); - } + // 2. Pop Q DMA (blocks until Q is loaded) + dma_queue_pop(dma); // ---- Load Q block & Initialize per-block state ---- fa_phase_q_load(&factx, q, q_start, kv_head, ib3, n_rows_g); @@ -1738,76 +1995,40 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { hmx_queue_t hmx_q = ctx->hmx_queue; if (factx.pipeline) { - // Pipeline path + // Double-buffered job structs because HMX queue runs asynchronously + hmx_fa_qk_job_t qk_job[2]; + hmx_fa_o_update_job_t ou_job[2]; + + // Prefetch block 1 early if there are multiple blocks + if (factx.n_kv_blocks > 1) { + fa_prefetch_block(dma, k, v, mask, 1, Bc, size_k_row_padded, size_k_row, size_v_row_padded, size_v_row, + ik2, ik3, iv2, iv3, q_start, im3, kv_head, G, m_line_bytes, n_rows_q, nek1, 1, &factx); + } + + // Prep and start QK-dot(0) + void * curr_k0 = dma_queue_pop(dma).dst; + fa_phase_k_interleave(&factx, kv_rows0, k_src_stride, curr_k0, 0, 0); + + qk_job[0].q_tiles = factx.vtcm_q_tiles; + qk_job[0].k_tiles = factx.vtcm_k_tiles[0]; + qk_job[0].s_tiles = factx.vtcm_s_tiles[0]; + qk_job[0].n_row_tiles = n_row_tiles; + qk_job[0].n_col_tiles = hmx_ceil_div(kv_rows0, HMX_FP16_TILE_N_COLS); + qk_job[0].n_dot_tiles = DK / 32; + qk_job[0].n_tiles_per_bc = n_tiles_per_bc; + qk_job[0].hmx_scales = factx.vtcm_hmx_scales_qk; + hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_qk_dot_worker, &qk_job[0])); + for (uint32_t kv_blk = 0; kv_blk < factx.n_kv_blocks; ++kv_blk) { const uint32_t kv_start = kv_blk * Bc; const uint32_t kv_rows = hex_smin(Bc, nek1 - kv_start); const size_t n_col_tiles = hmx_ceil_div(kv_rows, HMX_FP16_TILE_N_COLS); - // Push mask DMA - if (mask) { - if (__builtin_expect(factx.mask_broadcast, true)) { - const uint8_t * ms_src = (const uint8_t *) mask->data + q_start * mask->nb[1] + im3 * mask->nb[3] + kv_start * sizeof(__fp16); - dma_cache_push(dma, &factx.m_cache, ms_src, m_line_bytes, mask->nb[1], kv_rows * sizeof(__fp16), n_rows_q); - } else { - fa_push_mask_dma_gqa(dma, mask, q_start, im3, kv_start, kv_head, G, m_line_bytes, kv_rows, n_rows_q, &factx); - } - } - - // Prefetch next KV block early - if (kv_blk + 1 < factx.n_kv_blocks) { - const uint32_t prefetch_start = (kv_blk + 1) * Bc; - const uint32_t prefetch_rows = hex_smin(Bc, nek1 - prefetch_start); - const size_t prefetch_buf = 1 - buf_idx; - const uint8_t * k_prefetch_src = (const uint8_t *) k->data + prefetch_start * k->nb[1] + ik2 * k->nb[2] + ik3 * k->nb[3]; - dma_queue_push(dma, dma_make_ptr(factx.vtcm_k_fp16[prefetch_buf], k_prefetch_src), size_k_row_padded, k->nb[1], size_k_row, prefetch_rows); - const uint8_t * v_prefetch_src = (const uint8_t *) v->data + prefetch_start * v->nb[1] + iv2 * v->nb[2] + iv3 * v->nb[3]; - dma_queue_push(dma, dma_make_ptr(factx.vtcm_v_fp16[prefetch_buf], v_prefetch_src), size_v_row_padded, v->nb[1], size_v_row, prefetch_rows); - } - - // ---- Phase 1: K_int ---- - if (kv_blk > 0) { - ou_job.o_curr = o_tile_curr; - ou_job.o_prev = o_tile_prev; - ou_job.p_tiles = factx.vtcm_p_tiles; - ou_job.v_tiles = factx.vtcm_v_tiles[1 - buf_idx]; - ou_job.d_tiles = factx.vtcm_d_tiles; - ou_job.hmx_scales = factx.vtcm_hmx_scales_id; - ou_job.n_row_tiles = n_row_tiles; - ou_job.n_col_tiles = hmx_ceil_div(hex_smin(Bc, nek1 - (kv_blk - 1) * Bc), HMX_FP16_TILE_N_COLS); - ou_job.n_row_tiles_g_br = n_row_tiles_g_br; - ou_job.n_tiles_per_bc = n_tiles_per_bc; - ou_job.DV = DV; - hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_o_update_worker, &ou_job)); - } - - // Wait for current K DMA and interleave - void * curr_k = dma_queue_pop(dma).dst; - fa_phase_k_interleave(&factx, kv_rows, k_src_stride, curr_k, kv_start); - - // ---- Phase 2: qk_dot ---- - qk_job.q_tiles = factx.vtcm_q_tiles; - qk_job.k_tiles = factx.vtcm_k_tiles; - qk_job.s_tiles = factx.vtcm_s_tiles; - qk_job.n_row_tiles = n_row_tiles; - qk_job.n_col_tiles = n_col_tiles; - qk_job.n_dot_tiles = DK / 32; - qk_job.n_tiles_per_bc = n_tiles_per_bc; - qk_job.hmx_scales = factx.vtcm_hmx_scales_qk; - hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_qk_dot_worker, &qk_job)); - - // Wait for current V DMA and interleave + // ---- 1. Pop and run V-prep for current block ---- void * curr_v = dma_queue_pop(dma).dst; fa_phase_v_interleave(&factx, kv_rows, v_src_stride, curr_v, factx.vtcm_v_tiles[buf_idx], n_tiles_per_bc, kv_start); - if (kv_blk > 0) { - hmx_queue_pop(hmx_q); - hex_swap_ptr((void **) &o_tile_curr, (void **) &o_tile_prev); - } - - hmx_queue_pop(hmx_q); - - // ---- Phase 3: softmax + build_D ---- + // ---- 2. Pop and run mask-prep for current block ---- __fp16 * current_mask_vtcm = NULL; if (mask) { if (__builtin_expect(factx.mask_broadcast, true)) { @@ -1818,9 +2039,34 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { } } + // ---- 3. Pop and run K-prep for next block & push next QK-dot ---- + if (kv_blk + 1 < factx.n_kv_blocks) { + const uint32_t next_start = (kv_blk + 1) * Bc; + const uint32_t next_rows = hex_smin(Bc, nek1 - next_start); + const size_t next_buf = 1 - buf_idx; + + void * next_k = dma_queue_pop(dma).dst; + fa_phase_k_interleave(&factx, next_rows, k_src_stride, next_k, next_start, next_buf); + + qk_job[next_buf].q_tiles = factx.vtcm_q_tiles; + qk_job[next_buf].k_tiles = factx.vtcm_k_tiles[next_buf]; + qk_job[next_buf].s_tiles = factx.vtcm_s_tiles[next_buf]; + qk_job[next_buf].n_row_tiles = n_row_tiles; + qk_job[next_buf].n_col_tiles = hmx_ceil_div(next_rows, HMX_FP16_TILE_N_COLS); + qk_job[next_buf].n_dot_tiles = DK / 32; + qk_job[next_buf].n_tiles_per_bc = n_tiles_per_bc; + qk_job[next_buf].hmx_scales = factx.vtcm_hmx_scales_qk; + hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_qk_dot_worker, &qk_job[next_buf])); + } + + // ---- 4. Wait for current block's QK-dot to finish ---- + hmx_queue_pop(hmx_q); + + // ---- 5. Phase 2: softmax + build_D ---- fa_softmax_args_t sargs; memset(&sargs, 0, sizeof(sargs)); sargs.factx = &factx; + sargs.buf_idx = buf_idx; sargs.kv_rows = kv_rows; sargs.n_rows_g = n_rows_g; sargs.n_col_tiles = n_col_tiles; @@ -1838,8 +2084,39 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { sargs.mask_vtcm = current_mask_vtcm; sargs.mask_vtcm_row_stride = factx.mask_buf_row_stride; sargs.slopes = factx.vtcm_slopes; + + // Start HMX O update for block kv_blk - 1 (reads P[1 - buf_idx], V[1 - buf_idx]) + if (kv_blk > 0) { + const size_t prev_buf = 1 - buf_idx; + ou_job[prev_buf].o_curr = o_tile_curr; + ou_job[prev_buf].o_prev = o_tile_prev; + ou_job[prev_buf].p_tiles = factx.vtcm_p_tiles[prev_buf]; + ou_job[prev_buf].v_tiles = factx.vtcm_v_tiles[prev_buf]; + ou_job[prev_buf].d_tiles = factx.vtcm_d_tiles; + ou_job[prev_buf].hmx_scales = factx.vtcm_hmx_scales_id; + ou_job[prev_buf].n_row_tiles = n_row_tiles; + ou_job[prev_buf].n_col_tiles = hmx_ceil_div(hex_smin(Bc, nek1 - (kv_blk - 1) * Bc), HMX_FP16_TILE_N_COLS); + ou_job[prev_buf].n_row_tiles_g_br = n_row_tiles_g_br; + ou_job[prev_buf].n_tiles_per_bc = n_tiles_per_bc; + ou_job[prev_buf].DV = DV; + hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_o_update_worker, &ou_job[prev_buf])); + } + + // Run Softmax on HVX (blocking call) fa_phase_softmax_and_build_d(&factx, &sargs, n_row_tiles, n_row_tiles_g_br); + // Wait for HMX O update for block kv_blk - 1 to finish + if (kv_blk > 0) { + hmx_queue_pop(hmx_q); + hex_swap_ptr((void **) &o_tile_curr, (void **) &o_tile_prev); + } + + // Prefetch block kv_blk + 2 + if (kv_blk + 2 < factx.n_kv_blocks) { + fa_prefetch_block(dma, k, v, mask, kv_blk + 2, Bc, size_k_row_padded, size_k_row, size_v_row_padded, size_v_row, + ik2, ik3, iv2, iv3, q_start, im3, kv_head, G, m_line_bytes, n_rows_q, nek1, buf_idx, &factx); + } + buf_idx = 1 - buf_idx; } @@ -1847,18 +2124,23 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { if (factx.n_kv_blocks > 0) { const uint32_t last_blk = factx.n_kv_blocks - 1; const size_t last_cols = hmx_ceil_div(hex_smin(Bc, nek1 - last_blk * Bc), HMX_FP16_TILE_N_COLS); - ou_job.o_curr = o_tile_curr; - ou_job.o_prev = o_tile_prev; - ou_job.p_tiles = factx.vtcm_p_tiles; - ou_job.v_tiles = factx.vtcm_v_tiles[1 - buf_idx]; - ou_job.d_tiles = factx.vtcm_d_tiles; - ou_job.hmx_scales = factx.vtcm_hmx_scales_id; - ou_job.n_row_tiles = n_row_tiles; - ou_job.n_col_tiles = last_cols; - ou_job.n_row_tiles_g_br = n_row_tiles_g_br; - ou_job.n_tiles_per_bc = n_tiles_per_bc; - ou_job.DV = DV; - hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_o_update_worker, &ou_job)); + ou_job[0].o_curr = o_tile_curr; + ou_job[0].o_prev = o_tile_prev; + ou_job[0].p_tiles = factx.vtcm_p_tiles[1 - buf_idx]; + ou_job[0].v_tiles = factx.vtcm_v_tiles[1 - buf_idx]; + ou_job[0].d_tiles = factx.vtcm_d_tiles; + ou_job[0].hmx_scales = factx.vtcm_hmx_scales_id; + ou_job[0].n_row_tiles = n_row_tiles; + ou_job[0].n_col_tiles = last_cols; + ou_job[0].n_row_tiles_g_br = n_row_tiles_g_br; + ou_job[0].n_tiles_per_bc = n_tiles_per_bc; + ou_job[0].DV = DV; + hmx_queue_push(hmx_q, hmx_queue_make_desc(hmx_fa_o_update_worker, &ou_job[0])); + + // Overlapped: run HVX build diag inv L while HMX is busy executing the update + htp_trace_event_start(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); + fa_build_d_diag_inv_l(&factx, n_row_tiles, n_row_tiles_g_br); + htp_trace_event_stop(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); hmx_queue_pop(hmx_q); hex_swap_ptr((void **) &o_tile_curr, (void **) &o_tile_prev); @@ -1892,12 +2174,12 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { // Wait for current K DMA and interleave void * curr_k = dma_queue_pop(dma).dst; - fa_phase_k_interleave(&factx, kv_rows, k_src_stride, curr_k, kv_start); + fa_phase_k_interleave(&factx, kv_rows, k_src_stride, curr_k, kv_start, 0); { qk_job.q_tiles = factx.vtcm_q_tiles; - qk_job.k_tiles = factx.vtcm_k_tiles; - qk_job.s_tiles = factx.vtcm_s_tiles; + qk_job.k_tiles = factx.vtcm_k_tiles[0]; + qk_job.s_tiles = factx.vtcm_s_tiles[0]; qk_job.n_row_tiles = n_row_tiles; qk_job.n_col_tiles = n_col_tiles; qk_job.n_dot_tiles = (size_t) (DK / 32); @@ -1948,7 +2230,7 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { { ou_job.o_curr = o_tile_curr; ou_job.o_prev = o_tile_prev; - ou_job.p_tiles = factx.vtcm_p_tiles; + ou_job.p_tiles = factx.vtcm_p_tiles[0]; ou_job.v_tiles = factx.vtcm_v_tiles[0]; ou_job.d_tiles = factx.vtcm_d_tiles; ou_job.hmx_scales = factx.vtcm_hmx_scales_id; @@ -1959,6 +2241,12 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { ou_job.DV = DV; hmx_queue_push(ctx->hmx_queue, hmx_queue_make_desc(hmx_fa_o_update_worker, &ou_job)); + if (kv_blk + 1 == factx.n_kv_blocks) { + // Overlapped: run HVX build diag inv L while HMX is busy executing the update + htp_trace_event_start(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); + fa_build_d_diag_inv_l(&factx, n_row_tiles, n_row_tiles_g_br); + htp_trace_event_stop(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); + } hmx_queue_pop(ctx->hmx_queue); hex_swap_ptr((void **) &o_tile_curr, (void **) &o_tile_prev); @@ -1968,15 +2256,63 @@ int hmx_flash_attn_ext(struct htp_ops_context * octx) { } } + // Enqueue DMAs for the next iteration early so they overlap with O-PROC + uint32_t next_kv_head = kv_head + 1; + uint32_t next_q_start = q_start; + uint32_t next_ib3 = ib3; + if (next_kv_head >= n_kv_heads) { + next_kv_head = 0; + next_q_start = q_start + Br; + if (next_q_start >= neq1) { + next_q_start = 0; + next_ib3 = ib3 + 1; + } + } + bool has_next = (next_ib3 < neq3); + + if (has_next) { + const uint32_t next_n_rows_q = hex_smin(Br, neq1 - next_q_start); + const uint8_t * next_q_ptr = (const uint8_t *) q->data + next_q_start * q->nb[1] + (next_kv_head * factx.G) * q->nb[2] + next_ib3 * q->nb[3]; + const size_t next_q_row_bytes = q_transposed ? next_n_rows_q * q_row_bytes_trans_factor : q_row_bytes_untransposed; + const size_t next_n_rows = q_transposed ? factx.G : next_n_rows_q; + dma_queue_push(dma, dma_make_ptr(factx.vtcm_q_dma, next_q_ptr), next_q_row_bytes, hex_smax(q_src_stride, next_q_row_bytes), next_q_row_bytes, next_n_rows); + + if (factx.n_kv_blocks > 0) { + const uint32_t next_ik2 = next_kv_head; + const uint32_t next_iv2 = next_kv_head; + uint32_t next_ik3 = ik3; + uint32_t next_iv3 = iv3; + if (next_ib3 != ib3) { + next_ik3 = fastdiv(next_ib3, &kparams->broadcast_rk3); + next_iv3 = fastdiv(next_ib3, &kparams->broadcast_rv3); + } + + const uint8_t * next_k_src = (const uint8_t *) k->data + next_ik2 * k->nb[2] + next_ik3 * k->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx.vtcm_k_fp16[0], next_k_src), size_k_row_padded, k->nb[1], size_k_row, kv_rows0); + + const uint8_t * next_v_src = (const uint8_t *) v->data + next_iv2 * v->nb[2] + next_iv3 * v->nb[3]; + dma_queue_push(dma, dma_make_ptr(factx.vtcm_v_fp16[0], next_v_src), size_v_row_padded, v->nb[1], size_v_row, kv_rows0); + + if (factx.pipeline && mask) { + uint32_t next_im3 = im3; + if (next_ib3 != ib3) { + next_im3 = fastmodulo(next_ib3, mask->ne[3], &factx.src3_div3); + } + if (__builtin_expect(factx.mask_broadcast, true)) { + const uint8_t * ms_src = (const uint8_t *) mask->data + next_q_start * mask->nb[1] + next_im3 * mask->nb[3] + 0; + dma_cache_push(dma, &factx.m_cache, ms_src, m_line_bytes, mask->nb[1], kv_rows0 * sizeof(__fp16), next_n_rows_q); + } else { + fa_push_mask_dma_gqa(dma, mask, next_q_start, next_im3, 0, next_kv_head, G, m_line_bytes, kv_rows0, next_n_rows_q, &factx); + } + } + } + } + // ---- Final normalization ---- { - htp_trace_event_start(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); - fa_build_d_diag_inv_l(&factx, n_row_tiles, n_row_tiles_g_br); - htp_trace_event_stop(tr_hvx, HTP_TRACE_EVT_HVX_O_PROC, (uint16_t) q_start); - on_job.o_curr = o_tile_curr; on_job.o_prev = o_tile_prev; - on_job.d_tiles = factx.vtcm_d_tiles; + on_job.d_tiles = factx.vtcm_d_inv_l; on_job.hmx_scales = factx.vtcm_hmx_scales_id; on_job.n_row_tiles = n_row_tiles; on_job.n_row_tiles_g_br = n_row_tiles_g_br; diff --git a/ggml/src/ggml-hexagon/htp/flash-attn-ops.h b/ggml/src/ggml-hexagon/htp/flash-attn-ops.h index 16822f22b..efe5ce548 100644 --- a/ggml/src/ggml-hexagon/htp/flash-attn-ops.h +++ b/ggml/src/ggml-hexagon/htp/flash-attn-ops.h @@ -101,14 +101,16 @@ static_assert(sizeof(struct htp_fa_kernel_params) <= 128, "htp_fa_kernel_params struct hmx_fa_vtcm_layout { // Byte offsets from vtcm_base for each region. size_t off_q_tiles; + size_t off_q_dma; size_t off_o_tiles[2]; size_t off_k_fp16[2]; size_t off_v_fp16[2]; - size_t off_k_tiles; - size_t off_v_tiles[2]; // [1] allocated only when pipeline, else 0 - size_t off_s_tiles; - size_t off_p_tiles; + size_t off_k_tiles[2]; + size_t off_v_tiles[2]; + size_t off_s_tiles[2]; + size_t off_p_tiles[2]; size_t off_d_tiles; + size_t off_d_inv_l; size_t off_m_vec; size_t off_l_vec; size_t off_s_rowmax; @@ -140,7 +142,7 @@ struct hmx_fa_vtcm_layout { static inline void hmx_fa_vtcm_layout_build(struct hmx_fa_vtcm_layout * L, size_t gqa_factor, size_t DK, size_t DV, - size_t Br, size_t Bc, size_t n_threads, bool pipeline) { + size_t Br, size_t Bc, size_t n_threads, bool pipeline, bool is_q_fp32) { const size_t g_br = hex_align_up(gqa_factor * Br, HMX_FP16_TILE_N_ROWS); const size_t q_tile_size = hex_align_up(g_br * DK * sizeof(__fp16), HTP_FA_HMX_TILE_SIZE); const size_t o_tile_size = hex_align_up(g_br * DV * sizeof(__fp16), HTP_FA_HMX_TILE_SIZE); @@ -149,6 +151,7 @@ static inline void hmx_fa_vtcm_layout_build(struct hmx_fa_vtcm_layout * L, const size_t s_tile_size = hex_align_up(g_br * Bc * sizeof(__fp16), HTP_FA_HMX_TILE_SIZE); const size_t d_tile_size = hex_align_up(g_br * g_br * sizeof(__fp16), HTP_FA_HMX_TILE_SIZE); + const size_t q_dma_size = hex_align_up(g_br * DK * (is_q_fp32 ? sizeof(float) : sizeof(__fp16)), 128); const size_t k_dma_size = hex_align_up(Bc * hex_round_up(DK * sizeof(__fp16), 128), 128); const size_t v_dma_size = hex_align_up(Bc * hex_round_up(DV * sizeof(__fp16), 128), 128); const size_t col_vec_size = hex_align_up(g_br * sizeof(float), 256); @@ -160,27 +163,47 @@ static inline void hmx_fa_vtcm_layout_build(struct hmx_fa_vtcm_layout * L, size_t off = 0; - // Section 1: HMX Tiled Buffers (FA_HMX_TILE_SIZE = 2KB Aligned) + // Group A (Part 1 - HMX Tiled buffers) VTCM_LAYOUT_ALLOC(off, off_q_tiles, q_tile_size); VTCM_LAYOUT_ALLOC(off, off_o_tiles[0], o_tile_size); VTCM_LAYOUT_ALLOC(off, off_o_tiles[1], o_tile_size); - VTCM_LAYOUT_ALLOC(off, off_k_tiles, k_tile_size); - VTCM_LAYOUT_ALLOC(off, off_v_tiles[0], v_tile_size); - VTCM_LAYOUT_ALLOC_OPTIONAL(off, off_v_tiles[1], v_tile_size, pipeline); - VTCM_LAYOUT_ALLOC(off, off_s_tiles, s_tile_size); - VTCM_LAYOUT_ALLOC(off, off_p_tiles, s_tile_size); VTCM_LAYOUT_ALLOC(off, off_d_tiles, d_tile_size); + VTCM_LAYOUT_ALLOC(off, off_d_inv_l, d_tile_size); - // Section 2: HVX/DMA flat and vector buffers (128B / 256B Aligned) + // Group B & C share start offset (Group B tiles must be 2KB aligned) + size_t off_group_b_c = hex_align_up(off, HTP_FA_HMX_TILE_SIZE); + + // Group B: Compute-only buffers + size_t off_group_b = off_group_b_c; + VTCM_LAYOUT_ALLOC(off_group_b, off_k_tiles[0], k_tile_size); + VTCM_LAYOUT_ALLOC_OPTIONAL(off_group_b, off_k_tiles[1], k_tile_size, pipeline); + VTCM_LAYOUT_ALLOC(off_group_b, off_v_tiles[0], v_tile_size); + VTCM_LAYOUT_ALLOC_OPTIONAL(off_group_b, off_v_tiles[1], v_tile_size, pipeline); + VTCM_LAYOUT_ALLOC(off_group_b, off_s_tiles[0], s_tile_size); + VTCM_LAYOUT_ALLOC_OPTIONAL(off_group_b, off_s_tiles[1], s_tile_size, pipeline); + VTCM_LAYOUT_ALLOC(off_group_b, off_p_tiles[0], s_tile_size); + VTCM_LAYOUT_ALLOC_OPTIONAL(off_group_b, off_p_tiles[1], s_tile_size, pipeline); + VTCM_LAYOUT_ALLOC(off_group_b, off_s_rowmax, col_vec_size); + VTCM_LAYOUT_ALLOC(off_group_b, off_p_rowsum, col_vec_size); + VTCM_LAYOUT_ALLOC(off_group_b, off_row_bufs, row_vec_size * 2 * n_threads); + + const size_t group_b_size = off_group_b - off_group_b_c; + + // Group C: Q fetch DMA buffer + size_t off_group_c = off_group_b_c; + VTCM_LAYOUT_ALLOC(off_group_c, off_q_dma, q_dma_size); + + const size_t group_c_size = off_group_c - off_group_b_c; + + off = off_group_b_c + hex_smax(group_b_size, group_c_size); + + // Group A (Part 2 - remaining non-HMX buffers) VTCM_LAYOUT_ALLOC(off, off_k_fp16[0], k_dma_size); VTCM_LAYOUT_ALLOC(off, off_k_fp16[1], k_dma_size); VTCM_LAYOUT_ALLOC(off, off_v_fp16[0], v_dma_size); VTCM_LAYOUT_ALLOC(off, off_v_fp16[1], v_dma_size); VTCM_LAYOUT_ALLOC(off, off_m_vec, col_vec_size); VTCM_LAYOUT_ALLOC(off, off_l_vec, col_vec_size); - VTCM_LAYOUT_ALLOC(off, off_s_rowmax, col_vec_size); - VTCM_LAYOUT_ALLOC(off, off_p_rowsum, col_vec_size); - VTCM_LAYOUT_ALLOC(off, off_row_bufs, row_vec_size * 2 * n_threads); VTCM_LAYOUT_ALLOC(off, off_hmx_scales_id, 256); VTCM_LAYOUT_ALLOC(off, off_hmx_scales_qk, 256); VTCM_LAYOUT_ALLOC(off, off_mask_buf, m_buf_size); @@ -200,9 +223,9 @@ static inline void hmx_fa_vtcm_layout_build(struct hmx_fa_vtcm_layout * L, } // Exact VTCM usage for a given (gqa_factor, DK, DV, Br, Bc) configuration. -static inline size_t hmx_fa_compute_vtcm_usage(size_t gqa_factor, size_t DK, size_t DV, size_t Br, size_t Bc, size_t n_threads, bool pipeline) { +static inline size_t hmx_fa_compute_vtcm_usage(size_t gqa_factor, size_t DK, size_t DV, size_t Br, size_t Bc, size_t n_threads, bool pipeline, bool is_q_fp32) { struct hmx_fa_vtcm_layout L; - hmx_fa_vtcm_layout_build(&L, gqa_factor, DK, DV, Br, Bc, n_threads, pipeline); + hmx_fa_vtcm_layout_build(&L, gqa_factor, DK, DV, Br, Bc, n_threads, pipeline, is_q_fp32); return L.total_bytes; } @@ -239,7 +262,8 @@ static inline int hmx_fa_find_chunk_size(size_t * Br_out, size_t qo_len, size_t kv_len, size_t vtcm_budget, - size_t n_threads) { + size_t n_threads, + bool is_q_fp32) { const size_t T = HMX_FP16_TILE_N_ROWS; // 32 const size_t br_unit = hmx_ceil_div(T, gqa_factor); const size_t bc_unit = HMX_FP16_TILE_N_COLS * 2; // 64 @@ -253,8 +277,9 @@ static inline int hmx_fa_find_chunk_size(size_t * Br_out, const size_t Bc_limit = can_pipeline ? hex_align_down(kv_len / FA_MIN_KV_BLOCKS, bc_unit) : (kv_len >= bc_unit ? hex_align_down(kv_len, bc_unit) : bc_unit); // Cost coefficients calibrated from profiling - const size_t c_q_fixed = 1400; // per-Q-block: q_load + epilogue o_update + o_norm + o_store - const size_t c_iter_fixed = 200; // per-KV-iter: HMX queue push/pop + DMA pop + barriers + const size_t c_q_fixed = 800; // per-Q-block: q_load + epilogue o_update + o_norm + o_store + const size_t c_iter_base = 200; // per-KV-iter base (HMX dot/update + DMA) + const size_t c_softmax = 600; // per 64-row vector chunk on HVX size_t best_cost = SIZE_MAX, best_mn = 0; size_t best_Br = 0, best_Bc = 0; @@ -262,13 +287,20 @@ static inline int hmx_fa_find_chunk_size(size_t * Br_out, for (size_t Br = Br_max; Br >= br_unit; Br -= br_unit) { // Try all Bc candidates from Bc_limit down to bc_unit for (size_t Bc = Bc_limit; Bc >= bc_unit; Bc -= bc_unit) { - size_t vtcm_needed = hmx_fa_compute_vtcm_usage(gqa_factor, DK, DV, Br, Bc, n_threads, can_pipeline); + size_t vtcm_needed = hmx_fa_compute_vtcm_usage(gqa_factor, DK, DV, Br, Bc, n_threads, can_pipeline, is_q_fp32); if (vtcm_needed <= vtcm_budget) { // This Bc fits for this Br! - const size_t q_blocks = (qo_len + Br - 1) / Br; - const size_t kv_blocks = (kv_len + Bc - 1) / Bc; - const size_t cost = q_blocks * (c_q_fixed + kv_blocks * c_iter_fixed); - const size_t mn = Br * Bc; + const size_t q_blocks = (qo_len + Br - 1) / Br; + const size_t kv_blocks = (kv_len + Bc - 1) / Bc; + const size_t actual_threads = (kv_blocks >= 3 && n_threads >= 2) ? n_threads : 1; + const size_t n_rows_g = Br * gqa_factor; + const size_t n_row_vec_cnt = (n_rows_g + 63) / 64; + const size_t n_use = n_row_vec_cnt < actual_threads ? n_row_vec_cnt : actual_threads; + const size_t vecs_per_t = n_use > 0 ? (n_row_vec_cnt + n_use - 1) / n_use : 1; + + const size_t c_iter_actual = c_iter_base + c_softmax * vecs_per_t; + const size_t cost = q_blocks * (c_q_fixed + kv_blocks * c_iter_actual); + const size_t mn = Br * Bc; if (cost < best_cost || (cost == best_cost && mn > best_mn)) { best_cost = cost; diff --git a/ggml/src/ggml-hexagon/htp/hmx-mm-kernels-tiled.h b/ggml/src/ggml-hexagon/htp/hmx-mm-kernels-tiled.h index a4efb1b00..0011abba5 100644 --- a/ggml/src/ggml-hexagon/htp/hmx-mm-kernels-tiled.h +++ b/ggml/src/ggml-hexagon/htp/hmx-mm-kernels-tiled.h @@ -767,23 +767,25 @@ static void core_mma_chunk_fp16(__fp16 *restrict c, const __fp16 *restrict a, co // output : fp16 -> f32p -static void transfer_output_chunk_fp16_to_fp32( +static void transfer_output_chunk_fp16_to_fp32_col_chunk( float *restrict dst, const float *restrict src2, const __fp16 *restrict vtcm_src, uint32_t start_row, uint32_t n_rows, - uint32_t n_cols, + uint32_t c_len, + uint32_t total_n_cols, uint32_t dst_stride, uint32_t src2_stride, uint32_t dst_cols ) { - assert(n_cols % HTP_MM_HMX_TILE_N_COLS == 0); - const size_t tile_row_stride = (n_cols / HTP_MM_HMX_TILE_N_COLS) * HTP_MM_HMX_TILE_N_ELMS; + assert(c_len % HTP_MM_HMX_TILE_N_COLS == 0); + assert(total_n_cols % HTP_MM_HMX_TILE_N_COLS == 0); + const size_t tile_row_stride = (total_n_cols / HTP_MM_HMX_TILE_N_COLS) * HTP_MM_HMX_TILE_N_ELMS; const HVX_Vector one = hvx_vec_splat_f16(1.0); - const size_t limit_c = hex_smin(n_cols, dst_cols); + const size_t limit_c = hex_smin(c_len, dst_cols); const size_t limit_c_aligned = (limit_c & ~31); for (size_t r = 0; r < n_rows; r += 2) { @@ -848,6 +850,22 @@ static void transfer_output_chunk_fp16_to_fp32( } } +static inline void transfer_output_chunk_fp16_to_fp32( + float *restrict dst, + const float *restrict src2, + const __fp16 *restrict vtcm_src, + uint32_t start_row, + uint32_t n_rows, + uint32_t n_cols, + uint32_t dst_stride, + uint32_t src2_stride, + uint32_t dst_cols +) { + transfer_output_chunk_fp16_to_fp32_col_chunk( + dst, src2, vtcm_src, start_row, n_rows, n_cols, n_cols, dst_stride, src2_stride, dst_cols + ); +} + typedef struct { const __fp16 *vtcm_src; float *dst; diff --git a/ggml/src/ggml-hexagon/htp/htp-ctx.h b/ggml/src/ggml-hexagon/htp/htp-ctx.h index c4b60eb26..97b8c7f29 100644 --- a/ggml/src/ggml-hexagon/htp/htp-ctx.h +++ b/ggml/src/ggml-hexagon/htp/htp-ctx.h @@ -19,6 +19,8 @@ #endif #define HTP_MAX_MMAPS 16 +#define HTP_MAX_DIRTY_RANGES 16 + // Memory mapping struct htp_mmap { uint64_t size; @@ -95,7 +97,11 @@ struct htp_context { atomic_bool vtcm_needs_release; uint64_t max_vmem; - uint32_t dirty_map[HTP_OP_MAX_TENSORS / 32]; + struct htp_dirty_range { + uint32_t start; + uint32_t end; + uint32_t bi; + } dirty_ranges[HTP_MAX_DIRTY_RANGES]; // Persistent DDR scratchpad for MUL_MAT_ID mappings void * ddr_spad_base; diff --git a/ggml/src/ggml-hexagon/htp/htp-ops.h b/ggml/src/ggml-hexagon/htp/htp-ops.h index 262d6110f..cad9a4f54 100644 --- a/ggml/src/ggml-hexagon/htp/htp-ops.h +++ b/ggml/src/ggml-hexagon/htp/htp-ops.h @@ -123,7 +123,7 @@ enum htp_tensor_flags { // Tensor descriptor struct htp_tensor { uint32_t data; // Buffer offset in the messages, and data pointer on the NPU - uint32_t alias; // Index of the canonical tensor for this memory buffer + uint32_t reserved; // Reserved for alignment padding (must be multiple of 8) uint32_t size; // Data size in bytes uint32_t flags; // Buffer / tensor flags uint32_t type; // Data type @@ -173,6 +173,7 @@ enum htp_trace_event_id { HTP_TRACE_EVT_DMA = 0, HTP_TRACE_EVT_L2FLUSH = 1, HTP_TRACE_EVT_INIT = 2, + HTP_TRACE_EVT_BUFF = 3, HTP_TRACE_EVT_HVX_COMP = 20, HTP_TRACE_EVT_HVX_A_QUANT = 21, @@ -225,7 +226,10 @@ struct htp_opbatch_rsp { uint32_t n_tensors; // Number of tensors uint32_t n_ops; // Number of op profile descriptors uint32_t n_traces[HTP_MAX_NTHREADS + 1]; - uint8_t pad[8]; // align to 8 bytes + uint32_t usecs; // Number of usec + uint32_t pad; // align to 8 bytes + uint64_t cycles_start; // Start cycle counter + uint64_t cycles_stop; // Stop cycle counter // struct htp_prof_desc profs[]; -- dspqueue buf 0 }; diff --git a/ggml/src/ggml-hexagon/htp/htp-tensor.c b/ggml/src/ggml-hexagon/htp/htp-tensor.c index 3f028a7e4..39436e26d 100644 --- a/ggml/src/ggml-hexagon/htp/htp-tensor.c +++ b/ggml/src/ggml-hexagon/htp/htp-tensor.c @@ -2,6 +2,7 @@ #include #include +#include #include "hex-common.h" #include "hex-utils.h" @@ -10,84 +11,6 @@ #include "htp-ctx.h" #include "work-queue.h" -struct l2flush_task { - struct htp_thread_trace * trace; - uint32_t start; - uint32_t end; - uint32_t chunk_size; - uint32_t ti; -}; - -static void l2flush_thread_worker(unsigned int n, unsigned int i, void * data) { - struct l2flush_task * task = (struct l2flush_task *) data; - const uint32_t start = task->start; - const uint32_t end = task->end; - const uint32_t ti = task->ti; - const uint32_t chunk_size = task->chunk_size; - - const uint32_t thread_s = start + i * chunk_size; - if (thread_s >= end) { - return; - } - uint32_t thread_e = thread_s + chunk_size; - if (thread_e > end) { - thread_e = end; - } - - struct htp_thread_trace * tr = &task->trace[i]; - htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, ti); - hex_l2flush((void *) (uintptr_t) thread_s, thread_e - thread_s); - htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, ti); -} - -static void flush_all_dcache(struct htp_context * ctx) { - struct htp_thread_trace * tr = &ctx->trace[0]; - htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, 0); - qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); - hex_l2fetch_block(ctx, ctx->footprint); - htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, 0); - bitmap_reset(ctx->dirty_map, HTP_OP_MAX_TENSORS); -} - -static void flush_tensor_range(struct htp_context * ctx, const struct htp_tensor * t) { - struct htp_thread_trace * tr = &ctx->trace[0]; - - if (t->size > HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1) { - struct l2flush_task task; - task.start = hex_align_down((size_t) t->data, HEX_L2_LINE_SIZE); - task.end = hex_align_up((size_t) t->data + t->size, HEX_L2_LINE_SIZE); - task.ti = t->ti; - task.trace = ctx->trace; - - const uint32_t total_size = task.end - task.start; - const uint32_t n_blocks = (total_size + HEX_L2_BLOCK_SIZE - 1) / HEX_L2_BLOCK_SIZE; - const uint32_t blocks_per_thread = fastdiv(n_blocks + ctx->n_threads - 1, &ctx->n_threads_div); - task.chunk_size = blocks_per_thread * HEX_L2_BLOCK_SIZE; - - work_queue_run(ctx->work_queue, l2flush_thread_worker, &task, ctx->n_threads); - } else { - htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - hex_l2flush((void *) t->data, t->size); - htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - } - - htp_tensor_make_clean(t, ctx->dirty_map); -} - -void htp_tensor_flush(struct htp_context * ctx, const struct htp_tensor * t) { - if (!bitmap_test(ctx->dirty_map, t->ti)) { - return; - } - - if (t->size > HEX_L2_FLUSH_ALL_THRESHOLD) { - flush_all_dcache(ctx); - return; - } - - flush_tensor_range(ctx, t); -} - -// One dirty tensor's line-aligned range, placed in the flattened global block space. struct l2flush_range { uint32_t start; // line-aligned start address uint32_t end; // line-aligned end address @@ -103,9 +26,18 @@ struct l2flush_multi_task { uint32_t blocks_per_thread; }; +static void flush_all_dcache(struct htp_context * ctx) { + struct htp_thread_trace * tr = &ctx->trace[0]; + htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, 0); + qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); + hex_l2fetch_block(ctx, ctx->footprint); + htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, 0); + memset(ctx->dirty_ranges, 0, sizeof(ctx->dirty_ranges)); +} + static void l2flush_multi_worker(unsigned int n, unsigned int i, void * data) { - (void) n; struct l2flush_multi_task * task = (struct l2flush_multi_task *) data; + (void) n; const uint32_t gb_first = i * task->blocks_per_thread; uint32_t gb_last = gb_first + task->blocks_per_thread; @@ -141,11 +73,177 @@ static void l2flush_multi_worker(unsigned int n, unsigned int i, void * data) { htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, gb_first); } -void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { - uint64_t total_dirty = 0; +void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { + const struct htp_tensor * pending[HTP_OP_MAX_OUTPUTS]; + uint32_t n_pending = 0; + for (uint32_t i = 0; i < n; i++) { const struct htp_tensor * t = tensors[i]; - if (t && bitmap_test(ctx->dirty_map, t->ti)) { + if (!t) continue; + + uint32_t t_start = t->data; + uint32_t t_end = t_start + t->size; + + bool merged = false; + for (uint32_t j = 0; j < HTP_MAX_DIRTY_RANGES; j++) { + struct htp_dirty_range * r = &ctx->dirty_ranges[j]; + if (!r->start) continue; + + if (r->start <= t_end && t_start <= r->end) { + uint32_t new_start = (t_start < r->start) ? t_start : r->start; + uint32_t new_end = (t_end > r->end) ? t_end : r->end; + r->start = new_start; + r->end = new_end; + merged = true; + } + } + + if (!merged) { + pending[n_pending++] = t; + } + } + + if (n_pending == 0) { + return; + } + + uint32_t empty_indices[HTP_MAX_DIRTY_RANGES]; + uint32_t active_indices[HTP_MAX_DIRTY_RANGES]; + uint32_t n_active = 0; + uint32_t n_empty = 0; + for (uint32_t j = 0; j < HTP_MAX_DIRTY_RANGES; j++) { + if (ctx->dirty_ranges[j].start) { + active_indices[n_active++] = j; + } else { + empty_indices[n_empty++] = j; + } + } + + if (n_pending <= n_empty) { + for (uint32_t i = 0; i < n_pending; i++) { + uint32_t idx = empty_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + r->start = pending[i]->data; + r->end = pending[i]->data + pending[i]->size; + r->bi = pending[i]->bi; + } + return; + } + + uint32_t n_evict = n_pending - n_empty; + uint32_t total_evict_size = 0; + for (uint32_t i = 0; i < n_evict; i++) { + uint32_t idx = active_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + total_evict_size += r->end - r->start; + } + + if (total_evict_size > HEX_L2_FLUSH_ALL_THRESHOLD) { + flush_all_dcache(ctx); + for (uint32_t i = 0; i < n_pending; i++) { + struct htp_dirty_range * r = &ctx->dirty_ranges[i]; + r->start = pending[i]->data; + r->end = pending[i]->data + pending[i]->size; + r->bi = pending[i]->bi; + } + return; + } + + if (total_evict_size > HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1 && n_evict <= HTP_OP_MAX_INPUTS) { + struct l2flush_multi_task task; + task.trace = ctx->trace; + task.n_ranges = n_evict; + + uint32_t block_acc = 0; + for (uint32_t i = 0; i < n_evict; i++) { + uint32_t idx = active_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + + struct l2flush_range * rg = &task.ranges[i]; + rg->start = hex_align_down((size_t) r->start, HEX_L2_LINE_SIZE); + rg->end = hex_align_up((size_t) r->end, HEX_L2_LINE_SIZE); + rg->block_first = block_acc; + rg->n_blocks = (rg->end - rg->start + HEX_L2_BLOCK_SIZE - 1) / HEX_L2_BLOCK_SIZE; + block_acc += rg->n_blocks; + } + + task.total_blocks = block_acc; + task.blocks_per_thread = fastdiv(block_acc + ctx->n_threads - 1, &ctx->n_threads_div); + + work_queue_run(ctx->work_queue, l2flush_multi_worker, &task, ctx->n_threads); + } else { + struct htp_thread_trace * tr = &ctx->trace[0]; + htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, 0); + for (uint32_t i = 0; i < n_evict; i++) { + uint32_t idx = active_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + uint32_t size = r->end - r->start; + hex_l2flush((void *) (uintptr_t) r->start, size); + } + htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, 0); + } + + for (uint32_t i = 0; i < n_evict; i++) { + uint32_t idx = active_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + r->start = pending[i]->data; + r->end = pending[i]->data + pending[i]->size; + r->bi = pending[i]->bi; + } + + for (uint32_t i = 0; i < n_empty; i++) { + uint32_t idx = empty_indices[i]; + struct htp_dirty_range * r = &ctx->dirty_ranges[idx]; + r->start = pending[n_evict + i]->data; + r->end = pending[n_evict + i]->data + pending[n_evict + i]->size; + r->bi = pending[n_evict + i]->bi; + } +} + +static void make_tensor_clean(struct htp_context * ctx, const struct htp_tensor * t) { + uint32_t t_start = t->data; + uint32_t t_end = t_start + t->size; + + for (uint32_t i = 0; i < HTP_MAX_DIRTY_RANGES; i++) { + struct htp_dirty_range * r = &ctx->dirty_ranges[i]; + if (!r->start) continue; + + if (r->start < t_end && t_start < r->end) { + if (t_start <= r->start && r->end <= t_end) { + r->start = 0; + } else if (t_start <= r->start) { + r->start = t_end; + } else if (r->end <= t_end) { + r->end = t_start; + } + } + } +} + +static inline bool is_tensor_dirty(struct htp_context * ctx, const struct htp_tensor * t) { + uint32_t t_start = t->data; + uint32_t t_end = t_start + t->size; + + for (uint32_t i = 0; i < HTP_MAX_DIRTY_RANGES; i++) { + struct htp_dirty_range * r = &ctx->dirty_ranges[i]; + if (!r->start) continue; + + if (r->start < t_end && t_start < r->end) { + return true; + } + } + return false; +} + +void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n) { + const struct htp_tensor * dirty_tensors[HTP_OP_MAX_INPUTS]; + uint32_t n_dirty = 0; + uint64_t total_dirty = 0; + + for (uint32_t i = 0; i < n; i++) { + const struct htp_tensor * t = tensors[i]; + if (t && (t->flags & HTP_TENSOR_COMPUTE) && is_tensor_dirty(ctx, t)) { + dirty_tensors[n_dirty++] = t; total_dirty += t->size; } } @@ -159,21 +257,15 @@ void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * co return; } - // Aggregate is small enough to walk. Thread it across all dirty ranges at once - // when it is worth the dispatch, otherwise flush sequentially. - if (total_dirty > HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1) { + if (total_dirty >= HEX_L2_FLUSH_WQ_THRESHOLD && ctx->n_threads > 1) { struct l2flush_multi_task task; task.trace = ctx->trace; task.n_ranges = 0; uint32_t block_acc = 0; - for (uint32_t i = 0; i < n; i++) { - const struct htp_tensor * t = tensors[i]; - if (!t || !bitmap_test(ctx->dirty_map, t->ti)) { - continue; - } - // Clear as we go: dedups a tensor passed as multiple srcs (e.g. mul(x,x)). - htp_tensor_make_clean(t, ctx->dirty_map); + for (uint32_t i = 0; i < n_dirty; i++) { + const struct htp_tensor * t = dirty_tensors[i]; + make_tensor_clean(ctx, t); struct l2flush_range * rg = &task.ranges[task.n_ranges++]; rg->start = hex_align_down((size_t) t->data, HEX_L2_LINE_SIZE); @@ -191,14 +283,11 @@ void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * co } struct htp_thread_trace * tr = &ctx->trace[0]; - for (uint32_t i = 0; i < n; i++) { - const struct htp_tensor * t = tensors[i]; - if (!t || !bitmap_test(ctx->dirty_map, t->ti)) { - continue; - } + for (uint32_t i = 0; i < n_dirty; i++) { + const struct htp_tensor * t = dirty_tensors[i]; htp_trace_event_start(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - hex_l2flush((void *) t->data, t->size); + hex_l2flush((void *) (uintptr_t) t->data, t->size); htp_trace_event_stop(tr, HTP_TRACE_EVT_L2FLUSH, t->ti); - htp_tensor_make_clean(t, ctx->dirty_map); + make_tensor_clean(ctx, t); } } diff --git a/ggml/src/ggml-hexagon/htp/htp-tensor.h b/ggml/src/ggml-hexagon/htp/htp-tensor.h index e503bb90f..2c3fc54c7 100644 --- a/ggml/src/ggml-hexagon/htp/htp-tensor.h +++ b/ggml/src/ggml-hexagon/htp/htp-tensor.h @@ -5,10 +5,6 @@ #include "htp-ops.h" #include "hex-bitmap.h" -static inline struct htp_tensor * htp_tensor_alias(const struct htp_tensor * t) { - return (struct htp_tensor *) (uintptr_t) t->alias; -} - static inline void * htp_tensor_data(const struct htp_tensor * t) { return (void *) (uintptr_t) t->data; } @@ -17,20 +13,8 @@ static inline uint32_t * htp_tensor_flags(const struct htp_tensor * t) { return (uint32_t *) &t->flags; } -static inline void htp_tensor_make_dirty(const struct htp_tensor * t, uint32_t * dirty_map) { - struct htp_tensor * curr = (struct htp_tensor *) t; - do { - bitmap_set(dirty_map, curr->ti); - curr = htp_tensor_alias(curr); - } while (curr != t); -} - -static inline void htp_tensor_make_clean(const struct htp_tensor * t, uint32_t * dirty_map) { - bitmap_clear(dirty_map, t->ti); -} - struct htp_context; -void htp_tensor_flush(struct htp_context * ctx, const struct htp_tensor * t); void htp_tensor_flush_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n); +void htp_tensor_dirty_all(struct htp_context * ctx, const struct htp_tensor * const * tensors, uint32_t n); #endif // HTP_TENSOR_H diff --git a/ggml/src/ggml-hexagon/htp/hvx-fa-kernels.h b/ggml/src/ggml-hexagon/htp/hvx-fa-kernels.h index c05bd0b85..5b18f163c 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-fa-kernels.h +++ b/ggml/src/ggml-hexagon/htp/hvx-fa-kernels.h @@ -208,6 +208,77 @@ static inline void hvx_mad_f32_f16_aa_rx2(float * restrict y, const void * restr } } } +static inline void hvx_mad_f32_f16_aa_vec(float * restrict y, const void * restrict x, HVX_Vector S0, uint32_t n) { + const HVX_Vector * restrict vx0 = (const HVX_Vector *) x; + + HVX_VectorPair * restrict vy_p = (HVX_VectorPair *) y; + HVX_Vector * restrict vy = (HVX_Vector *) y; + + uint32_t nvec = n / VLEN_FP16; // num full fp16 hvx vectors + uint32_t nloe = n % VLEN_FP16; // leftover elements + + uint32_t i = 0; + + #pragma unroll(2) + for (i = 0; i < nvec; ++i) { + vy_p[i] = hvx_vec_mpyacc_f32_f16(vy_p[i], Q6_Vh_vshuff_Vh(vx0[i]), S0); + } + + if (nloe) { + HVX_VectorPair xy_p = vy_p[i]; + xy_p = hvx_vec_mpyacc_f32_f16(xy_p, Q6_Vh_vshuff_Vh(vx0[i]), S0); + + HVX_Vector xy = Q6_V_lo_W(xy_p); + i = 2 * i; // index for vy + + if (nloe >= VLEN_FP32) { + vy[i] = xy; + nloe -= VLEN_FP32; ++i; xy = Q6_V_hi_W(xy_p); + } + + if (nloe) { + hvx_vec_store_a(&vy[i], nloe * 4, xy); + } + } +} + +static inline void hvx_mad_f32_f16_aa_rx2_vec(float * restrict y, const void * restrict x0, const void * restrict x1, + HVX_Vector S0, HVX_Vector S1, uint32_t n) { + const HVX_Vector * restrict vx0 = (const HVX_Vector *) x0; + const HVX_Vector * restrict vx1 = (const HVX_Vector *) x1; + + HVX_VectorPair * restrict vy_p = (HVX_VectorPair *) y; + HVX_Vector * restrict vy = (HVX_Vector *) y; + + uint32_t nvec = n / VLEN_FP16; // num full fp16 hvx vectors + uint32_t nloe = n % VLEN_FP16; // leftover elements + + uint32_t i = 0; + + #pragma unroll(2) + for (i = 0; i < nvec; ++i) { + vy_p[i] = hvx_vec_mpyacc_f32_f16(vy_p[i], Q6_Vh_vshuff_Vh(vx0[i]), S0); + vy_p[i] = hvx_vec_mpyacc_f32_f16(vy_p[i], Q6_Vh_vshuff_Vh(vx1[i]), S1); + } + + if (nloe) { + HVX_VectorPair xy_p = vy_p[i]; + xy_p = hvx_vec_mpyacc_f32_f16(xy_p, Q6_Vh_vshuff_Vh(vx0[i]), S0); + xy_p = hvx_vec_mpyacc_f32_f16(xy_p, Q6_Vh_vshuff_Vh(vx1[i]), S1); + + HVX_Vector xy = Q6_V_lo_W(xy_p); + i = 2 * i; // index for vy + + if (nloe >= VLEN_FP32) { + vy[i] = xy; + nloe -= VLEN_FP32; ++i; xy = Q6_V_hi_W(xy_p); + } + + if (nloe) { + hvx_vec_store_a(&vy[i], nloe * 4, xy); + } + } +} static inline void hvx_scale_vec_f32_aa(uint8_t * restrict dst, const uint8_t * restrict src, const uint32_t n, HVX_Vector vs) { assert((size_t) dst % 128 == 0); diff --git a/ggml/src/ggml-hexagon/htp/hvx-reduce.h b/ggml/src/ggml-hexagon/htp/hvx-reduce.h index 3c0073ef6..76d712dc8 100644 --- a/ggml/src/ggml-hexagon/htp/hvx-reduce.h +++ b/ggml/src/ggml-hexagon/htp/hvx-reduce.h @@ -286,6 +286,46 @@ static inline float hvx_sum_of_squares_f32(const uint8_t * restrict src, const i } } +// Signed 32-bit Integer Max variants + +static inline HVX_Vector hvx_vec_reduce_max_n_i32(HVX_Vector in, unsigned int n) { + unsigned int total = n * 4; // total vec nbytes + unsigned int width = 4; // int32 nbytes + + HVX_Vector max_val = in, max_t; + while (width < total) { + max_t = Q6_V_vror_VR(max_val, width); // rotate right + max_val = Q6_Vw_vmax_VwVw(max_t, max_val); // elementwise signed max + width = width << 1; + } + return max_val; +} + +static inline HVX_Vector hvx_vec_reduce_max_i32(HVX_Vector in) { + return hvx_vec_reduce_max_n_i32(in, 32); +} + +static inline int32_t hvx_reduce_max_i32_a(const uint8_t * restrict src, const int num_elems) { + HVX_Vector init_vec = Q6_V_vsplat_R(((const int32_t *) src)[0]); + HVX_Vector pad_vec = Q6_V_vsplat_R(0x80000000); + assert((uintptr_t) src % 128 == 0); + hvx_reduce_loop_body(HVX_Vector, init_vec, pad_vec, Q6_Vw_vmax_VwVw, hvx_vec_reduce_max_i32, hvx_vec_get_i32); +} + +static inline int32_t hvx_reduce_max_i32_u(const uint8_t * restrict src, const int num_elems) { + HVX_Vector init_vec = Q6_V_vsplat_R(((const int32_t *) src)[0]); + HVX_Vector pad_vec = Q6_V_vsplat_R(0x80000000); + hvx_reduce_loop_body(HVX_UVector, init_vec, pad_vec, Q6_Vw_vmax_VwVw, hvx_vec_reduce_max_i32, hvx_vec_get_i32); +} + +static inline int32_t hvx_reduce_max_i32(const uint8_t * restrict src, const int num_elems) { + if (hex_is_aligned((void *) src, 128)) { + return hvx_reduce_max_i32_a(src, num_elems); + } else { + return hvx_reduce_max_i32_u(src, num_elems); + } +} + #undef hvx_reduce_loop_body #undef HVX_REDUCE_MAX_OP #undef HVX_REDUCE_SUM_OP diff --git a/ggml/src/ggml-hexagon/htp/main.c b/ggml/src/ggml-hexagon/htp/main.c index 321b832e1..7d65e4643 100644 --- a/ggml/src/ggml-hexagon/htp/main.c +++ b/ggml/src/ggml-hexagon/htp/main.c @@ -901,10 +901,8 @@ static void prep_tensor(struct htp_context *ctx, struct htp_buf_desc *bufs, stru uint32_t offset = t->data; uint32_t size = t->size; uint32_t bi = t->bi; - uint32_t alias = t->alias; t->data = (uint32_t) (bufs[bi].base + offset); // update data to the actual pointer - t->alias = (uint32_t) (tens + alias); // update alias to the actual pointer FARF(HIGH, "prep-tensor #%u: bi %u offset %u size %u data %p : %u:%u:%u:%u", idx, t->bi, offset, t->size, (void*) t->data, t->ne[0], t->ne[1], t->ne[3], t->ne[3]); @@ -955,14 +953,14 @@ static int proc_op_req(struct htp_ops_context * octx, struct htp_tensor *tens, u octx->dsts[i] = dst; octx->dst_dma[i] = octx->ctx->dma; // FIXME: ? octx->ctx->dma_cached : octx->ctx->dma; - htp_tensor_make_dirty(dst, octx->ctx->dirty_map); - FARF(HIGH, "prep-dst[%u] #%u: data %p size %u : %u:%u:%u:%u", i, dst_idx, (void*) dst->data, dst->size, dst->ne[0], dst->ne[1], dst->ne[2], dst->ne[3]); } int status = execute_op(octx); + htp_tensor_dirty_all(octx->ctx, octx->dsts, HTP_OP_MAX_OUTPUTS); + octx->src0_spad.src = NULL; octx->src1_spad.src = NULL; octx->src2_spad.src = NULL; @@ -994,12 +992,6 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r FARF(HIGH, "processing opbatch #%u: n-bufs %u n-tensors %u n-ops %u n-traces %u : m-size %u b-size %u t-size %u o-size %u", req->id, n_bufs, n_tens, n_ops, req->n_traces, dbuf->size, b_size, t_size, o_size); - // Clean cache at the start of the batch - // We cant trace this part because the trace buffer is setup later - qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); - hex_l2fetch_block(ctx, ctx->footprint); - bitmap_reset(ctx->dirty_map, HTP_OP_MAX_TENSORS); - // Setup descriptor pointers uint8_t * m_ptr = dbuf->ptr; struct htp_buf_desc* bufs = (struct htp_buf_desc*) m_ptr; m_ptr += b_size; @@ -1007,13 +999,8 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r struct htp_op_desc* ops = (struct htp_op_desc*) m_ptr; m_ptr += o_size; struct htp_prof_desc* pds = (struct htp_prof_desc*) m_ptr; - prep_op_bufs(ctx, bufs, n_bufs); - prep_tensors(ctx, bufs, tens, n_tens); - - struct htp_ops_context *octx = &ctx->octx; - memset(octx, 0, sizeof(*octx)); - octx->n_threads = ctx->n_threads; - octx->ctx = ctx; + struct profile_data batch_prof; + profile_start(HTP_PROF_BASIC, &batch_prof); memset(ctx->trace, 0, sizeof(ctx->trace)); if (ctx->profiler == HTP_PROF_TRACE) { @@ -1024,6 +1011,24 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r } } + // Clean cache at the start of the batch + htp_trace_event_start(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); + qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); + hex_l2fetch_block(ctx, ctx->footprint); + memset(ctx->dirty_ranges, 0, sizeof(ctx->dirty_ranges)); + htp_trace_event_stop(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); + + htp_trace_event_start(&ctx->trace[0], HTP_TRACE_EVT_BUFF, 0); + prep_op_bufs(ctx, bufs, n_bufs); + htp_trace_event_stop(&ctx->trace[0], HTP_TRACE_EVT_BUFF, 0); + + prep_tensors(ctx, bufs, tens, n_tens); + + struct htp_ops_context *octx = &ctx->octx; + memset(octx, 0, sizeof(*octx)); + octx->n_threads = ctx->n_threads; + octx->ctx = ctx; + work_queue_wakeup(ctx->work_queue); if (ctx->hmx_queue) { hmx_queue_wakeup(ctx->hmx_queue); @@ -1056,13 +1061,23 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r } work_queue_suspend(ctx->work_queue); + // Flush remaining dirty tensors at the end of the batch + htp_trace_event_start(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); + qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); + htp_trace_event_stop(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); + + profile_stop(HTP_PROF_BASIC, &batch_prof); + struct htp_opbatch_rsp rsp; memset(&rsp, 0, sizeof(rsp)); - rsp.id = req->id; - rsp.status = op_status; - rsp.n_bufs = n_bufs; - rsp.n_tensors = n_tens; - rsp.n_ops = n_ops; + rsp.id = req->id; + rsp.status = op_status; + rsp.n_bufs = n_bufs; + rsp.n_tensors = n_tens; + rsp.n_ops = n_ops; + rsp.usecs = batch_prof.usecs; + rsp.cycles_start = batch_prof.cycles_start; + rsp.cycles_stop = batch_prof.cycles_stop; if (ctx->profiler == HTP_PROF_TRACE) { for (int t = 0; t <= HTP_MAX_NTHREADS; t++) { @@ -1073,11 +1088,6 @@ static void process_opbatch(struct htp_context * ctx, const struct htp_opbatch_r struct dspqueue_buffer write_dbuf = *dbuf; write_dbuf.flags = DSPQUEUE_BUFFER_FLAG_FLUSH_SENDER | DSPQUEUE_BUFFER_FLAG_INVALIDATE_RECIPIENT; - // Flush remaining dirty tensors at the end of the batch - htp_trace_event_start(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); - qurt_mem_cache_clean((qurt_addr_t) 0, 0, QURT_MEM_CACHE_FLUSH_INVALIDATE_ALL, QURT_MEM_DCACHE); - htp_trace_event_stop(&ctx->trace[0], HTP_TRACE_EVT_L2FLUSH, 0); - err = dspqueue_write(queue, 0, 1, &write_dbuf, sizeof(rsp), (const uint8_t *) &rsp, DSPQUEUE_TIMEOUT_NONE); if (err != 0) { FARF(ERROR, "dspqueue_write failed: 0x%08x", (unsigned) err); diff --git a/ggml/src/ggml-hexagon/htp/matmul-ops.c b/ggml/src/ggml-hexagon/htp/matmul-ops.c index 0611aa982..9d385469a 100644 --- a/ggml/src/ggml-hexagon/htp/matmul-ops.c +++ b/ggml/src/ggml-hexagon/htp/matmul-ops.c @@ -14,6 +14,8 @@ #include "hex-dma.h" #include "hvx-utils.h" #include "hvx-dump.h" +#include "hvx-arith.h" +#include "hvx-reduce.h" #define GGML_COMMON_DECL_C #include "ggml-common.h" @@ -82,6 +84,8 @@ struct htp_mm_context { // Precomputed values uint32_t src0_nrows_per_thread; + uint32_t src0_row_size_padded; + uint32_t src1_nrows; struct fastdiv_values mm_div_ne12_ne1; struct fastdiv_values mm_div_ne1; @@ -103,6 +107,7 @@ struct htp_mm_context { // Fields for scattered mapping & HMX support in MUL_MAT_ID const uint32_t * matrix_row_counts; const struct mmid_row_mapping * matrix_rows; + uint32_t mapping_stride; // Dynamic VTCM pointers allocated sequentially uint8_t * vtcm_src0; @@ -154,8 +159,6 @@ static const uint8_t __attribute__((aligned(VLEN))) kvalues_mxfp4_lut[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; - - #define htp_matmul_tensors_preamble \ const struct htp_tensor * restrict src0 = octx->src[0]; \ const struct htp_tensor * restrict src1 = octx->src[1]; \ @@ -444,6 +447,16 @@ static void hvx_mv_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void \ uint32_t push_ct = ct_start; \ if (src0_start_row < src0_end_row) { \ + if (src2) { \ + float * vtcm_src2_ptr = (float *) mmctx->vtcm_src2 + src0_start_row; \ + const float * src2_ptr = (const float *) src2->data + src0_start_row; \ + int slice_size = (int)MIN(src0_end_row, ne0) - (int)src0_start_row; \ + if (slice_size > 0) { \ + dma_queue_push(dma_queue, dma_make_ptr(vtcm_src2_ptr, src2_ptr), \ + slice_size * sizeof(float), slice_size * sizeof(float), slice_size * sizeof(float), 1); \ + dma_queue_pop_nowait(dma_queue); \ + } \ + } \ for (uint32_t d = 0; d < n_prefetch && push_ct < ct_end; d++, push_ct++) { \ dma_queue_push(dma_queue, dma_make_ptr(vtcm_src0_ptr + d * tile_row_transfer_size_aligned, \ src0_row + push_ct * tile_row_stride), aligned_tile_size, tile_size, tile_size, n_k_tiles_a); \ @@ -465,7 +478,7 @@ static void hvx_mv_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void \ htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ct); \ DOT_2X1(ne10, dst_ptr, w_tile, src1_col, valid_rows, NULL); \ - htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ct); \ + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ct); \ \ if (push_ct < ct_end) { \ dma_queue_push(dma_queue, dma_make_ptr((uint8_t *)w_tile, src0_row + push_ct * tile_row_stride), \ @@ -476,24 +489,16 @@ static void hvx_mv_2d_repacked_##SUFFIX(unsigned int nth, unsigned int ith, void \ int copy_cnt = (int)MIN(src0_end_row, ne0) - (int)src0_start_row; \ if (copy_cnt > 0) { \ + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, ct_end); \ if (src2) { \ - float * dst_ptr = &dst_col[src0_start_row]; \ - const float * src2_ptr = (const float *) src2->data + src0_start_row; \ - float * tmp_ptr = tmp; \ - int remaining = copy_cnt; \ - while (remaining > 0) { \ - int n = MIN(remaining, 32); \ - HVX_Vector v_out = hvx_vmemu(tmp_ptr); \ - HVX_Vector v_z = hvx_vmemu(src2_ptr); \ - hvx_vec_store_u(dst_ptr, n * sizeof(float), hvx_vec_add_f32_f32(v_out, v_z)); \ - dst_ptr += n; \ - src2_ptr += n; \ - tmp_ptr += n; \ - remaining -= n; \ - } \ + hvx_add_f32_uaa((uint8_t *) &dst_col[src0_start_row], \ + (const uint8_t *) tmp, \ + (const uint8_t *) ((const float *) mmctx->vtcm_src2 + src0_start_row), \ + copy_cnt); \ } else { \ hvx_copy_f32_ua((uint8_t *) &dst_col[src0_start_row], (uint8_t *) tmp, copy_cnt); \ } \ + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, ct_end); \ } \ } @@ -1069,6 +1074,16 @@ static void hvx_mv_2d(unsigned int nth, unsigned int ith, void * data) { // Prefill vtcm with 2x src0 rows if (src0_start_row < src0_end_row) { + if (src2) { + float * vtcm_src2_ptr = (float *) mmctx->vtcm_src2 + src0_start_row; + const float * src2_ptr = (const float *) src2->data + src0_start_row; + int slice_size = (int)src0_end_row - (int)src0_start_row; + if (slice_size > 0) { + dma_queue_push(dma_queue, dma_make_ptr(vtcm_src2_ptr, src2_ptr), + slice_size * sizeof(float), slice_size * sizeof(float), slice_size * sizeof(float), 1); + dma_queue_pop_nowait(dma_queue); + } + } for (uint32_t ir0 = src0_start_row; ir0 < src0_end_row_x2; ir0 += 2) { const uint32_t is0 = (ir0 - src0_start_row); if (is0 >= n_prefetch) { @@ -1114,27 +1129,21 @@ static void hvx_mv_2d(unsigned int nth, unsigned int ith, void * data) { } int copy_cnt = src0_end_row - src0_start_row; - if (src2) { - float * dst_ptr = &dst_col[src0_start_row]; - const float * src2_ptr = (const float *) src2->data + src0_start_row; - float * tmp_ptr = tmp; - int remaining = copy_cnt; - while (remaining > 0) { - int n = MIN(remaining, 32); - HVX_Vector v_out = hvx_vmemu(tmp_ptr); - HVX_Vector v_z = hvx_vmemu(src2_ptr); - hvx_vec_store_u(dst_ptr, n * sizeof(float), hvx_vec_add_f32_f32(v_out, v_z)); - dst_ptr += n; - src2_ptr += n; - tmp_ptr += n; - remaining -= n; + if (copy_cnt > 0) { + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, src0_end_row); + if (src2) { + hvx_add_f32_uaa((uint8_t *) &dst_col[src0_start_row], + (const uint8_t *) tmp, + (const uint8_t *) ((const float *) mmctx->vtcm_src2 + src0_start_row), + copy_cnt); + } else { + hvx_copy_f32_ua((uint8_t *) &dst_col[src0_start_row], (uint8_t *) tmp, copy_cnt); } - } else { - hvx_copy_f32_ua((uint8_t *) &dst_col[src0_start_row], (uint8_t *) tmp, copy_cnt); + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, src0_end_row); } } -#define MMID_MATRIX_ROW(row_id, i1) matrix_rows[(row_id) * ids->ne[0] * ids->ne[1] + (i1)] +#define MMID_MATRIX_ROW(row_id, i1) matrix_rows[(row_id) * mmctx->mapping_stride + (i1)] static void hvx_mm_id(unsigned int nth, unsigned int ith, void * data) { htp_matmul_preamble; @@ -1519,7 +1528,7 @@ static int hvx_mm_matmul(struct htp_ops_context * octx) { struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, ne10, src1_nrows, octx->n_threads, - dst_row_size, src0_row_size, src1_row_size, kparams->n_prefetch, false, false, false); + dst_row_size, src0_row_size, src1_row_size, src2 ? src2->nb[1] : 0, kparams->n_prefetch, false, false, false); if (kparams->kernel_type == HTP_MM_KERNEL_HVX_F16_F16_VTCM || kparams->kernel_type == HTP_MM_KERNEL_HVX_F32_F32_VTCM || @@ -1551,6 +1560,7 @@ static int hvx_mm_matmul(struct htp_ops_context * octx) { uint8_t * const base = (uint8_t *) octx->ctx->vtcm_base; mmctx->vtcm_src1 = VTCM_LAYOUT_PTR(uint8_t, base, L.off_src1); mmctx->vtcm_src0 = VTCM_LAYOUT_PTR(uint8_t, base, L.off_src0); + mmctx->vtcm_src2 = VTCM_LAYOUT_PTR(uint8_t, base, L.off_src2); mmctx->vtcm_dst = VTCM_LAYOUT_PTR(uint8_t, base, L.off_dst); octx->src1_spad.src = NULL; @@ -2346,12 +2356,77 @@ static void dequantize_tiled_weight_chunk_to_fp16_tiles( } } +typedef struct { + float *dst; + const float *src2; + const __fp16 *vtcm_src; + uint32_t n_rows; + uint32_t n_cols; + uint32_t dst_stride; + uint32_t src2_stride; + uint32_t dst_cols; + struct fastdiv_values n_threads_div; + struct htp_thread_trace *traces; + struct htp_context *ctx; +} output_transfer_col_chunk_state_t; + +static void transfer_output_chunk_col_chunk_worker_fn(unsigned int n, unsigned int i, void *data) { + (void) n; + output_transfer_col_chunk_state_t *st = (output_transfer_col_chunk_state_t *) data; + struct htp_thread_trace * tr = &st->traces[i]; + + uint32_t n_blocks = st->n_cols / 32; + uint32_t b_first = fastdiv(n_blocks * i, &st->n_threads_div); + uint32_t b_last = fastdiv(n_blocks * (i + 1), &st->n_threads_div); + uint32_t c_first = b_first * 32; + uint32_t c_last = b_last * 32; + uint32_t c_len = c_last - c_first; + + if (c_len == 0) return; + + htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_O_PROC, c_first); + + float *dst = st->dst + c_first; + const float *src2 = st->src2 ? (st->src2 + c_first) : NULL; + const __fp16 *vtcm_src = st->vtcm_src + b_first * HTP_MM_HMX_TILE_N_ELMS; + + int chunk_dst_cols = (int)st->dst_cols - (int)c_first; + if (chunk_dst_cols > 0) { + transfer_output_chunk_fp16_to_fp32_col_chunk( + dst, src2, vtcm_src, 0, st->n_rows, c_len, st->n_cols, + st->dst_stride, st->src2_stride, (uint32_t)chunk_dst_cols + ); + } + + htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_O_PROC, c_first); +} + static void transfer_output_chunk_threaded(struct htp_context *ctx, float *dst, const float *src2, const __fp16 *vtcm_src, int n_rows, int n_cols, int dst_stride, uint32_t src2_stride, int dst_cols, int n_threads) { assert(n_cols % HTP_MM_HMX_TILE_N_COLS == 0); if (n_rows <= 0) return; + uint32_t n_blocks = (uint32_t)n_cols / 32; + if (n_threads > 1 && n_blocks >= (uint32_t)n_threads) { + struct fastdiv_values n_threads_div = init_fastdiv_values(n_threads); + output_transfer_col_chunk_state_t col_state; + col_state.dst = dst; + col_state.src2 = src2; + col_state.vtcm_src = vtcm_src; + col_state.n_rows = (uint32_t)n_rows; + col_state.n_cols = (uint32_t)n_cols; + col_state.dst_stride = (uint32_t)dst_stride; + col_state.src2_stride = src2_stride; + col_state.dst_cols = (uint32_t)dst_cols; + col_state.n_threads_div = n_threads_div; + col_state.traces = ctx->trace; + col_state.ctx = ctx; + + worker_pool_run_func(ctx->worker_pool, transfer_output_chunk_col_chunk_worker_fn, &col_state, n_threads); + return; + } + size_t n_tot_chunks = n_rows; size_t n_chunks_per_task = (n_threads == 1) ? n_tot_chunks : hmx_ceil_div(n_rows, n_threads); n_chunks_per_task = hex_align_up(n_chunks_per_task, 2); @@ -3338,12 +3413,10 @@ int op_matmul(struct htp_ops_context * octx) { static int hmx_mm_op_matmul_id( struct htp_ops_context * octx, - struct htp_mm_context * mmctx, - const uint32_t * matrix_row_counts, - const struct mmid_row_mapping * matrix_rows, - void * mapping_buf, - bool must_free_mapping + struct htp_mm_context * mmctx ) { + const uint32_t * matrix_row_counts = mmctx->matrix_row_counts; + const struct mmid_row_mapping * matrix_rows = mmctx->matrix_rows; htp_matmul_tensors_preamble; const struct htp_mm_kernel_params * kparams = (const struct htp_mm_kernel_params *) octx->kernel_params; const int n_ids = octx->src[2]->ne[0]; @@ -3361,28 +3434,24 @@ static int hmx_mm_op_matmul_id( nb11, nb12, nb1, nb2, (int) src0->nb[1], (int) src0->type, - matrix_rows, cur_a, n_ids * octx->src[2]->ne[1]); + matrix_rows, cur_a, mmctx->mapping_stride); if (ret != 0) { FARF(ERROR, "HMX matmul failed for expert %u, error %d\n", cur_a, ret); - if (must_free_mapping) free(mapping_buf); return HTP_STATUS_NO_SUPPORT; } } - if (must_free_mapping) free(mapping_buf); return HTP_STATUS_OK; } static int hvx_mm_matmul_id( struct htp_ops_context * octx, struct htp_mm_context * mmctx, - size_t src0_row_size_padded, - uint32_t src1_nrows, - worker_callback_t matmul_id_job_func, - void * mapping_buf, - bool must_free_mapping + work_queue_func_t hvx_mmid_task_func ) { htp_matmul_tensors_preamble; + const uint32_t src0_row_size_padded = mmctx->src0_row_size_padded; + const uint32_t src1_nrows = mmctx->src1_nrows; struct htp_thread_trace * tr = &octx->ctx->trace[0]; htp_trace_event_start(tr, HTP_TRACE_EVT_INIT, 0); @@ -3395,7 +3464,7 @@ static int hvx_mm_matmul_id( const uint32_t nb = (ne10 + qk - 1) / qk; const uint32_t total_nb = src1_nrows * nb; - worker_callback_t quant_task_func; + work_queue_func_t quant_task_func; uint32_t n_quant_tasks = 1; if (src1_nrows < octx->n_threads) { n_quant_tasks = MIN(total_nb, octx->n_threads); @@ -3416,7 +3485,7 @@ static int hvx_mm_matmul_id( struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, ne10, src1_nrows, octx->n_threads, - 0, src0_row_size, src1_row_size, kparams->n_prefetch, true, false, false); + 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, true, false, false); size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; @@ -3431,7 +3500,6 @@ static int hvx_mm_matmul_id( // Make sure the reserved vtcm size is sufficient if (octx->ctx->vtcm_size < vtcm_size) { FARF(ERROR, "matmul-id-%s : current VTCM reservation %zu is too small, needed %zu\n", mmctx->type, octx->ctx->vtcm_size, vtcm_size); - if (must_free_mapping) free(mapping_buf); return HTP_STATUS_VTCM_TOO_SMALL; } @@ -3461,12 +3529,78 @@ static int hvx_mm_matmul_id( htp_trace_event_stop(tr, HTP_TRACE_EVT_INIT, 0); - worker_pool_run_func(octx->ctx->worker_pool, matmul_id_job_func, mmctx, octx->n_threads); + worker_pool_run_func(octx->ctx->worker_pool, hvx_mmid_task_func, mmctx, octx->n_threads); - if (must_free_mapping) free(mapping_buf); return HTP_STATUS_OK; } +static inline void scan_expert_ids_n( + const struct htp_tensor * ids, + const uint32_t n_ids, + uint32_t n_as, + uint32_t * counts, + struct mmid_row_mapping * matrix_rows, + uint32_t mapping_stride +) { + const size_t ids_nb1 = ids->nb[1]; + const uint8_t * ids_data = (const uint8_t *) ids->data; + + for (uint32_t iid1 = 0; iid1 < ids->ne[1]; ++iid1) { + const int32_t * row_ptr = (const int32_t *) (ids_data + iid1 * ids_nb1); + for (uint32_t id = 0; id < n_ids; ++id) { + const int32_t i02 = row_ptr[id]; + if (i02 < 0) { + continue; + } + assert(i02 < n_as); + + if (matrix_rows) { + matrix_rows[i02 * mapping_stride + counts[i02]] = (struct mmid_row_mapping) { id, iid1 }; + } + counts[i02] += 1; + } + } +} + +static inline void scan_expert_ids( + const struct htp_tensor * ids, + uint32_t n_ids, + uint32_t n_as, + uint32_t * counts, + struct mmid_row_mapping * matrix_rows, + uint32_t mapping_stride +) { + const size_t ids_nb0 = ids->nb[0]; + + if (ids_nb0 == 4) { + switch (n_ids) { + case 8: scan_expert_ids_n(ids, 8, n_as, counts, matrix_rows, mapping_stride); break; + case 4: scan_expert_ids_n(ids, 4, n_as, counts, matrix_rows, mapping_stride); break; + case 2: scan_expert_ids_n(ids, 2, n_as, counts, matrix_rows, mapping_stride); break; + default: scan_expert_ids_n(ids, n_ids, n_as, counts, matrix_rows, mapping_stride); break; + } + } else { + // Strided fallback + const size_t ids_nb1 = ids->nb[1]; + const uint8_t * ids_data = (const uint8_t *) ids->data; + for (uint32_t iid1 = 0; iid1 < ids->ne[1]; ++iid1) { + const int32_t * row_ptr = (const int32_t *) (ids_data + iid1 * ids_nb1); + for (uint32_t id = 0; id < n_ids; ++id) { + const int32_t i02 = *(const int32_t *) ((const uint8_t *) row_ptr + id * ids_nb0); + if (i02 < 0) { + continue; + } + assert(i02 < n_as); + + if (matrix_rows) { + matrix_rows[i02 * mapping_stride + counts[i02]] = (struct mmid_row_mapping) { id, iid1 }; + } + counts[i02] += 1; + } + } + } +} + int op_matmul_id(struct htp_ops_context * octx) { htp_matmul_tensors_preamble; @@ -3489,74 +3623,72 @@ int op_matmul_id(struct htp_ops_context * octx) { const uint32_t src0_nrows = ne01; // per expert const uint32_t src1_nrows = ne11 * ne12 * ne13; - worker_callback_t quant_task_func; - worker_callback_t matmul_id_job_func = src1_nrows > 1 ? hvx_mm_id : hvx_mv_id; - - // Compute src0_nrows_per_thread - mmctx->src0_nrows_per_thread = (src0_nrows + octx->n_threads - 1) / octx->n_threads; - mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); + mmctx->src0_nrows_per_thread = (src0_nrows + octx->n_threads - 1) / octx->n_threads; + mmctx->src0_nrows_per_thread = hex_round_up(mmctx->src0_nrows_per_thread, 32); // row groups const int n_ids = ids->ne[0]; // n_expert_used const int n_as = ne02; // n_expert - size_t matrix_row_counts_size = n_as * sizeof(uint32_t); - size_t matrix_row_map_size = n_as * ids->ne[0] * ids->ne[1] * sizeof(struct mmid_row_mapping); - const size_t total_map_size = matrix_row_counts_size + matrix_row_map_size; - - void * mapping_buf = NULL; - bool must_free_mapping = false; - - if (octx->ctx->ddr_spad_base && total_map_size <= octx->ctx->ddr_spad_size) { - mapping_buf = octx->ctx->ddr_spad_base; - } else { - mapping_buf = memalign(128, total_map_size); - if (mapping_buf) { - must_free_mapping = true; - } else { - return HTP_STATUS_INTERNAL_ERR; - } - } - - uint32_t * matrix_row_counts = (uint32_t *) mapping_buf; - struct mmid_row_mapping * matrix_rows = (struct mmid_row_mapping *) ((uint8_t *) mapping_buf + matrix_row_counts_size); - - mmctx->matrix_row_counts = matrix_row_counts; - mmctx->matrix_rows = matrix_rows; - mmctx->mm_div_ne11 = kparams->div_ne11; - - if (hvx_mm_init_vec_dot(mmctx, src0->type) != 0) { - if (must_free_mapping) free(mapping_buf); - return HTP_STATUS_NO_SUPPORT; - } + uint8_t * mapping_buf = octx->ctx->ddr_spad_base; + uint32_t mapping_stride = 1; + uint32_t * matrix_row_counts = (uint32_t *) mapping_buf; + struct mmid_row_mapping * matrix_rows = NULL; if (src1_nrows > 1) { - // initialize matrix_row_counts and map - memset(matrix_row_counts, 0, n_as * sizeof(uint32_t)); + const size_t matrix_row_counts_size = n_as * sizeof(uint32_t); + assert(octx->ctx->ddr_spad_size >= matrix_row_counts_size); - // group rows by src0 matrix - for (uint32_t iid1 = 0; iid1 < ids->ne[1]; ++iid1) { // token idx - for (uint32_t id = 0; id < n_ids; ++id) { // expert idx - const int32_t i02 = *(const int32_t *) ((const uint8_t *) ids->data + iid1 * ids->nb[1] + id * ids->nb[0]); + hex_l2fetch_block((const void *) ids->data, ids->ne[1] * ids->nb[1]); - if (i02 < 0) { - continue; - } - assert(i02 < n_as); + memset(matrix_row_counts, 0, matrix_row_counts_size); + scan_expert_ids(ids, n_ids, n_as, matrix_row_counts, NULL, 0); - matrix_rows[i02 * n_ids * ids->ne[1] + matrix_row_counts[i02]] = (struct mmid_row_mapping) { id, iid1 }; - matrix_row_counts[i02] += 1; + uint32_t max_count = hvx_reduce_max_i32((const uint8_t *) matrix_row_counts, n_as); + mapping_stride = max_count > 0 ? max_count : 1; + + size_t matrix_row_map_size = n_as * mapping_stride * sizeof(struct mmid_row_mapping); + const size_t total_map_size = matrix_row_counts_size + matrix_row_map_size; + + if (total_map_size > octx->ctx->ddr_spad_size) { + mapping_buf = memalign(128, total_map_size); + if (!mapping_buf) { + return HTP_STATUS_INTERNAL_ERR; } } + + matrix_row_counts = (uint32_t *) mapping_buf; + matrix_rows = (struct mmid_row_mapping *) (mapping_buf + matrix_row_counts_size); + + memset(matrix_row_counts, 0, n_as * sizeof(uint32_t)); + scan_expert_ids(ids, n_ids, n_as, matrix_row_counts, matrix_rows, mapping_stride); } + mmctx->matrix_row_counts = matrix_row_counts; + mmctx->matrix_rows = matrix_rows; + mmctx->mapping_stride = mapping_stride; + mmctx->mm_div_ne11 = kparams->div_ne11; + mmctx->src0_row_size_padded = src0_row_size_padded; + mmctx->src1_nrows = src1_nrows; + htp_trace_event_stop(tr, HTP_TRACE_EVT_INIT, 0); + int s; if (kparams->n_hmx) { - return hmx_mm_op_matmul_id(octx, mmctx, matrix_row_counts, matrix_rows, mapping_buf, must_free_mapping); + s = hmx_mm_op_matmul_id(octx, mmctx); + } else { + if (hvx_mm_init_vec_dot(mmctx, src0->type) == 0) { + s = hvx_mm_matmul_id(octx, mmctx, src1_nrows > 1 ? hvx_mm_id : hvx_mv_id); + } else { + s = HTP_STATUS_NO_SUPPORT; + } } - return hvx_mm_matmul_id(octx, mmctx, src0_row_size_padded, src1_nrows, matmul_id_job_func, mapping_buf, must_free_mapping); + if (mapping_buf != octx->ctx->ddr_spad_base) { + free(mapping_buf); + } + + return s; } int op_matmul_qkv(struct htp_ops_context * octx) { @@ -3633,7 +3765,7 @@ int op_matmul_qkv(struct htp_ops_context * octx) { struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, src1->ne[0], src1_nrows, octx->n_threads, - 0, src0_row_size, src1_row_size, kparams->n_prefetch, false, true, false); + 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, false, true, false); size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; @@ -3778,7 +3910,7 @@ int op_matmul_ffn(struct htp_ops_context * octx) { struct htp_mm_hvx_vtcm_layout L; htp_mm_hvx_vtcm_layout_build(&L, kparams->kernel_type, src0->type, src1->ne[0], src1_nrows, octx->n_threads, - 0, src0_row_size, src1_row_size, kparams->n_prefetch, false, false, true); + 0, src0_row_size, src1_row_size, 0, kparams->n_prefetch, false, false, true); size_t vtcm_size = kparams->vtcm_size > 0 ? (size_t)kparams->vtcm_size : L.total_bytes; diff --git a/ggml/src/ggml-hexagon/htp/matmul-ops.h b/ggml/src/ggml-hexagon/htp/matmul-ops.h index 445044660..6c393664c 100644 --- a/ggml/src/ggml-hexagon/htp/matmul-ops.h +++ b/ggml/src/ggml-hexagon/htp/matmul-ops.h @@ -460,6 +460,7 @@ static inline void htp_mm_hvx_vtcm_layout_build( size_t dst_row_size, size_t src0_row_size, size_t src1_row_size, + size_t src2_row_size, uint32_t n_prefetch, bool is_matmul_id, bool is_fused_qkv, @@ -467,7 +468,7 @@ static inline void htp_mm_hvx_vtcm_layout_build( ) { size_t src0_sz = 0; size_t src1_sz = 0; - size_t src2_sz = 0; + size_t src2_sz = src2_row_size > 0 ? htp_mm_round_up(src2_row_size, 128) : 0; size_t src3_sz = 0; size_t dst_sz = 0; diff --git a/scripts/snapdragon/ggml-hexagon-profile.py b/scripts/snapdragon/ggml-hexagon-profile.py index 2e096aaa9..97a3acd26 100755 --- a/scripts/snapdragon/ggml-hexagon-profile.py +++ b/scripts/snapdragon/ggml-hexagon-profile.py @@ -6,6 +6,7 @@ import re import argparse import statistics import logging +import bisect from typing import Any, Dict, List, Optional from collections import defaultdict @@ -30,7 +31,7 @@ op_pattern = re.compile( ) trace_pattern = re.compile( - r"trace-op\s+(?P[A-Z_0-9+]+):\s+thread\s+(?P\d+)\s+event\s+(?P[A-Z_0-9\-]+)\s+info\s+(?P\d+)\s+(?Pstart|stop)\s+(?P\d+)" + r"trace-evt\s+(?P[A-Z_0-9\-]+):\s+thread\s+(?P\d+)\s+info\s+(?P\d+)\s+(?Pstart|stop)\s+(?P\d+)" ) logger = logging.getLogger("ggml-hexagon-profile") @@ -50,9 +51,13 @@ def normalize_event_name(evt_type): class CycleUnwrapper: - def __init__(self): - self.last_raw = None - self.high_part = 0 + def __init__(self, initial_val=None): + if initial_val is not None: + self.last_raw = initial_val & 0xFFFFFFFF + self.high_part = initial_val & 0xFFFFFFFF00000000 + else: + self.last_raw = None + self.high_part = 0 def unwrap(self, raw): if self.last_raw is None: @@ -78,10 +83,12 @@ def parse_log(file_path, pmu_index=None): sys.exit(1) all_ops: List[Dict[str, Any]] = [] + all_traces: List[Dict[str, Any]] = [] current_op: Optional[Dict[str, Any]] = None timestamp_pattern = re.compile(r"^(?P\d+)\.(?P\d+)\.(?P\d+)\.(?P\d+)\s+[A-Z]\s+") - unwrapper = CycleUnwrapper() + unwrapper = None + trace_unwrapper = None for line in f: ts_match = timestamp_pattern.match(line) @@ -100,6 +107,7 @@ def parse_log(file_path, pmu_index=None): if not prefix_match: continue + names = parts[1] if len(parts) == 7: dims, types, timings = parts[2], parts[3], parts[6] elif len(parts) == 6: @@ -120,6 +128,7 @@ def parse_log(file_path, pmu_index=None): op_match = op_pattern.search(line) if op_match: op_name = op_match.group('op_name') + names = "" dims = op_match.group('dims').strip() types = op_match.group('types').strip() else: @@ -136,24 +145,31 @@ def parse_log(file_path, pmu_index=None): except (ValueError, IndexError): pmu_val = None - evt_raw = op_match.group('evt') if 'evt' in op_match.groupdict() else None evt_val = None - if evt_raw: + evt_val = None + if types.startswith("evt-cnt "): try: - evt_val = [int(x.strip()) for x in evt_raw.split(',')] + evt_val = [int(x.strip()) for x in types[8:].split(',')] except ValueError: evt_val = None cycles_start_raw = op_match.group('start') unwrapped_cycles_start = None - if cycles_start_raw: - unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw)) + if op_name == "OPBATCH": + if cycles_start_raw: + unwrapped_cycles_start = int(cycles_start_raw) + unwrapper = CycleUnwrapper(unwrapped_cycles_start) + trace_unwrapper = CycleUnwrapper(unwrapped_cycles_start) + else: + if cycles_start_raw and unwrapper is not None: + unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw)) idx = line.find("profile-op ") op_text = line[idx + 11:].strip() if idx != -1 else line.strip() current_op = { 'name': op_name, + 'names': names, 'dims': dims, 'types': types, 'op_text': op_text, @@ -170,110 +186,239 @@ def parse_log(file_path, pmu_index=None): continue trace_match = trace_pattern.search(line) - if trace_match and current_op: - if trace_match.group('op_name') == current_op['name']: - raw_cyc = int(trace_match.group('cycles')) - current_op['trace_events'].append({ - 'thread': int(trace_match.group('thread')), - 'event': trace_match.group('event'), - 'info': int(trace_match.group('info')), - 'cycles': raw_cyc, - 'unwrapped_cycles': unwrapper.unwrap(raw_cyc), - 'state': trace_match.group('state') - }) + if trace_match: + raw_cyc = int(trace_match.group('cycles')) + unwrapped_cyc = None + if trace_unwrapper is not None: + unwrapped_cyc = trace_unwrapper.unwrap(raw_cyc) + all_traces.append({ + 'thread': int(trace_match.group('thread')), + 'event': trace_match.group('event'), + 'info': int(trace_match.group('info')), + 'cycles': raw_cyc, + 'unwrapped_cycles': unwrapped_cyc, + 'state': trace_match.group('state') + }) f.close() + + # Assign start/end cycles to all ops + for op in all_ops: + op['start_cycles'] = op['unwrapped_cycles_start'] + op['end_cycles'] = op['start_cycles'] + op['cycles'] if op['start_cycles'] is not None else None + + # Filter ops with valid start_cycles + valid_ops = [op for op in all_ops if op['start_cycles'] is not None and op['end_cycles'] is not None] + + # Separate OPBATCH ops from other ops + opbatch_ops = [op for op in valid_ops if op['name'] == "OPBATCH"] + other_ops = [op for op in valid_ops if op['name'] != "OPBATCH"] + + # Sort them by start_cycles to enable binary search + opbatch_ops.sort(key=lambda op: op['start_cycles']) + other_ops.sort(key=lambda op: op['start_cycles']) + + opbatch_starts = [op['start_cycles'] for op in opbatch_ops] + other_starts = [op['start_cycles'] for op in other_ops] + + # Map trace events to any operator whose cycles contain them + for e in all_traces: + cyc = e['unwrapped_cycles'] + if cyc is None: + continue + + # Map to OPBATCH + idx = bisect.bisect_right(opbatch_starts, cyc) - 1 + if idx >= 0: + op = opbatch_ops[idx] + if op['start_cycles'] <= cyc <= op['end_cycles']: + op['trace_events'].append(e) + + # Map to other ops + idx = bisect.bisect_right(other_starts, cyc) - 1 + if idx >= 0: + op = other_ops[idx] + if op['start_cycles'] <= cyc <= op['end_cycles']: + op['trace_events'].append(e) + return all_ops -def print_ascii_timeline(op_name, dims, types, usec, cycles, events, evt_val=None): - evt_str = "" - if evt_val: - evt_str = " - evt [" + ",".join(str(x) for x in evt_val) + "]" +def print_bubbles_timeline(op): + op_name = op['name'] + dims = op['dims'] + types = op['types'] + usec = op['usec'] + cycles = op['cycles'] + events = op['trace_events'] logger.info("=" * 100) - logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles{evt_str}") + logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles") logger.info("=" * 100) - events = sorted(events, key=lambda e: e['cycles']) if not events: logger.info(" No trace events recorded.") return - min_cycles = events[0]['cycles'] + # Identify start and end cycles for this operator + op_start = op['start_cycles'] + op_end = op['end_cycles'] + if op_start is None or op_end is None: + logger.info(" Cannot analyze bubbles: missing start/end cycle counts.") + return - logger.info("Cycles %-30s" % "EventDetails" + " ".join(f"T{i:<2}" for i in range(10)) + " HMX") - logger.info("-" * 100) - - thread_stacks = [[] for _ in range(11)] + batch_duration = op_end - op_start + if batch_duration <= 0: + logger.info(" Cannot analyze bubbles: batch duration is 0.") + return + # Group events by (thread, track_type) + tracks = defaultdict(list) for e in events: t = e['thread'] - if t < 0 or t > 10: - continue + is_dma = (normalize_event_name(e['event']) == 'DMA') + track_type = 'dma' if is_dma else 'compute' + tracks[(t, track_type)].append(e) - if e['cycles'] >= min_cycles: - rel_cycles = e['cycles'] - min_cycles - else: - rel_cycles = (e['cycles'] + 0x100000000) - min_cycles + active_threads = sorted(list(set(t for (t, track_type) in tracks.keys()))) + if not active_threads: + logger.info(" No active threads in trace.") + return - state = e['state'] - evt_type = e['event'] + bubble_threshold = 10000 # 10k cycles - # Determine char representing the event - norm_evt = normalize_event_name(evt_type) - char = '?' - if norm_evt == 'V-COMP': - char = 'V' - elif norm_evt == 'M-COMP': - char = 'H' - elif norm_evt == 'A-QUANT': - char = 'Q' - elif norm_evt == 'A-PREP': - char = 'A' - elif norm_evt == 'Q-PREP': - char = 'q' - elif norm_evt == 'K-PREP': - char = 'k' - elif norm_evt == 'V-PREP': - char = 'v' - elif norm_evt == 'W-DEQUANT': - char = 'D' - elif norm_evt == 'O-PROC': - char = 'O' - elif norm_evt == 'W-PREP': - char = 'P' - elif norm_evt == 'DMA': - char = 'M' + thread_stats = {} + for t in active_threads: + thread_stats[t] = { + 'compute_idle_cycles': batch_duration, + 'compute_idle_pct': 100.0, + 'compute_bubbles': [], - if state == 'start': - thread_stacks[t].append(char) - elif state == 'stop': - if thread_stacks[t]: - if thread_stacks[t][-1] == char: - thread_stacks[t].pop() - elif char in thread_stacks[t]: - thread_stacks[t].remove(char) - else: - thread_stacks[t].pop() + 'dma_idle_cycles': batch_duration, + 'dma_idle_pct': 100.0, + 'dma_bubbles': [] + } - cols = [] - for i in range(11): - if thread_stacks[i]: - cols.append(f"[{thread_stacks[i][-1]}]") + total_compute_idle_pct = 0.0 + total_dma_idle_pct = 0.0 + + for t in active_threads: + for track_type in ['compute', 'dma']: + key = (t, track_type) + track_events = tracks.get(key, []) + + if not track_events: + gaps = [(op_start, op_end)] + idle_cycles = batch_duration else: - cols.append(" | ") + track_events = sorted(track_events, key=lambda e: e.get('unwrapped_cycles') or e['cycles']) - evt_desc = f"T{t}: {evt_type} {state} ({e['info']})" - logger.info(f"{rel_cycles:10d} %-30s" % evt_desc + " ".join(cols[:10]) + " " + cols[10]) + active_intervals = [] + active_count = 0 + curr_start = None + + for e in track_events: + cyc = e.get('unwrapped_cycles') or e['cycles'] + cyc = max(op_start, min(op_end, cyc)) + state = e['state'] + + if state == 'start': + if active_count == 0: + curr_start = cyc + active_count += 1 + elif state == 'stop': + if active_count > 0: + active_count -= 1 + if active_count == 0: + active_intervals.append((curr_start, cyc)) + else: + active_intervals.append((op_start, cyc)) + + if active_count > 0 and curr_start is not None: + active_intervals.append((curr_start, op_end)) + + # Merge intervals + active_intervals.sort(key=lambda x: x[0]) + merged_intervals = [] + for start, end in active_intervals: + if not merged_intervals: + merged_intervals.append([start, end]) + else: + last_start, last_end = merged_intervals[-1] + if start <= last_end: + merged_intervals[-1][1] = max(last_end, end) + else: + merged_intervals.append([start, end]) + + # Calculate gaps + gaps = [] + curr_time = op_start + for start, end in merged_intervals: + if start > curr_time: + gaps.append((curr_time, start)) + curr_time = max(curr_time, end) + if curr_time < op_end: + gaps.append((curr_time, op_end)) + + idle_cycles = sum(end - start for start, end in gaps) + + idle_pct = (idle_cycles / batch_duration) * 100.0 + + bubbles = [] + for start, end in gaps: + dur = end - start + if dur >= bubble_threshold: + bubbles.append((start, end, dur)) + + if track_type == 'compute': + thread_stats[t]['compute_idle_cycles'] = idle_cycles + thread_stats[t]['compute_idle_pct'] = idle_pct + thread_stats[t]['compute_bubbles'] = bubbles + total_compute_idle_pct += idle_pct + else: + thread_stats[t]['dma_idle_cycles'] = idle_cycles + thread_stats[t]['dma_idle_pct'] = idle_pct + thread_stats[t]['dma_bubbles'] = bubbles + total_dma_idle_pct += idle_pct + + avg_compute_idle = total_compute_idle_pct / len(active_threads) + avg_dma_idle = total_dma_idle_pct / len(active_threads) + + logger.info(" Combined Idle Statistics:") + logger.info(f" Active Threads : {', '.join(str(t) for t in active_threads)}") + logger.info(f" Avg Thread Compute IDLE : {avg_compute_idle:.1f}%") + logger.info(f" Avg Thread DMA IDLE : {avg_dma_idle:.1f}%") logger.info("-" * 100) + logger.info(" Per-Thread Idle Analysis:") + for t in active_threads: + stats = thread_stats[t] + thread_name = f"Thread {t:<2} (HVX)" if t != 10 else "Thread 10 (HMX)" + logger.info(f" {thread_name} -> Compute Idle: {stats['compute_idle_pct']:.1f}% | DMA Idle: {stats['dma_idle_pct']:.1f}%") -def print_ascii_summary(op_name, dims, types, usec, cycles, events, evt_val=None): - evt_str = "" - if evt_val: - evt_str = " - evt [" + ",".join(str(x) for x in evt_val) + "]" + all_bubbles = [] + for t in active_threads: + stats = thread_stats[t] + for start, end, dur in stats['compute_bubbles']: + pct = (dur / batch_duration) * 100.0 + all_bubbles.append((dur, f"Thread {t} Compute: bubble of {dur} cycles ({pct:.1f}%) at {start - op_start} to {end - op_start}")) + for start, end, dur in stats['dma_bubbles']: + pct = (dur / batch_duration) * 100.0 + all_bubbles.append((dur, f"Thread {t} DMA : bubble of {dur} cycles ({pct:.1f}%) at {start - op_start} to {end - op_start}")) + + if all_bubbles: + logger.info("-" * 100) + logger.info(f" Significant Bubbles (>= {bubble_threshold} cycles):") + all_bubbles.sort(key=lambda x: x[0], reverse=True) + for dur, desc in all_bubbles[:15]: + logger.info(f" {desc}") + else: + logger.info("-" * 100) + logger.info(f" No significant bubbles detected (all idle gaps < {bubble_threshold} cycles).") + + +def print_ascii_summary(op_name, dims, types, usec, cycles, events): logger.info("=" * 100) - logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles{evt_str}") + logger.info(f"{op_name} ({dims} : {types}) - {usec} usec {cycles} cycles") logger.info("=" * 100) events = sorted(events, key=lambda e: e['cycles']) @@ -415,8 +560,8 @@ def main(): parser.add_argument("--pmu-index", type=int) parser.add_argument("--pmu-name", type=str) parser.add_argument("--width", action='append', default=['dims:40'], help="Override column width, e.g. --width dims:50") - parser.add_argument("--timeline", type=str, nargs='?', const='summary', choices=["summary", "diagram"], - help="Output ASCII art event summary or timing diagram (default: summary)") + parser.add_argument("--timeline", type=str, nargs='?', const='summary', choices=["summary", "bubbles"], + help="Output ASCII art event summary or thread idle bubble analysis (default: summary)") parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line") group = parser.add_mutually_exclusive_group() @@ -457,12 +602,11 @@ def main(): ops = ops[-args.tail:] if args.timeline: - logger.info(f"\n# ASCII Timing {args.timeline.capitalize()}\n") for op in ops: if args.timeline == "summary": - print_ascii_summary(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'], op.get('evt_val')) - elif args.timeline == "diagram": - print_ascii_timeline(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events'], op.get('evt_val')) + print_ascii_summary(op['name'], op['dims'], op['types'], op['usec'], op['cycles'], op['trace_events']) + elif args.timeline == "bubbles": + print_bubbles_timeline(op) else: generate_report(ops, args.top, overrides, args.sort, pmu_name=final_pmu_name) diff --git a/scripts/snapdragon/ggml-hexagon-trace.py b/scripts/snapdragon/ggml-hexagon-trace.py index 37f137a9e..4755adfa1 100755 --- a/scripts/snapdragon/ggml-hexagon-trace.py +++ b/scripts/snapdragon/ggml-hexagon-trace.py @@ -6,6 +6,7 @@ import re import argparse import statistics import logging +import bisect from typing import Any, Dict, List, Optional from collections import defaultdict @@ -16,11 +17,11 @@ op_pattern = re.compile( ) trace_pattern = re.compile( - r"trace-op\s+(?P[A-Z_0-9+]+):\s+thread\s+(?P\d+)\s+event\s+(?P[A-Z_0-9\-]+)\s+info\s+(?P\d+)\s+(?Pstart|stop)\s+(?P\d+)" + r"trace-evt\s+(?P[A-Z_0-9\-]+):\s+thread\s+(?P\d+)\s+info\s+(?P\d+)\s+(?Pstart|stop)\s+(?P\d+)" ) -def normalize_event_name(evt_type): +def normalize_event_name(evt_type, info=0): if evt_type == "HVX_COMP": return "V-COMP" if evt_type == "HMX_COMP": @@ -32,9 +33,13 @@ def normalize_event_name(evt_type): class CycleUnwrapper: - def __init__(self): - self.last_raw = None - self.high_part = 0 + def __init__(self, initial_val=None): + if initial_val is not None: + self.last_raw = initial_val & 0xFFFFFFFF + self.high_part = initial_val & 0xFFFFFFFF00000000 + else: + self.last_raw = None + self.high_part = 0 def unwrap(self, raw): if self.last_raw is None: @@ -60,8 +65,10 @@ def parse_log(file_path): sys.exit(1) all_ops: List[Dict[str, Any]] = [] + all_traces: List[Dict[str, Any]] = [] current_op: Optional[Dict[str, Any]] = None - unwrapper = CycleUnwrapper() + unwrapper = None + trace_unwrapper = None line_idx = 0 for line in f: @@ -73,6 +80,7 @@ def parse_log(file_path): if not prefix_match: continue + names = parts[1] if len(parts) == 7: dims, types, strides, params, timings = parts[2], parts[3], parts[4], parts[5], parts[6] elif len(parts) == 6: @@ -93,6 +101,7 @@ def parse_log(file_path): op_match = op_pattern.search(line) if op_match: op_name = op_match.group('op_name') + names = "" dims = op_match.group('dims').strip() if op_match.group('dims') else '' types = op_match.group('types').strip() if op_match.group('types') else '' strides = op_match.group('strides').strip() if op_match.group('strides') else '' @@ -103,18 +112,30 @@ def parse_log(file_path): if op_match: cycles_start_raw = op_match.group('start') unwrapped_cycles_start = None - if cycles_start_raw: - unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw)) + if op_name == "OPBATCH": + if cycles_start_raw: + unwrapped_cycles_start = int(cycles_start_raw) + unwrapper = CycleUnwrapper(unwrapped_cycles_start) + trace_unwrapper = CycleUnwrapper(unwrapped_cycles_start) + else: + if cycles_start_raw and unwrapper is not None: + unwrapped_cycles_start = unwrapper.unwrap(int(cycles_start_raw)) idx = line.find("profile-op ") op_text = line[idx + 11:].strip() if idx != -1 else line.strip() + evt_str = None + if types.startswith("evt-cnt "): + evt_str = types[8:].strip() + current_op = { 'name': op_name, + 'names': names, 'dims': dims, 'types': types, 'strides': strides, 'params': params, + 'evt': evt_str, 'op_text': op_text, 'usec': int(op_match.group('usec')), 'cycles': int(op_match.group('cycles')), @@ -127,20 +148,22 @@ def parse_log(file_path): continue trace_match = trace_pattern.search(line) - if trace_match and current_op: - if trace_match.group('op_name') == current_op['name']: - raw_cyc = int(trace_match.group('cycles')) - current_op['trace_events'].append({ - 'thread': int(trace_match.group('thread')), - 'event': trace_match.group('event'), - 'info': int(trace_match.group('info')), - 'cycles': raw_cyc, - 'unwrapped_cycles': unwrapper.unwrap(raw_cyc), - 'state': trace_match.group('state') - }) + if trace_match: + raw_cyc = int(trace_match.group('cycles')) + unwrapped_cyc = None + if trace_unwrapper is not None: + unwrapped_cyc = trace_unwrapper.unwrap(raw_cyc) + all_traces.append({ + 'thread': int(trace_match.group('thread')), + 'event': trace_match.group('event'), + 'info': int(trace_match.group('info')), + 'cycles': raw_cyc, + 'unwrapped_cycles': unwrapped_cyc, + 'state': trace_match.group('state') + }) f.close() - return all_ops + return all_ops, all_traces # --- Simple protobuf encoder --- @@ -246,7 +269,7 @@ def write_trace_packet_to_file(f, packet_bytes): # --- End Protobuf Encoder --- -def generate_perfetto_trace(filtered_ops, output_path): +def generate_perfetto_trace(filtered_ops, trace_events, output_path): if not filtered_ops: logger.warning("No operators found after filtering.") return @@ -269,14 +292,12 @@ def generate_perfetto_trace(filtered_ops, output_path): # Process events completed_events = [] - for op in filtered_ops: - events = op['trace_events'] - if not events: - continue - events = sorted(events, key=lambda e: e['unwrapped_cycles']) + if trace_events: + trace_events = sorted(trace_events, key=lambda e: e['unwrapped_cycles']) + one_usec_cycles = max(avg_freq_mhz, 1.0) active_starts = {} - for e in events: + for e in trace_events: t = e['thread'] evt = e['event'] info = e['info'] @@ -285,6 +306,17 @@ def generate_perfetto_trace(filtered_ops, output_path): key = (t, evt, info) if state == 'start': + # Handle missing stop (start followed by another start) + if key in active_starts: + prev_start = active_starts[key] + completed_events.append({ + 'thread': t, + 'event': evt, + 'info': info, + 'start_cyc': prev_start, + 'end_cyc': prev_start + one_usec_cycles, + 'missing_stop': True, + }) active_starts[key] = cyc elif state == 'stop': if key in active_starts: @@ -296,8 +328,29 @@ def generate_perfetto_trace(filtered_ops, output_path): 'info': info, 'start_cyc': start_cyc, 'end_cyc': cyc, - 'op_name': op['name'] }) + else: + # Handle missing start (stop without start) + completed_events.append({ + 'thread': t, + 'event': evt, + 'info': info, + 'start_cyc': cyc - one_usec_cycles, + 'end_cyc': cyc, + 'missing_start': True, + }) + + # Clear remaining unmatched starts + for key, start_cyc in active_starts.items(): + t, evt, info = key + completed_events.append({ + 'thread': t, + 'event': evt, + 'info': info, + 'start_cyc': start_cyc, + 'end_cyc': start_cyc + one_usec_cycles, + 'missing_stop': True, + }) completed_events.sort(key=lambda e: e['start_cyc']) @@ -316,7 +369,7 @@ def generate_perfetto_trace(filtered_ops, output_path): ts = e['ts_ns'] dur = e['dur_ns'] - norm_evt = normalize_event_name(evt) + norm_evt = normalize_event_name(evt, e['info']) if norm_evt == "DMA": track_key = (t, "DMA") elif t == 10: @@ -343,7 +396,7 @@ def generate_perfetto_trace(filtered_ops, output_path): evt = e['event'] slot = e['slot'] - norm_evt = normalize_event_name(evt) + norm_evt = normalize_event_name(evt, e['info']) if norm_evt == "DMA": track_evt = "DMA" evt_id = 1 @@ -421,18 +474,26 @@ def generate_perfetto_trace(filtered_ops, output_path): for op in filtered_ops: op_start_ns = int(round(((op['start_cycles'] - global_min_cyc) / avg_freq_mhz) * 1000)) op_dur_ns = int(round((op['cycles'] / avg_freq_mhz) * 1000)) - if op_start_ns < last_op_end_ns: - op_start_ns = last_op_end_ns - clamped_dur = max(op_dur_ns, 100) # Clamp to 100ns (0.1us) + if op['name'] != "OPBATCH": + if op_start_ns < last_op_end_ns: + op_start_ns = last_op_end_ns + clamped_dur = max(op_dur_ns, 100) # Clamp to 100ns (0.1us) + last_op_end_ns = op_start_ns + clamped_dur + else: + clamped_dur = max(op_dur_ns, 100) # Debug annotations for Ops debug_annots = [] if 'line_num' in op: debug_annots.append(make_debug_annotation("line", int_val=op['line_num'])) - if 'strides' in op and op['strides']: + if 'names' in op and op['names'] and op['names'] != '----': + debug_annots.append(make_debug_annotation("names", string_val=op['names'])) + if 'strides' in op and op['strides'] and op['strides'] != '----': debug_annots.append(make_debug_annotation("strides", string_val=op['strides'])) if 'params' in op and op['params'] and op['params'] != '----': debug_annots.append(make_debug_annotation("params", string_val=op['params'])) + if 'evt' in op and op['evt']: + debug_annots.append(make_debug_annotation("evt", string_val=op['evt'])) # Slice Begin evt_begin = make_track_event(1, 2, name=f"{op['name']} ({op['dims']})", category="operator", debug_annotations=debug_annots) @@ -444,15 +505,21 @@ def generate_perfetto_trace(filtered_ops, output_path): packet_end = make_trace_packet(op_start_ns + clamped_dur, track_event=evt_end) write_trace_packet_to_file(f, packet_end) - last_op_end_ns = op_start_ns + clamped_dur - # Emit Thread Trace Events for e in completed_events: - norm_name = normalize_event_name(e['event']) + norm_name = normalize_event_name(e['event'], e['info']) name = f"DMA {e['info']}" if norm_name == "DMA" else norm_name + if e.get('missing_start') or e.get('missing_stop'): + name += "!" + + debug_annots = [] + if e.get('missing_start'): + debug_annots.append(make_debug_annotation("missing_start", string_val="true")) + if e.get('missing_stop'): + debug_annots.append(make_debug_annotation("missing_stop", string_val="true")) # Slice Begin - evt_begin = make_track_event(1, e['uuid'], name=name, category="trace") + evt_begin = make_track_event(1, e['uuid'], name=name, category="trace", debug_annotations=debug_annots if debug_annots else None) packet_begin = make_trace_packet(e['ts_ns'], track_event=evt_begin) write_trace_packet_to_file(f, packet_begin) @@ -477,7 +544,7 @@ def main(): args = parser.parse_args() logging.basicConfig(level=logging.INFO, format='%(message)s') - ops = parse_log(args.logfile) + ops, traces = parse_log(args.logfile) if args.filter: try: @@ -492,7 +559,30 @@ def main(): elif args.tail is not None: ops = ops[-args.tail:] - generate_perfetto_trace(ops, args.output) + if args.filter or args.head is not None or args.tail is not None: + valid_ranges = [] + for op in ops: + start_cyc = op['unwrapped_cycles_start'] + end_cyc = start_cyc + op['cycles'] if start_cyc is not None else None + if start_cyc is not None and end_cyc is not None: + valid_ranges.append((start_cyc, end_cyc)) + + valid_ranges.sort(key=lambda r: r[0]) + range_starts = [r[0] for r in valid_ranges] + + filtered_traces = [] + for e in traces: + cyc = e['unwrapped_cycles'] + if cyc is None: + continue + idx = bisect.bisect_right(range_starts, cyc) - 1 + if idx >= 0: + start, end = valid_ranges[idx] + if start <= cyc <= end: + filtered_traces.append(e) + traces = filtered_traces + + generate_perfetto_trace(ops, traces, args.output) if __name__ == "__main__": From 0cea36222fe9bac5ebfc45716c9eef11f37046c4 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Fri, 24 Jul 2026 08:02:23 +0200 Subject: [PATCH 16/21] vendor: update subprocess.h (#26061) --- scripts/sync_vendor.py | 2 +- tools/server/server-models.cpp | 2 +- vendor/sheredom/subprocess.h | 829 ++++++++++++++++++++++++++++----- 3 files changed, 706 insertions(+), 127 deletions(-) diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 7bab415a8..a9343e37c 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -21,7 +21,7 @@ vendor = { f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/split.py": "split.py", f"https://raw.githubusercontent.com/yhirose/cpp-httplib/{HTTPLIB_VERSION}/LICENSE": "vendor/cpp-httplib/LICENSE", - "https://raw.githubusercontent.com/sheredom/subprocess.h/b49c56e9fe214488493021017bf3954b91c7c1f5/subprocess.h": "vendor/sheredom/subprocess.h", + "https://raw.githubusercontent.com/sheredom/subprocess.h/8671cee1fc09f11a70ce3782a0ee13177c3aa387/subprocess.h": "vendor/sheredom/subprocess.h", } for url, filename in vendor.items(): diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp index 7c6bfda0f..9eac58e9d 100644 --- a/tools/server/server-models.cpp +++ b/tools/server/server-models.cpp @@ -852,7 +852,7 @@ void server_models::load(const std::string & name, const load_options & opts) { // so that we can use stdout for commands and stderr for logging int options = subprocess_option_no_window | subprocess_option_combined_stdout_stderr; inst.subproc->sproc.emplace(); - int result = subprocess_create_ex(argv.data(), options, envp.data(), &inst.subproc->get()); + int result = subprocess_create_ex(argv.data(), options, envp.data(), nullptr, &inst.subproc->get()); if (result != 0) { throw std::runtime_error("failed to spawn server instance"); } diff --git a/vendor/sheredom/subprocess.h b/vendor/sheredom/subprocess.h index 3e40bae04..5e809023a 100644 --- a/vendor/sheredom/subprocess.h +++ b/vendor/sheredom/subprocess.h @@ -89,7 +89,25 @@ enum subprocess_option_e { // Search for program names in the PATH variable. Always enabled on Windows. // Note: this will **not** search for paths in any provided custom environment // and instead uses the PATH of the spawning process. - subprocess_option_search_user_path = 0x10 + subprocess_option_search_user_path = 0x10, + + // Make subprocess_read_stdout and subprocess_read_stderr return immediately + // with 0 if no data is available. Requires subprocess_option_enable_async. + subprocess_option_enable_async_no_wait = 0x20 +}; + +// Error codes returned by subprocess_create and subprocess_create_ex. +// subprocess_error_success is always zero; all errors are non-zero. +enum subprocess_error_e { + subprocess_error_success = 0, + subprocess_error_unknown = -1, + subprocess_error_invalid_options = -2, + subprocess_error_invalid_environment = -3, + subprocess_error_not_found = -4, + subprocess_error_permission_denied = -5, + subprocess_error_no_memory = -6, + subprocess_error_pipe = -7, + subprocess_error_spawn = -8 }; #if defined(__cplusplus) @@ -99,11 +117,14 @@ extern "C" { /// @brief Create a process. /// @param command_line An array of strings for the command line to execute for /// this process. The last element must be NULL to signify the end of the array. -/// The memory backing this parameter only needs to persist until this function -/// returns. +/// On Windows these strings are interpreted as UTF-8 and passed to the Unicode +/// process creation APIs. The memory backing this parameter only needs to +/// persist until this function returns. /// @param options A bit field of subprocess_option_e's to pass. /// @param out_process The newly created process. -/// @return On success zero is returned. +/// @return On success zero is returned. On failure a non-zero +/// `subprocess_error_e` value is returned; inspect `errno` on POSIX platforms +/// or `GetLastError()` on Windows for the platform-specific failure reason. subprocess_weak int subprocess_create(const char *const command_line[], int options, struct subprocess_s *const out_process); @@ -111,20 +132,28 @@ subprocess_weak int subprocess_create(const char *const command_line[], /// @brief Create a process (extended create). /// @param command_line An array of strings for the command line to execute for /// this process. The last element must be NULL to signify the end of the array. -/// The memory backing this parameter only needs to persist until this function -/// returns. +/// On Windows these strings are interpreted as UTF-8 and passed to the Unicode +/// process creation APIs. The memory backing this parameter only needs to +/// persist until this function returns. /// @param options A bit field of subprocess_option_e's to pass. /// @param environment An optional array of strings for the environment to use /// for a child process (each element of the form FOO=BAR). The last element -/// must be NULL to signify the end of the array. +/// must be NULL to signify the end of the array. On Windows these strings are +/// interpreted as UTF-8. +/// @param process_cwd The current working directory of the newly created +/// process. If NULL, will be the same as the parent process. On Windows this +/// string is interpreted as UTF-8. /// @param out_process The newly created process. -/// @return On success zero is returned. +/// @return On success zero is returned. On failure a non-zero +/// `subprocess_error_e` value is returned; inspect `errno` on POSIX platforms +/// or `GetLastError()` on Windows for the platform-specific failure reason. /// /// If `options` contains `subprocess_option_inherit_environment`, then /// `environment` must be NULL. subprocess_weak int subprocess_create_ex(const char *const command_line[], int options, const char *const environment[], + const char *const process_cwd, struct subprocess_s *const out_process); /// @brief Get the standard input file for a process. @@ -189,7 +218,8 @@ subprocess_weak int subprocess_terminate(struct subprocess_s *const process); /// @param buffer The buffer to read into. /// @param size The maximum number of bytes to read. /// @return The number of bytes actually read into buffer. Can only be 0 if the -/// process has complete. +/// process has complete, or if the process was created with +/// `subprocess_option_enable_async_no_wait` and no data is currently available. /// /// The only safe way to read from the standard output of a process during it's /// execution is to use the `subprocess_option_enable_async` option in @@ -203,7 +233,8 @@ subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, /// @param buffer The buffer to read into. /// @param size The maximum number of bytes to read. /// @return The number of bytes actually read into buffer. Can only be 0 if the -/// process has complete. +/// process has complete, or if the process was created with +/// `subprocess_option_enable_async_no_wait` and no data is currently available. /// /// The only safe way to read from the standard error of a process during it's /// execution is to use the `subprocess_option_enable_async` option in @@ -230,9 +261,14 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #endif #if !defined(_WIN32) +#include +#include #include #include #include +#if defined(__APPLE__) +#include +#endif #include #include #include @@ -240,6 +276,23 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process); #if defined(_WIN32) +#include + +#if defined(__clang__) +#if __has_warning("-Wc++-keyword") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++-keyword" +#endif +#endif + +typedef wchar_t subprocess_wchar_t; + +#if defined(__clang__) +#if __has_warning("-Wc++-keyword") +#pragma clang diagnostic pop +#endif +#endif + #if (_MSC_VER < 1920) #ifdef _WIN64 typedef __int64 subprocess_intptr_t; @@ -262,7 +315,7 @@ typedef size_t subprocess_size_t; typedef struct _PROCESS_INFORMATION *LPPROCESS_INFORMATION; typedef struct _SECURITY_ATTRIBUTES *LPSECURITY_ATTRIBUTES; -typedef struct _STARTUPINFOA *LPSTARTUPINFOA; +typedef struct _STARTUPINFOW *LPSTARTUPINFOW; typedef struct _OVERLAPPED *LPOVERLAPPED; #ifdef __clang__ @@ -276,6 +329,10 @@ typedef struct _OVERLAPPED *LPOVERLAPPED; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" #endif +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif struct subprocess_subprocess_information_s { void *hProcess; @@ -292,9 +349,9 @@ struct subprocess_security_attributes_s { struct subprocess_startup_info_s { unsigned long cb; - char *lpReserved; - char *lpDesktop; - char *lpTitle; + subprocess_wchar_t *lpReserved; + subprocess_wchar_t *lpDesktop; + subprocess_wchar_t *lpTitle; unsigned long dwX; unsigned long dwY; unsigned long dwXSize; @@ -325,6 +382,9 @@ struct subprocess_overlapped_s { void *hEvent; }; +#ifdef __clang__ +#pragma clang diagnostic pop +#endif #ifdef __MINGW32__ #pragma GCC diagnostic pop #endif @@ -333,6 +393,7 @@ struct subprocess_overlapped_s { #endif __declspec(dllimport) unsigned long __stdcall GetLastError(void); +__declspec(dllimport) void __stdcall SetLastError(unsigned long); __declspec(dllimport) int __stdcall SetHandleInformation(void *, unsigned long, unsigned long); __declspec(dllimport) int __stdcall CreatePipe(void **, void **, @@ -352,9 +413,12 @@ __declspec(dllimport) void *__stdcall CreateFileA(const char *, unsigned long, void *); __declspec(dllimport) void *__stdcall CreateEventA(LPSECURITY_ATTRIBUTES, int, int, const char *); -__declspec(dllimport) int __stdcall CreateProcessA( - const char *, char *, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, int, - unsigned long, void *, const char *, LPSTARTUPINFOA, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall CreateProcessW( + const subprocess_wchar_t *, subprocess_wchar_t *, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, int, unsigned long, void *, + const subprocess_wchar_t *, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +__declspec(dllimport) int __stdcall MultiByteToWideChar( + unsigned int, unsigned long, const char *, int, subprocess_wchar_t *, int); __declspec(dllimport) int __stdcall CloseHandle(void *); __declspec(dllimport) unsigned long __stdcall WaitForSingleObject( void *, unsigned long); @@ -365,6 +429,10 @@ __declspec(dllimport) unsigned long __stdcall WaitForMultipleObjects( unsigned long, void *const *, int, unsigned long); __declspec(dllimport) int __stdcall GetOverlappedResult(void *, LPOVERLAPPED, unsigned long *, int); +__declspec(dllimport) int __stdcall PeekNamedPipe(void *, void *, unsigned long, + unsigned long *, + unsigned long *, + unsigned long *); #if defined(_DLL) #define SUBPROCESS_DLLIMPORT __declspec(dllimport) @@ -380,6 +448,7 @@ __declspec(dllimport) int __stdcall GetOverlappedResult(void *, LPOVERLAPPED, SUBPROCESS_DLLIMPORT int __cdecl _fileno(FILE *); SUBPROCESS_DLLIMPORT int __cdecl _open_osfhandle(subprocess_intptr_t, int); SUBPROCESS_DLLIMPORT subprocess_intptr_t __cdecl _get_osfhandle(int); +SUBPROCESS_DLLIMPORT int __cdecl _close(int); #ifndef __MINGW32__ void *__cdecl _alloca(subprocess_size_t); @@ -414,12 +483,68 @@ struct subprocess_s { int return_status; #endif - subprocess_size_t alive; + int alive; + int no_wait; }; #ifdef __clang__ #pragma clang diagnostic pop #endif +#if defined(_WIN32) +subprocess_weak int subprocess_error_from_windows_error(unsigned long error); +int subprocess_error_from_windows_error(unsigned long error) { + enum { + errorFileNotFound = 2, + errorPathNotFound = 3, + errorTooManyOpenFiles = 4, + errorAccessDenied = 5, + errorInvalidHandle = 6, + errorNotEnoughMemory = 8, + errorOutOfMemory = 14, + errorInvalidDrive = 15, + errorBadPathname = 161, + errorDirectory = 267 + }; + + switch (error) { + case errorFileNotFound: + case errorPathNotFound: + case errorInvalidDrive: + case errorBadPathname: + case errorDirectory: + return subprocess_error_not_found; + case errorAccessDenied: + return subprocess_error_permission_denied; + case errorTooManyOpenFiles: + case errorNotEnoughMemory: + case errorOutOfMemory: + return subprocess_error_no_memory; + case errorInvalidHandle: + return subprocess_error_pipe; + default: + return subprocess_error_unknown; + } +} +#else +subprocess_weak int subprocess_error_from_errno(int error); +int subprocess_error_from_errno(int error) { + switch (error) { + case ENOENT: + case ENOTDIR: + return subprocess_error_not_found; + case EACCES: + case EPERM: + return subprocess_error_permission_denied; + case EMFILE: + case ENFILE: + case ENOMEM: + return subprocess_error_no_memory; + default: + return subprocess_error_unknown; + } +} +#endif + #if defined(__clang__) #if __has_warning("-Wunsafe-buffer-usage") #pragma clang diagnostic push @@ -429,6 +554,18 @@ struct subprocess_s { #if defined(_WIN32) subprocess_weak int subprocess_create_named_pipe_helper(void **rd, void **wr); +subprocess_weak void subprocess_close_handle(void **handle); +void subprocess_close_handle(void **handle) { + const void *const invalidHandleValue = + SUBPROCESS_PTR_CAST(void *, ~(SUBPROCESS_CAST(subprocess_intptr_t, 0))); + + if (*handle && (invalidHandleValue != *handle)) { + CloseHandle(*handle); + } + + *handle = SUBPROCESS_NULL; +} + int subprocess_create_named_pipe_helper(void **rd, void **wr) { const unsigned long pipeAccessInbound = 0x00000001; const unsigned long fileFlagOverlapped = 0x40000000; @@ -442,9 +579,23 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), SUBPROCESS_NULL, 1}; char name[256] = {0}; +#if defined(__clang__) +#if __has_warning("-Wunique-object-duplication") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunique-object-duplication" +#endif +#endif static subprocess_tls long index = 0; +#if defined(__clang__) +#if __has_warning("-Wunique-object-duplication") +#pragma clang diagnostic pop +#endif +#endif const long unique = index++; + *rd = SUBPROCESS_NULL; + *wr = SUBPROCESS_NULL; + #if defined(_MSC_VER) && _MSC_VER < 1900 #pragma warning(push, 1) #pragma warning(disable : 4996) @@ -472,6 +623,7 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { openExisting, fileAttributeNormal, SUBPROCESS_NULL); if (invalidHandleValue == *wr) { + subprocess_close_handle(rd); return -1; } @@ -482,27 +634,41 @@ int subprocess_create_named_pipe_helper(void **rd, void **wr) { int subprocess_create(const char *const commandLine[], int options, struct subprocess_s *const out_process) { return subprocess_create_ex(commandLine, options, SUBPROCESS_NULL, - out_process); + SUBPROCESS_NULL, out_process); } int subprocess_create_ex(const char *const commandLine[], int options, const char *const environment[], + const char *const process_cwd, struct subprocess_s *const out_process) { #if defined(_WIN32) int fd; - void *rd, *wr; + int async_no_wait; + void *rd = SUBPROCESS_NULL; + void *wr = SUBPROCESS_NULL; char *commandLineCombined; + subprocess_wchar_t *commandLineCombinedWide = SUBPROCESS_NULL; + subprocess_wchar_t *process_cwd_wide = SUBPROCESS_NULL; subprocess_size_t len; + int wide_len; int i, j; int need_quoting; unsigned long flags = 0; + unsigned long last_error = 0; + int result = subprocess_error_unknown; + const unsigned int codePageUtf8 = 65001; + const unsigned long mbErrInvalidChars = 0x00000008; const unsigned long startFUseStdHandles = 0x00000100; const unsigned long handleFlagInherit = 0x00000001; const unsigned long createNoWindow = 0x08000000; - struct subprocess_subprocess_information_s processInfo; + const unsigned long createUnicodeEnvironment = 0x00000400; + struct subprocess_subprocess_information_s processInfo = {SUBPROCESS_NULL, + SUBPROCESS_NULL, 0, + 0}; struct subprocess_security_attributes_s saAttr = {sizeof(saAttr), SUBPROCESS_NULL, 1}; - char *used_environment = SUBPROCESS_NULL; + subprocess_wchar_t empty_environment[2] = {0, 0}; + subprocess_wchar_t *used_environment = SUBPROCESS_NULL; struct subprocess_startup_info_s startInfo = {0, SUBPROCESS_NULL, SUBPROCESS_NULL, @@ -522,6 +688,14 @@ int subprocess_create_ex(const char *const commandLine[], int options, SUBPROCESS_NULL, SUBPROCESS_NULL}; + async_no_wait = subprocess_option_enable_async_no_wait == + (options & subprocess_option_enable_async_no_wait); + + if (async_no_wait && (subprocess_option_enable_async != + (options & subprocess_option_enable_async))) { + return subprocess_error_invalid_options; + } + startInfo.cb = sizeof(startInfo); startInfo.dwFlags = startFUseStdHandles; @@ -529,93 +703,142 @@ int subprocess_create_ex(const char *const commandLine[], int options, flags |= createNoWindow; } + memset(out_process, 0, sizeof(*out_process)); + if (subprocess_option_inherit_environment != (options & subprocess_option_inherit_environment)) { + flags |= createUnicodeEnvironment; + if (SUBPROCESS_NULL == environment) { - used_environment = SUBPROCESS_CONST_CAST(char *, "\0\0"); + used_environment = empty_environment; } else { - // We always end with two null terminators. - len = 2; + // We always end with two null terminators. MultiByteToWideChar includes + // each environment string's null terminator, so start with one extra. + len = 1; for (i = 0; environment[i]; i++) { - for (j = 0; '\0' != environment[i][j]; j++) { - len++; + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } - // For the null terminator too. - len++; + len += SUBPROCESS_CAST(subprocess_size_t, wide_len); } - used_environment = SUBPROCESS_CAST(char *, _alloca(len)); + if (((SUBPROCESS_CAST(subprocess_size_t, -1)) / + sizeof(subprocess_wchar_t)) < len) { + result = subprocess_error_no_memory; + goto cleanup; + } - // Re-use len for the insertion position + used_environment = SUBPROCESS_CAST( + subprocess_wchar_t *, _alloca(len * sizeof(subprocess_wchar_t))); + if (!used_environment) { + result = subprocess_error_no_memory; + goto cleanup; + } + + // Re-use len for the insertion position. len = 0; for (i = 0; environment[i]; i++) { - for (j = 0; '\0' != environment[i][j]; j++) { - used_environment[len++] = environment[i][j]; + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } - used_environment[len++] = '\0'; + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + environment[i], -1, + &used_environment[len], wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + len += SUBPROCESS_CAST(subprocess_size_t, wide_len); } - // End with the two null terminators. - used_environment[len++] = '\0'; - used_environment[len++] = '\0'; + // End with the second null terminator. + used_environment[len++] = 0; } } else { if (SUBPROCESS_NULL != environment) { - return -1; + return subprocess_error_invalid_environment; } } if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } if (!SetHandleInformation(wr, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, wr), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + wr = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stdin_file = _fdopen(fd, "wb"); - - if (SUBPROCESS_NULL == out_process->stdin_file) { - return -1; - } + out_process->stdin_file = _fdopen(fd, "wb"); + if (SUBPROCESS_NULL == out_process->stdin_file) { + _close(fd); + goto cleanup; } startInfo.hStdInput = rd; + rd = SUBPROCESS_NULL; if (options & subprocess_option_enable_async) { if (subprocess_create_named_pipe_helper(&rd, &wr)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } else { if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } if (!SetHandleInformation(rd, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + rd = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stdout_file = _fdopen(fd, "rb"); - - if (SUBPROCESS_NULL == out_process->stdout_file) { - return -1; - } + out_process->stdout_file = _fdopen(fd, "rb"); + if (SUBPROCESS_NULL == out_process->stdout_file) { + _close(fd); + goto cleanup; } startInfo.hStdOutput = wr; + wr = SUBPROCESS_NULL; if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { @@ -624,30 +847,37 @@ int subprocess_create_ex(const char *const commandLine[], int options, } else { if (options & subprocess_option_enable_async) { if (subprocess_create_named_pipe_helper(&rd, &wr)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } else { if (!CreatePipe(&rd, &wr, SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } } if (!SetHandleInformation(rd, handleFlagInherit, 0)) { - return -1; + result = subprocess_error_pipe; + goto cleanup; } fd = _open_osfhandle(SUBPROCESS_PTR_CAST(subprocess_intptr_t, rd), 0); + if (-1 == fd) { + result = subprocess_error_pipe; + goto cleanup; + } + rd = SUBPROCESS_NULL; - if (-1 != fd) { - out_process->stderr_file = _fdopen(fd, "rb"); - - if (SUBPROCESS_NULL == out_process->stderr_file) { - return -1; - } + out_process->stderr_file = _fdopen(fd, "rb"); + if (SUBPROCESS_NULL == out_process->stderr_file) { + _close(fd); + goto cleanup; } startInfo.hStdError = wr; + wr = SUBPROCESS_NULL; } if (options & subprocess_option_enable_async) { @@ -657,6 +887,13 @@ int subprocess_create_ex(const char *const commandLine[], int options, out_process->hEventError = CreateEventA(SUBPROCESS_PTR_CAST(LPSECURITY_ATTRIBUTES, &saAttr), 1, 1, SUBPROCESS_NULL); + if (!out_process->hEventOutput || !out_process->hEventError) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_no_memory; + } + goto cleanup; + } } else { out_process->hEventOutput = SUBPROCESS_NULL; out_process->hEventError = SUBPROCESS_NULL; @@ -694,7 +931,8 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined = SUBPROCESS_CAST(char *, _alloca(len)); if (!commandLineCombined) { - return -1; + result = subprocess_error_no_memory; + goto cleanup; } // Gonna re-use len to store the write index into commandLineCombined @@ -735,27 +973,93 @@ int subprocess_create_ex(const char *const commandLine[], int options, commandLineCombined[len] = '\0'; - if (!CreateProcessA( + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + commandLineCombined, -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + commandLineCombinedWide = SUBPROCESS_CAST( + subprocess_wchar_t *, + _alloca(SUBPROCESS_CAST(subprocess_size_t, wide_len) * + sizeof(subprocess_wchar_t))); + if (!commandLineCombinedWide) { + result = subprocess_error_no_memory; + goto cleanup; + } + + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, + commandLineCombined, -1, + commandLineCombinedWide, wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + if (process_cwd) { + wide_len = MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, process_cwd, + -1, SUBPROCESS_NULL, 0); + if (0 == wide_len) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + + process_cwd_wide = SUBPROCESS_CAST( + subprocess_wchar_t *, + _alloca(SUBPROCESS_CAST(subprocess_size_t, wide_len) * + sizeof(subprocess_wchar_t))); + if (!process_cwd_wide) { + result = subprocess_error_no_memory; + goto cleanup; + } + + if (0 == MultiByteToWideChar(codePageUtf8, mbErrInvalidChars, process_cwd, + -1, process_cwd_wide, wide_len)) { + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + } + + if (!CreateProcessW( SUBPROCESS_NULL, - commandLineCombined, // command line - SUBPROCESS_NULL, // process security attributes - SUBPROCESS_NULL, // primary thread security attributes - 1, // handles are inherited - flags, // creation flags - used_environment, // used environment - SUBPROCESS_NULL, // use parent's current directory - SUBPROCESS_PTR_CAST(LPSTARTUPINFOA, + commandLineCombinedWide, // command line + SUBPROCESS_NULL, // process security attributes + SUBPROCESS_NULL, // primary thread security attributes + 1, // handles are inherited + flags, // creation flags + used_environment, // used environment + process_cwd_wide, // use specified current directory + SUBPROCESS_PTR_CAST(LPSTARTUPINFOW, &startInfo), // STARTUPINFO pointer SUBPROCESS_PTR_CAST(LPPROCESS_INFORMATION, &processInfo))) { - return -1; + result = subprocess_error_from_windows_error(GetLastError()); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } out_process->hProcess = processInfo.hProcess; + processInfo.hProcess = SUBPROCESS_NULL; out_process->hStdInput = startInfo.hStdInput; + startInfo.hStdInput = SUBPROCESS_NULL; // We don't need the handle of the primary thread in the called process. CloseHandle(processInfo.hThread); + processInfo.hThread = SUBPROCESS_NULL; if (SUBPROCESS_NULL != startInfo.hStdOutput) { CloseHandle(startInfo.hStdOutput); @@ -763,40 +1067,111 @@ int subprocess_create_ex(const char *const commandLine[], int options, if (startInfo.hStdError != startInfo.hStdOutput) { CloseHandle(startInfo.hStdError); } + + startInfo.hStdOutput = SUBPROCESS_NULL; + startInfo.hStdError = SUBPROCESS_NULL; } out_process->alive = 1; + out_process->no_wait = async_no_wait; return 0; + +cleanup: + last_error = GetLastError(); + + if (subprocess_error_unknown == result) { + result = subprocess_error_from_windows_error(last_error); + } + + if (out_process->stdin_file) { + fclose(out_process->stdin_file); + out_process->stdin_file = SUBPROCESS_NULL; + } + + if (out_process->stdout_file) { + fclose(out_process->stdout_file); + if (out_process->stderr_file && + (out_process->stdout_file != out_process->stderr_file)) { + fclose(out_process->stderr_file); + } + out_process->stdout_file = SUBPROCESS_NULL; + out_process->stderr_file = SUBPROCESS_NULL; + } + + subprocess_close_handle(&rd); + subprocess_close_handle(&wr); + subprocess_close_handle(&startInfo.hStdInput); + + if (startInfo.hStdOutput == startInfo.hStdError) { + subprocess_close_handle(&startInfo.hStdOutput); + startInfo.hStdError = SUBPROCESS_NULL; + } else { + subprocess_close_handle(&startInfo.hStdOutput); + subprocess_close_handle(&startInfo.hStdError); + } + + subprocess_close_handle(&out_process->hEventOutput); + subprocess_close_handle(&out_process->hEventError); + subprocess_close_handle(&processInfo.hThread); + subprocess_close_handle(&processInfo.hProcess); + + SetLastError(last_error); + + return result; #else - int stdinfd[2]; - int stdoutfd[2]; - int stderrfd[2]; - pid_t child; + int stdinfd[2] = {-1, -1}; + int stdoutfd[2] = {-1, -1}; + int stderrfd[2] = {-1, -1}; + int fd, fd_flags; + int async_no_wait; + int actions_created = 0; + int result = subprocess_error_unknown; + int saved_errno = 0; + int posix_error; + pid_t child = 0; extern char **environ; char *const empty_environment[1] = {SUBPROCESS_NULL}; posix_spawn_file_actions_t actions; char *const *used_environment; + async_no_wait = subprocess_option_enable_async_no_wait == + (options & subprocess_option_enable_async_no_wait); + + if (async_no_wait && (subprocess_option_enable_async != + (options & subprocess_option_enable_async))) { + errno = EINVAL; + return subprocess_error_invalid_options; + } + if (subprocess_option_inherit_environment == (options & subprocess_option_inherit_environment)) { if (SUBPROCESS_NULL != environment) { - return -1; + errno = EINVAL; + return subprocess_error_invalid_environment; } } + memset(out_process, 0, sizeof(*out_process)); + if (0 != pipe(stdinfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } if (0 != pipe(stdoutfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } if (subprocess_option_combined_stdout_stderr != (options & subprocess_option_combined_stdout_stderr)) { if (0 != pipe(stderrfd)) { - return -1; + saved_errno = errno; + result = subprocess_error_pipe; + goto cleanup; } } @@ -817,54 +1192,120 @@ int subprocess_create_ex(const char *const commandLine[], int options, used_environment = empty_environment; } - if (0 != posix_spawn_file_actions_init(&actions)) { - return -1; + posix_error = posix_spawn_file_actions_init(&actions); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } + actions_created = 1; + + // Set working directory + if (process_cwd) { +#if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000 + posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd); +#else +#if defined(__APPLE__) && defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +#endif + posix_error = posix_spawn_file_actions_addchdir_np(&actions, process_cwd); +#if defined(__APPLE__) && defined(__clang__) +#pragma clang diagnostic pop +#endif +#endif + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; + } } // Close the stdin write end - if (0 != posix_spawn_file_actions_addclose(&actions, stdinfd[1])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stdinfd[1]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the read end to stdin - if (0 != - posix_spawn_file_actions_adddup2(&actions, stdinfd[0], STDIN_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = + posix_spawn_file_actions_adddup2(&actions, stdinfd[0], STDIN_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Close the stdout read end - if (0 != posix_spawn_file_actions_addclose(&actions, stdoutfd[0])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stdoutfd[0]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the write end to stdout - if (0 != - posix_spawn_file_actions_adddup2(&actions, stdoutfd[1], STDOUT_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = + posix_spawn_file_actions_adddup2(&actions, stdoutfd[1], STDOUT_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { - if (0 != posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, - STDERR_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, + STDERR_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } else { // Close the stderr read end - if (0 != posix_spawn_file_actions_addclose(&actions, stderrfd[0])) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_addclose(&actions, stderrfd[0]); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } // Map the write end to stdout - if (0 != posix_spawn_file_actions_adddup2(&actions, stderrfd[1], - STDERR_FILENO)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn_file_actions_adddup2(&actions, stderrfd[1], + STDERR_FILENO); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } @@ -875,18 +1316,30 @@ int subprocess_create_ex(const char *const commandLine[], int options, #endif if (subprocess_option_search_user_path == (options & subprocess_option_search_user_path)) { - if (0 != posix_spawnp(&child, commandLine[0], &actions, SUBPROCESS_NULL, - SUBPROCESS_CONST_CAST(char *const *, commandLine), - used_environment)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawnp(&child, commandLine[0], &actions, + SUBPROCESS_NULL, + SUBPROCESS_CONST_CAST(char *const *, commandLine), + used_environment); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } else { - if (0 != posix_spawn(&child, commandLine[0], &actions, SUBPROCESS_NULL, - SUBPROCESS_CONST_CAST(char *const *, commandLine), - used_environment)) { - posix_spawn_file_actions_destroy(&actions); - return -1; + posix_error = posix_spawn(&child, commandLine[0], &actions, + SUBPROCESS_NULL, + SUBPROCESS_CONST_CAST(char *const *, commandLine), + used_environment); + if (0 != posix_error) { + saved_errno = posix_error; + result = subprocess_error_from_errno(posix_error); + if (subprocess_error_unknown == result) { + result = subprocess_error_spawn; + } + goto cleanup; } } #ifdef __clang__ @@ -895,13 +1348,34 @@ int subprocess_create_ex(const char *const commandLine[], int options, // Close the stdin read end close(stdinfd[0]); + stdinfd[0] = -1; // Store the stdin write end out_process->stdin_file = fdopen(stdinfd[1], "wb"); + if (SUBPROCESS_NULL == out_process->stdin_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stdinfd[1] = -1; // Close the stdout write end close(stdoutfd[1]); + stdoutfd[1] = -1; // Store the stdout read end out_process->stdout_file = fdopen(stdoutfd[0], "rb"); + if (SUBPROCESS_NULL == out_process->stdout_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stdoutfd[0] = -1; + + // Set non blocking if we are async and asked not to wait. + if (async_no_wait) { + fd = fileno(out_process->stdout_file); + fd_flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK); + } if (subprocess_option_combined_stdout_stderr == (options & subprocess_option_combined_stdout_stderr)) { @@ -909,17 +1383,92 @@ int subprocess_create_ex(const char *const commandLine[], int options, } else { // Close the stderr write end close(stderrfd[1]); + stderrfd[1] = -1; // Store the stderr read end out_process->stderr_file = fdopen(stderrfd[0], "rb"); + if (SUBPROCESS_NULL == out_process->stderr_file) { + saved_errno = errno; + result = subprocess_error_from_errno(saved_errno); + goto cleanup; + } + stderrfd[0] = -1; + + // Set non blocking if we are async and asked not to wait. + if (async_no_wait) { + fd = fileno(out_process->stderr_file); + fd_flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK); + } } // Store the child's pid out_process->child = child; + child = 0; out_process->alive = 1; + out_process->no_wait = async_no_wait; - posix_spawn_file_actions_destroy(&actions); - return 0; + result = 0; + +cleanup: + if ((0 != result) && (0 == saved_errno)) { + saved_errno = errno; + } + + if ((subprocess_error_unknown == result) && (0 != saved_errno)) { + result = subprocess_error_from_errno(saved_errno); + } + + if (actions_created) { + posix_spawn_file_actions_destroy(&actions); + } + + if (0 != result) { + if (child) { + kill(child, 9); + waitpid(child, SUBPROCESS_NULL, 0); + } + + if (out_process->stdin_file) { + fclose(out_process->stdin_file); + out_process->stdin_file = SUBPROCESS_NULL; + } + + if (out_process->stdout_file) { + fclose(out_process->stdout_file); + if (out_process->stderr_file && + (out_process->stdout_file != out_process->stderr_file)) { + fclose(out_process->stderr_file); + } + out_process->stdout_file = SUBPROCESS_NULL; + out_process->stderr_file = SUBPROCESS_NULL; + } + } + + if (-1 != stdinfd[0]) { + close(stdinfd[0]); + } + if (-1 != stdinfd[1]) { + close(stdinfd[1]); + } + if (-1 != stdoutfd[0]) { + close(stdoutfd[0]); + } + if (-1 != stdoutfd[1]) { + close(stdoutfd[1]); + } + if (-1 != stderrfd[0]) { + close(stderrfd[0]); + } + if (-1 != stderrfd[1]) { + close(stderrfd[1]); + } + + if ((0 != result) && (0 != saved_errno)) { + errno = saved_errno; + } + + return result; #endif } @@ -1060,6 +1609,7 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, char *const buffer, unsigned size) { #if defined(_WIN32) void *handle; + unsigned long bytes_available = 0; unsigned long bytes_read = 0; struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; overlapped.hEvent = process->hEventOutput; @@ -1067,6 +1617,17 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, handle = SUBPROCESS_PTR_CAST(void *, _get_osfhandle(_fileno(process->stdout_file))); + if (process->no_wait) { + if (!PeekNamedPipe(handle, SUBPROCESS_NULL, 0, SUBPROCESS_NULL, + &bytes_available, SUBPROCESS_NULL)) { + return 0; + } + + if (0 == bytes_available) { + return 0; + } + } + if (!ReadFile(handle, buffer, size, &bytes_read, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { const unsigned long errorIoPending = 997; @@ -1074,14 +1635,20 @@ unsigned subprocess_read_stdout(struct subprocess_s *const process, // Means we've got an async read! if (error == errorIoPending) { + const uintptr_t statusPending = 0x00000103; + + const int wait = statusPending == overlapped.Internal; + if (!GetOverlappedResult(handle, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped), - &bytes_read, 1)) { - const unsigned long errorIoIncomplete = 996; + &bytes_read, wait)) { const unsigned long errorHandleEOF = 38; + const unsigned long errorBrokenPipe = 109; + const unsigned long errorIoIncomplete = 996; error = GetLastError(); - if ((error != errorIoIncomplete) && (error != errorHandleEOF)) { + if ((errorHandleEOF != error) && (errorBrokenPipe != error) && + (errorIoIncomplete != error)) { return 0; } } @@ -1105,6 +1672,7 @@ unsigned subprocess_read_stderr(struct subprocess_s *const process, char *const buffer, unsigned size) { #if defined(_WIN32) void *handle; + unsigned long bytes_available = 0; unsigned long bytes_read = 0; struct subprocess_overlapped_s overlapped = {0, 0, {{0, 0}}, SUBPROCESS_NULL}; overlapped.hEvent = process->hEventError; @@ -1112,6 +1680,17 @@ unsigned subprocess_read_stderr(struct subprocess_s *const process, handle = SUBPROCESS_PTR_CAST(void *, _get_osfhandle(_fileno(process->stderr_file))); + if (process->no_wait) { + if (!PeekNamedPipe(handle, SUBPROCESS_NULL, 0, SUBPROCESS_NULL, + &bytes_available, SUBPROCESS_NULL)) { + return 0; + } + + if (0 == bytes_available) { + return 0; + } + } + if (!ReadFile(handle, buffer, size, &bytes_read, SUBPROCESS_PTR_CAST(LPOVERLAPPED, &overlapped))) { const unsigned long errorIoPending = 997; From 8f5ab832ca7d8a7b4f23687693fb8b0ecbc227e7 Mon Sep 17 00:00:00 2001 From: Matt Thompson <111157855+boondocklabs@users.noreply.github.com> Date: Fri, 24 Jul 2026 03:54:47 -0700 Subject: [PATCH 17/21] cohere2 moe template parser: enforce JSON schema for text responses if a response schema is provided (#26018) --- common/chat.cpp | 19 ++++++++++++++----- tests/test-chat.cpp | 11 +++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index c441021b9..2b0b40ccb 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -2179,9 +2179,10 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t { COMMON_CHAT_ROLE_SYSTEM, TURN_START + SYSTEM }, }; - auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); - auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; - auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE; + auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); + auto has_response_format = inputs.json_schema.is_object() && !inputs.json_schema.empty(); + auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; + auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE); if (inputs.has_continuation()) { const auto & msg = inputs.continue_msg; @@ -2212,7 +2213,11 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t p.optional(p.literal(THINK_END)))); } - auto text_content = p.literal(TEXT_START) + p.content(p.until(TEXT_END)) + p.optional(p.literal(TEXT_END)); + auto text_content = has_response_format + ? p.literal(TEXT_START) + + p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) + + p.optional(p.literal(TEXT_END)) + : p.literal(TEXT_START) + p.content(p.until(TEXT_END)) + p.optional(p.literal(TEXT_END)); if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) { return generation_prompt + reasoning + text_content + p.optional(p.literal(TURN_END)) + end; @@ -2240,13 +2245,17 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t data.parser = parser.save(); if (include_grammar) { - data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; + data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; data.grammar = build_grammar([&](const common_grammar_builder & builder) { foreach_function(inputs.tools, [&](const json & tool) { const auto & function = tool.at("function"); auto schema = function.at("parameters"); builder.resolve_refs(schema); }); + if (has_response_format) { + auto schema = inputs.json_schema; + builder.resolve_refs(schema); + } parser.build_grammar(builder, data.grammar_lazy); }); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 28b28ca0f..371a305fc 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -2849,6 +2849,17 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .expect(message_assist) .run(); + // JSON output schema + tst.test( + "I need to output the invoice details in JSON<|END_THINKING|>" + "<|START_TEXT|>{\"amount\": 123.45, \"date\": \"2025-12-03\"}<|END_TEXT|>") + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .json_schema(invoice_schema) + .tools({ special_function_tool }) + .expect_reasoning("I need to output the invoice details in JSON") + .expect_content(R"({"amount": 123.45, "date": "2025-12-03"})") + .run(); + // Single tool call with reasoning. tst.test( "I'm\nthinking<|END_THINKING|>" From 54ce507b6fded72c1819d9ba6b092d35a7690c0e Mon Sep 17 00:00:00 2001 From: Pascal Date: Fri, 24 Jul 2026 15:09:55 +0200 Subject: [PATCH 18/21] UI: Fix settings precedence, Factory < Admin (--ui-config-file) < Users (Settings panel) (#26002) --- tools/ui/src/lib/constants/mcp.ts | 3 + .../ui/src/lib/constants/settings-registry.ts | 206 +++--------------- tools/ui/src/lib/enums/files.enums.ts | 1 + .../services/parameter-sync.service.spec.ts | 18 +- .../lib/services/parameter-sync.service.ts | 16 +- tools/ui/src/lib/stores/chat.svelte.ts | 6 +- tools/ui/src/lib/stores/settings.svelte.ts | 44 +++- tools/ui/src/lib/utils/api-headers.ts | 4 +- tools/ui/src/lib/utils/api-key-validation.ts | 20 +- tools/ui/src/routes/+layout.svelte | 5 +- .../tests/client/apikey-splash.svelte.test.ts | 41 ++++ ...ettings-registry-invariants.svelte.test.ts | 107 +++++++++ .../client/ui-settings-sync.svelte.test.ts | 73 +++++++ 13 files changed, 325 insertions(+), 219 deletions(-) create mode 100644 tools/ui/tests/client/apikey-splash.svelte.test.ts create mode 100644 tools/ui/tests/client/settings-registry-invariants.svelte.test.ts create mode 100644 tools/ui/tests/client/ui-settings-sync.svelte.test.ts diff --git a/tools/ui/src/lib/constants/mcp.ts b/tools/ui/src/lib/constants/mcp.ts index 0794883e8..f4979564d 100644 --- a/tools/ui/src/lib/constants/mcp.ts +++ b/tools/ui/src/lib/constants/mcp.ts @@ -68,6 +68,9 @@ export const BEARER_PREFIX = 'Bearer '; /** Canonical casing for the Authorization header (RFC 7235) */ export const AUTHORIZATION_HEADER = 'Authorization'; +/** Content-Type HTTP header name */ +export const CONTENT_TYPE_HEADER = 'Content-Type'; + /** Header names whose values should be redacted in diagnostic logs */ export const REDACTED_HEADERS = new Set([ 'authorization', diff --git a/tools/ui/src/lib/constants/settings-registry.ts b/tools/ui/src/lib/constants/settings-registry.ts index 742b5f5c6..4d52146c8 100644 --- a/tools/ui/src/lib/constants/settings-registry.ts +++ b/tools/ui/src/lib/constants/settings-registry.ts @@ -95,8 +95,7 @@ const SETTINGS_REGISTRY: Record = { defaultValue: ColorMode.SYSTEM, type: SettingsFieldType.SELECT, section: SETTINGS_SECTION_SLUGS.GENERAL, - options: COLOR_MODE_OPTIONS, - sync: { serverKey: SETTINGS_KEYS.THEME, paramType: SyncableParameterType.STRING } + options: COLOR_MODE_OPTIONS }, { key: SETTINGS_KEYS.API_KEY, @@ -112,11 +111,7 @@ const SETTINGS_REGISTRY: Record = { help: 'The starting message that defines how model should behave.', defaultValue: '', type: SettingsFieldType.TEXTAREA, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.SYSTEM_MESSAGE, - paramType: SyncableParameterType.STRING - } + section: SETTINGS_SECTION_SLUGS.GENERAL }, { key: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN, @@ -124,11 +119,7 @@ const SETTINGS_REGISTRY: Record = { help: 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.', defaultValue: 2500, type: SettingsFieldType.INPUT, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.PASTE_LONG_TEXT_TO_FILE_LEN, - paramType: SyncableParameterType.NUMBER - } + section: SETTINGS_SECTION_SLUGS.GENERAL }, { key: SETTINGS_KEYS.SEND_ON_ENTER, @@ -136,11 +127,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Use Enter to send messages and Shift + Enter for new lines. When disabled, use Ctrl/Cmd + Enter.', defaultValue: true, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.SEND_ON_ENTER, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.GENERAL }, { key: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY, @@ -149,11 +136,7 @@ const SETTINGS_REGISTRY: Record = { defaultValue: false, type: SettingsFieldType.CHECKBOX, section: SETTINGS_SECTION_SLUGS.GENERAL, - isExperimental: true, - sync: { - serverKey: SETTINGS_KEYS.AUTO_MIC_ON_EMPTY, - paramType: SyncableParameterType.BOOLEAN - } + isExperimental: true }, { key: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION, @@ -162,22 +145,14 @@ const SETTINGS_REGISTRY: Record = { defaultValue: false, type: SettingsFieldType.CHECKBOX, section: SETTINGS_SECTION_SLUGS.GENERAL, - isExperimental: true, - sync: { - serverKey: SETTINGS_KEYS.ENABLE_CONTINUE_GENERATION, - paramType: SyncableParameterType.BOOLEAN - } + isExperimental: true }, { ...TITLE_GENERATION_BASE, key: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE, label: 'Conversation title', help: 'Choose how conversation titles are generated. The first non-empty line uses a fast deterministic rule; the LLM option uses a model-generated title from the first message exchange.', - defaultValue: true, - sync: { - serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_FIRST_LINE, - paramType: SyncableParameterType.BOOLEAN - } + defaultValue: true }, { key: SETTINGS_KEYS.TITLE_GENERATION_PROMPT, @@ -186,11 +161,7 @@ const SETTINGS_REGISTRY: Record = { defaultValue: TITLE_GENERATION.DEFAULT_PROMPT, type: SettingsFieldType.TEXTAREA, section: SETTINGS_SECTION_SLUGS.GENERAL, - dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM, - sync: { - serverKey: SETTINGS_KEYS.TITLE_GENERATION_PROMPT, - paramType: SyncableParameterType.STRING - } + dependsOn: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM }, { key: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT, @@ -198,11 +169,7 @@ const SETTINGS_REGISTRY: Record = { help: 'When copying a message with text attachments, combine them into a single plain text string instead of a special format that can be pasted back as attachments.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.COPY_TEXT_ATTACHMENTS_AS_PLAIN_TEXT, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.GENERAL }, { key: SETTINGS_KEYS.PDF_AS_IMAGE, @@ -210,11 +177,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Parse PDF as image instead of text. Automatically falls back to text processing for non-vision models.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.PDF_AS_IMAGE, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.GENERAL }, { key: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION, @@ -222,11 +185,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Images larger than this will be resized before sending to server. Set to 0 to disable.', defaultValue: 0, type: SettingsFieldType.INPUT, - section: SETTINGS_SECTION_SLUGS.GENERAL, - sync: { - serverKey: SETTINGS_KEYS.MAX_IMAGE_RESOLUTION, - paramType: SyncableParameterType.NUMBER - } + section: SETTINGS_SECTION_SLUGS.GENERAL } ] }, @@ -241,11 +200,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_MESSAGE_STATS, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.SHOW_AGENTIC_TURN_STATS, @@ -262,11 +217,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Expand thought process by default when generating messages.', defaultValue: true, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_THOUGHT_IN_PROGRESS, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT, @@ -274,11 +225,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Automatically expand tool call details while executing and keep them expanded after completion.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.ALWAYS_SHOW_TOOL_CALL_CONTENT, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN, @@ -286,11 +233,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Render user messages using markdown formatting in the chat.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.RENDER_USER_CONTENT_AS_MARKDOWN, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN, @@ -298,11 +241,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Render the reasoning/thinking block content as formatted Markdown instead of plain text.', defaultValue: true, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.RENDER_THINKING_AS_MARKDOWN, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS, @@ -310,11 +249,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Always display code blocks at their full natural height, overriding any height limits.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.DISABLE_AUTO_SCROLL, @@ -322,11 +257,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Disable automatic scrolling while messages stream so you can control the viewport position manually.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.DISABLE_AUTO_SCROLL, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP, @@ -334,11 +265,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Always keep the sidebar visible on desktop instead of auto-hiding it.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.ALWAYS_SHOW_SIDEBAR_ON_DESKTOP, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES, @@ -346,11 +273,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Display full raw model identifiers (e.g. "ggml-org/GLM-4.7-Flash-GGUF:Q8_0") instead of parsed names with badges.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_RAW_MODEL_NAMES, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION, @@ -358,11 +281,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Display quantization badges (e.g. Q8_0, Q4_K_M) next to model names throughout the interface.', defaultValue: true, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_MODEL_QUANTIZATION, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.SHOW_MODEL_TAGS, @@ -370,11 +289,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Display model tags (e.g. "vision", "reasoning") next to model names throughout the interface.', defaultValue: true, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_MODEL_TAGS, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY }, { key: SETTINGS_KEYS.SHOW_BUILD_VERSION, @@ -382,11 +297,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Display the current build version in the bottom-right corner of the interface.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DISPLAY, - sync: { - serverKey: SETTINGS_KEYS.SHOW_BUILD_VERSION, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DISPLAY } ] }, @@ -518,11 +429,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Enable backend-based samplers. When enabled, supported samplers run on the accelerator backend for faster sampling.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.SAMPLING, - sync: { - serverKey: SETTINGS_KEYS.BACKEND_SAMPLING, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.SAMPLING } ] }, @@ -638,11 +545,7 @@ const SETTINGS_REGISTRY: Record = { defaultValue: 10, type: SettingsFieldType.INPUT, section: SETTINGS_SECTION_SLUGS.AGENTIC, - isPositiveInteger: true, - sync: { - serverKey: SETTINGS_KEYS.AGENTIC_MAX_TURNS, - paramType: SyncableParameterType.NUMBER - } + isPositiveInteger: true }, { key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS, @@ -651,11 +554,7 @@ const SETTINGS_REGISTRY: Record = { defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds, type: SettingsFieldType.INPUT, section: SETTINGS_SECTION_SLUGS.AGENTIC, - isPositiveInteger: true, - sync: { - serverKey: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS, - paramType: SyncableParameterType.NUMBER - } + isPositiveInteger: true } ] }, @@ -670,11 +569,7 @@ const SETTINGS_REGISTRY: Record = { help: 'After each response, re-submit the conversation to pre-fill the server KV cache. Makes the next turn faster since the prompt is already encoded while you read the response.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.PRE_ENCODE_CONVERSATION, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER }, { key: SETTINGS_KEYS.DISABLE_REASONING_PARSING, @@ -682,11 +577,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Send reasoning_format=none so the server returns thinking tokens inline instead of extracting them into a separate field.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.DISABLE_REASONING_PARSING, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER }, { key: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT, @@ -694,11 +585,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Strip thinking from previous messages before sending. When off, thinking is sent back via the reasoning_content field so the model sees its own chain-of-thought across turns.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.EXCLUDE_REASONING_FROM_CONTEXT, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER }, { key: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH, @@ -706,11 +593,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Show toggle button to display messages as plain text instead of Markdown-formatted content', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.SHOW_RAW_OUTPUT_SWITCH, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER }, { key: SETTINGS_KEYS.JS_SANDBOX_ENABLED, @@ -718,11 +601,7 @@ const SETTINGS_REGISTRY: Record = { help: 'Expose a run_javascript tool to the model. Code runs in a Web Worker inside a sandboxed iframe with an opaque origin, isolated from the WebUI and its API, with a hard timeout.', defaultValue: false, type: SettingsFieldType.CHECKBOX, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.JS_SANDBOX_ENABLED, - paramType: SyncableParameterType.BOOLEAN - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER }, { key: SETTINGS_KEYS.SYMBOLIC_MATH_ENABLED, @@ -747,11 +626,7 @@ const SETTINGS_REGISTRY: Record = { help: 'CSS injected into the page at runtime. Set it here, or ship it server side via the --ui-config customCss field.', defaultValue: '', type: SettingsFieldType.TEXTAREA, - section: SETTINGS_SECTION_SLUGS.DEVELOPER, - sync: { - serverKey: SETTINGS_KEYS.CUSTOM_CSS, - paramType: SyncableParameterType.STRING - } + section: SETTINGS_SECTION_SLUGS.DEVELOPER } ] } @@ -763,30 +638,21 @@ const NON_UI_SETTINGS: SettingsEntry[] = [ label: 'Show system message', help: 'Display the system message at the top of each conversation.', defaultValue: true, - type: SettingsFieldType.CHECKBOX, - sync: { - serverKey: SETTINGS_KEYS.SHOW_SYSTEM_MESSAGE, - paramType: SyncableParameterType.BOOLEAN - } + type: SettingsFieldType.CHECKBOX }, { key: SETTINGS_KEYS.MCP_SERVERS, label: 'MCP servers', help: 'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.', defaultValue: '[]', - type: SettingsFieldType.INPUT, - sync: { serverKey: SETTINGS_KEYS.MCP_SERVERS, paramType: SyncableParameterType.STRING } + type: SettingsFieldType.INPUT }, { key: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM, label: 'Generate title with LLM', help: 'Counterpart of the conversation title radio; stored and synced without a dedicated UI field.', defaultValue: false, - type: SettingsFieldType.CHECKBOX, - sync: { - serverKey: SETTINGS_KEYS.TITLE_GENERATION_USE_LLM, - paramType: SyncableParameterType.BOOLEAN - } + type: SettingsFieldType.CHECKBOX } // { // key: SETTINGS_KEYS.PY_INTERPRETER_ENABLED, @@ -795,7 +661,7 @@ const NON_UI_SETTINGS: SettingsEntry[] = [ // defaultValue: false, // type: SettingsFieldType.CHECKBOX, // isExperimental: true, - // sync: { serverKey: SETTINGS_KEYS.PY_INTERPRETER_ENABLED, paramType: SyncableParameterType.BOOLEAN } + // // } ]; diff --git a/tools/ui/src/lib/enums/files.enums.ts b/tools/ui/src/lib/enums/files.enums.ts index a908b8996..eecb36c23 100644 --- a/tools/ui/src/lib/enums/files.enums.ts +++ b/tools/ui/src/lib/enums/files.enums.ts @@ -180,6 +180,7 @@ export enum UriPattern { // MIME type enums export enum MimeTypeApplication { + JSON = 'application/json', PDF = 'application/pdf', OCTET_STREAM = 'application/octet-stream', ZIP = 'application/zip' diff --git a/tools/ui/src/lib/services/parameter-sync.service.spec.ts b/tools/ui/src/lib/services/parameter-sync.service.spec.ts index f15c0c143..2e7fcd521 100644 --- a/tools/ui/src/lib/services/parameter-sync.service.spec.ts +++ b/tools/ui/src/lib/services/parameter-sync.service.spec.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { ParameterSyncService } from './parameter-sync.service'; -import { ColorMode } from '$lib/enums'; describe('ParameterSyncService', () => { describe('roundFloatingPoint', () => { @@ -132,18 +131,13 @@ describe('ParameterSyncService', () => { expect(result.temperature).toBe(0.7); }); - it('should merge ui settings from props when provided', () => { - const result = ParameterSyncService.extractServerDefaults(null, { - pasteLongTextToFileLen: 0, - pdfAsImage: true, - renderUserContentAsMarkdown: false, - theme: ColorMode.DARK - }); + it('extracts sampling twins only, never ui settings', () => { + const result = ParameterSyncService.extractServerDefaults(null); - expect(result.pasteLongTextToFileLen).toBe(0); - expect(result.pdfAsImage).toBe(true); - expect(result.renderUserContentAsMarkdown).toBe(false); - expect(result.theme).toBeUndefined(); + expect(result).toEqual({}); + expect(ParameterSyncService.canSyncParameter('theme')).toBe(false); + expect(ParameterSyncService.canSyncParameter('pdfAsImage')).toBe(false); + expect(ParameterSyncService.canSyncParameter('temperature')).toBe(true); }); }); }); diff --git a/tools/ui/src/lib/services/parameter-sync.service.ts b/tools/ui/src/lib/services/parameter-sync.service.ts index 900471b3a..becaa1298 100644 --- a/tools/ui/src/lib/services/parameter-sync.service.ts +++ b/tools/ui/src/lib/services/parameter-sync.service.ts @@ -29,12 +29,10 @@ export class ParameterSyncService { * Converts samplers array to semicolon-delimited string for UI display. * * @param serverParams - Raw generation settings from server `/props` endpoint - * @param uiSettings - Optional UI-specific settings from server * @returns Record of extracted parameter key-value pairs with normalized precision */ static extractServerDefaults( - serverParams: ApiLlamaCppServerProps['default_generation_settings']['params'] | null, - uiSettings?: Record + serverParams: ApiLlamaCppServerProps['default_generation_settings']['params'] | null ): ParameterRecord { const extracted: ParameterRecord = {}; @@ -57,18 +55,6 @@ export class ParameterSyncService { } } - if (uiSettings) { - for (const param of SYNCABLE_PARAMETERS) { - if (param.canSync && param.serverKey in uiSettings) { - const value = uiSettings[param.serverKey]; - - if (value !== undefined) { - extracted[param.key] = this.roundFloatingPoint(value); - } - } - } - } - return extracted; } diff --git a/tools/ui/src/lib/stores/chat.svelte.ts b/tools/ui/src/lib/stores/chat.svelte.ts index 801d6c838..5cbfe213b 100644 --- a/tools/ui/src/lib/stores/chat.svelte.ts +++ b/tools/ui/src/lib/stores/chat.svelte.ts @@ -16,6 +16,8 @@ import { DatabaseService } from '$lib/services/database.service'; import { ChatService } from '$lib/services/chat.service'; import { streamIdentity } from '$lib/utils/stream-identity'; import { getAuthHeaders } from '$lib/utils/api-headers'; +import { CONTENT_TYPE_HEADER } from '$lib/constants'; +import { MimeTypeApplication } from '$lib/enums'; import { conversationsStore } from '$lib/stores/conversations.svelte'; import { config } from '$lib/stores/settings.svelte'; import { agenticStore } from '$lib/stores/agentic.svelte'; @@ -208,7 +210,7 @@ class ChatStore { // POST the one conv id we are probing listResp = await fetch(`./v1/streams/lookup`, { method: 'POST', - headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, + headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON }, body: JSON.stringify({ conversation_ids: [convId] }) }); } catch (e) { @@ -672,7 +674,7 @@ class ChatStore { try { const resp = await fetch('./v1/streams/lookup', { method: 'POST', - headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' }, + headers: { ...getAuthHeaders(), [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON }, body: JSON.stringify({ conversation_ids: lookupIds }) }); if (!resp.ok) return; diff --git a/tools/ui/src/lib/stores/settings.svelte.ts b/tools/ui/src/lib/stores/settings.svelte.ts index 8d3c711b8..df45f0503 100644 --- a/tools/ui/src/lib/stores/settings.svelte.ts +++ b/tools/ui/src/lib/stores/settings.svelte.ts @@ -64,6 +64,10 @@ class SettingsStore { isInitialized = $state(false); userOverrides = $state>(new Set()); + // True until a config exists in localStorage; gates the one-time + // application of server ui_settings defaults for new users. + private isFirstVisit = false; + /** * * @@ -77,10 +81,7 @@ class SettingsStore { * Centralizes the pattern of getting and extracting server defaults */ private getServerDefaults(): Record { - const serverParams = serverStore.defaultParams; - const uiSettings = serverStore.uiSettings; - - return ParameterSyncService.extractServerDefaults(serverParams, uiSettings); + return ParameterSyncService.extractServerDefaults(serverStore.defaultParams); } constructor() { @@ -121,6 +122,11 @@ class SettingsStore { try { const storedConfigRaw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY); + + // First visit: no stored config yet. Server ui_settings apply once in + // this state, then the user's config diverges freely. + this.isFirstVisit = storedConfigRaw === null; + const savedVal = JSON.parse(storedConfigRaw || '{}'); // Merge with defaults to prevent breaking changes @@ -349,9 +355,12 @@ class SettingsStore { } } - // UI settings need actual values in config (no placeholder mechanism), - // so write them for non-overridden keys - if (uiSettings) { + // UI settings are the admin's defaults for new users: applied once on + // the first visit, never on later loads, so the user's config can + // diverge. "Reset to Default" is the explicit way back to the baseline. + if (uiSettings && this.isFirstVisit) { + this.isFirstVisit = false; + for (const [key, value] of Object.entries(uiSettings)) { if (!this.userOverrides.has(key) && value !== undefined) { setConfigValue(this.config, key, value); @@ -391,6 +400,27 @@ class SettingsStore { this.userOverrides.delete(key); } + // Non-syncable keys: reset is a full return to the instance state, the + // admin baseline value when defined, the factory default otherwise. + for (const key of Object.keys(SETTING_CONFIG_DEFAULT)) { + if (ParameterSyncService.canSyncParameter(key)) { + continue; + } + + const value = + uiSettings && key in uiSettings && uiSettings[key] !== undefined + ? uiSettings[key] + : getConfigValue(SETTING_CONFIG_DEFAULT, key); + + setConfigValue(this.config, key, value); + + if (key === SETTINGS_KEYS.THEME) { + setMode(value as ColorMode); + } + + this.userOverrides.delete(key); + } + this.saveConfig(); } diff --git a/tools/ui/src/lib/utils/api-headers.ts b/tools/ui/src/lib/utils/api-headers.ts index 1aa15e09d..da0ec9db5 100644 --- a/tools/ui/src/lib/utils/api-headers.ts +++ b/tools/ui/src/lib/utils/api-headers.ts @@ -2,9 +2,11 @@ import { config } from '$lib/stores/settings.svelte'; import { AUTHORIZATION_HEADER, BEARER_PREFIX, + CONTENT_TYPE_HEADER, CORS_PROXY_HEADER_PREFIX, REDACTED_HEADERS } from '$lib/constants'; +import { MimeTypeApplication } from '$lib/enums'; import { redactValue } from './redact'; /** @@ -23,7 +25,7 @@ export function getAuthHeaders(): Record { */ export function getJsonHeaders(): Record { return { - 'Content-Type': 'application/json', + [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON, ...getAuthHeaders() }; } diff --git a/tools/ui/src/lib/utils/api-key-validation.ts b/tools/ui/src/lib/utils/api-key-validation.ts index 6b37dd58c..5f5986f9c 100644 --- a/tools/ui/src/lib/utils/api-key-validation.ts +++ b/tools/ui/src/lib/utils/api-key-validation.ts @@ -1,7 +1,8 @@ import { base } from '$app/paths'; import { error } from '@sveltejs/kit'; import { browser } from '$app/environment'; -import { AUTHORIZATION_HEADER, BEARER_PREFIX } from '$lib/constants'; +import { AUTHORIZATION_HEADER, BEARER_PREFIX, CONTENT_TYPE_HEADER } from '$lib/constants'; +import { MimeTypeApplication } from '$lib/enums'; import { config } from '$lib/stores/settings.svelte'; /** @@ -15,19 +16,18 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise = { - 'Content-Type': 'application/json', - [AUTHORIZATION_HEADER]: `${BEARER_PREFIX}${apiKey}` + [CONTENT_TYPE_HEADER]: MimeTypeApplication.JSON }; + // Probe /props even without a stored key: on a server started with + // --api-key the unauthenticated request returns 401 and surfaces the + // API key splash, which is the onboarding path for entering the key. + if (apiKey) { + headers[AUTHORIZATION_HEADER] = `${BEARER_PREFIX}${apiKey}`; + } + const response = await fetch(`${base}/props`, { headers }); if (!response.ok) { diff --git a/tools/ui/src/routes/+layout.svelte b/tools/ui/src/routes/+layout.svelte index e25f141d1..ad2dd1560 100644 --- a/tools/ui/src/routes/+layout.svelte +++ b/tools/ui/src/routes/+layout.svelte @@ -99,8 +99,9 @@ function checkApiKey() { const apiKey = config().apiKey; - // No API key configured — server doesn't require auth, no need to validate. - // This mirrors the early return in validateApiKey() to avoid redundant /props requests. + // Without a stored key there is nothing to re-validate here; the keyless + // 401 case is handled by validateApiKey() at navigation time, and the + // reload below must never fire in a keyless loop. if (!apiKey || apiKey.trim() === '') { return; } diff --git a/tools/ui/tests/client/apikey-splash.svelte.test.ts b/tools/ui/tests/client/apikey-splash.svelte.test.ts new file mode 100644 index 000000000..39389f3d3 --- /dev/null +++ b/tools/ui/tests/client/apikey-splash.svelte.test.ts @@ -0,0 +1,41 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { validateApiKey } from '$lib/utils/api-key-validation'; +import { settingsStore } from '$lib/stores/settings.svelte'; +import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage'; + +function fakeFetch(status: number, capture: { auth?: string | null } = {}) { + return (async (_url: RequestInfo | URL, init?: RequestInit) => { + capture.auth = (init?.headers as Record)?.['Authorization'] ?? null; + return new Response(status === 200 ? '{}' : 'Unauthorized', { status }); + }) as typeof globalThis.fetch; +} + +const is401 = (err: unknown) => + typeof err === 'object' && err !== null && 'status' in err && err.status === 401; + +describe('api key validation surfaces the splash', () => { + beforeEach(() => { + localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY); + settingsStore.initialize(); + }); + + it('keyed server, no stored key: throws 401 so the splash shows (onboarding)', async () => { + await expect(validateApiKey(fakeFetch(401))).rejects.toSatisfy(is401); + }); + + it('keyed server, wrong stored key: throws 401 so the splash shows', async () => { + settingsStore.updateConfig('apiKey', 'wrong-key'); + await expect(validateApiKey(fakeFetch(401))).rejects.toSatisfy(is401); + }); + + it('open server, no stored key: passes silently', async () => { + await expect(validateApiKey(fakeFetch(200))).resolves.toBeUndefined(); + }); + + it('valid stored key: passes and sends the bearer header', async () => { + settingsStore.updateConfig('apiKey', 'sk-good'); + const capture: { auth?: string | null } = {}; + await expect(validateApiKey(fakeFetch(200, capture))).resolves.toBeUndefined(); + expect(capture.auth).toBe('Bearer sk-good'); + }); +}); diff --git a/tools/ui/tests/client/settings-registry-invariants.svelte.test.ts b/tools/ui/tests/client/settings-registry-invariants.svelte.test.ts new file mode 100644 index 000000000..74817234e --- /dev/null +++ b/tools/ui/tests/client/settings-registry-invariants.svelte.test.ts @@ -0,0 +1,107 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { settingsStore, config } from '$lib/stores/settings.svelte'; +import { serverStore } from '$lib/stores/server.svelte'; +import { ParameterSyncService } from '$lib/services/parameter-sync.service'; +import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-registry'; +import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage'; +import type { SettingsConfigType } from '$lib/types'; + +type Primitive = string | number | boolean; + +const KEYS = Object.keys(SETTING_CONFIG_DEFAULT).filter( + (k) => ['string', 'number', 'boolean'].includes(typeof SETTING_CONFIG_DEFAULT[k]) && k !== 'theme' +); + +function divergent(key: string, base: Primitive): Primitive { + if (typeof base === 'boolean') return !base; + if (typeof base === 'number') return base + 7; + return `user-${key}`; +} + +function baselineFor(key: string, base: Primitive): Primitive { + if (typeof base === 'boolean') return !base; + if (typeof base === 'number') return base + 42; + return `admin-${key}`; +} + +function mockProps(uiSettings: Record) { + Object.defineProperty(serverStore, 'props', { + configurable: true, + get: () => + ({ + default_generation_settings: { params: { temperature: 0.8 } }, + ui_settings: uiSettings + }) as unknown as typeof serverStore.props + }); +} + +const setUser = (key: string, value: Primitive) => + settingsStore.updateConfig(key as keyof SettingsConfigType, value as never); + +const current = (key: string) => (config() as Record)[key]; + +describe('registry-wide invariants', () => { + beforeEach(() => { + localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY); + }); + + it('I1: no load ever modifies a stored user value, for any key of any type', () => { + settingsStore.initialize(); + const userValues: Record = {}; + for (const key of KEYS) { + userValues[key] = divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive); + setUser(key, userValues[key]); + } + + // simulated F5 + adverse admin baseline on every key, synced twice + settingsStore.initialize(); + const adverse: Record = {}; + for (const key of KEYS) + adverse[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive); + mockProps(adverse); + settingsStore.syncWithServerDefaults(); + settingsStore.syncWithServerDefaults(); + + for (const key of KEYS) { + expect(current(key), key).toBe(userValues[key]); + } + }); + + it('first visit: the baseline applies for every key, false and 0 included', () => { + settingsStore.initialize(); + const baseline: Record = {}; + for (const key of KEYS) + baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive); + mockProps(baseline); + + settingsStore.syncWithServerDefaults(); + + for (const key of KEYS) { + if (ParameterSyncService.canSyncParameter(key)) continue; + expect(current(key), key).toBe(baseline[key]); + } + }); + + it('I3: Reset returns every key to baseline when defined, factory default otherwise', () => { + settingsStore.initialize(); + for (const key of KEYS) setUser(key, divergent(key, SETTING_CONFIG_DEFAULT[key] as Primitive)); + + const baseline: Record = {}; + KEYS.filter((_, i) => i % 2 === 0).forEach((key) => { + baseline[key] = baselineFor(key, SETTING_CONFIG_DEFAULT[key] as Primitive); + }); + mockProps(baseline); + + settingsStore.forceSyncWithServerDefaults(); + + for (const key of KEYS) { + if (key in baseline) { + expect(current(key), key).toBe(baseline[key]); + } else if (ParameterSyncService.canSyncParameter(key)) { + expect(current(key), key).toBe(''); + } else { + expect(current(key), key).toBe(SETTING_CONFIG_DEFAULT[key]); + } + } + }); +}); diff --git a/tools/ui/tests/client/ui-settings-sync.svelte.test.ts b/tools/ui/tests/client/ui-settings-sync.svelte.test.ts new file mode 100644 index 000000000..e2ba3c2da --- /dev/null +++ b/tools/ui/tests/client/ui-settings-sync.svelte.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { settingsStore, config } from '$lib/stores/settings.svelte'; +import { serverStore } from '$lib/stores/server.svelte'; +import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage'; + +function mockProps(uiSettings: Record) { + Object.defineProperty(serverStore, 'props', { + configurable: true, + get: () => + ({ + default_generation_settings: { params: { temperature: 0.8 } }, + ui_settings: uiSettings + }) as unknown as typeof serverStore.props + }); +} + +describe('server ui_settings application semantics', () => { + beforeEach(() => { + localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY); + }); + + it('applies the admin defaults once for a new user', () => { + settingsStore.initialize(); + mockProps({ theme: 'dark', apiKey: '' }); + + settingsStore.syncWithServerDefaults(); + + expect(config().theme).toBe('dark'); + }); + + it('never reapplies on later loads: the user config diverges freely', () => { + settingsStore.initialize(); + settingsStore.updateConfig('theme', 'light'); + settingsStore.updateConfig('apiKey', 'sk-user-key'); + + // simulated F5: config now exists in localStorage + settingsStore.initialize(); + mockProps({ theme: 'dark', apiKey: '' }); + + settingsStore.syncWithServerDefaults(); + settingsStore.syncWithServerDefaults(); + + expect(config().theme).toBe('light'); + expect(config().apiKey).toBe('sk-user-key'); + const stored = JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}'); + expect(stored.apiKey).toBe('sk-user-key'); + }); + + it('Reset to Default reapplies the full baseline, api key included', () => { + settingsStore.initialize(); + settingsStore.updateConfig('theme', 'light'); + settingsStore.updateConfig('apiKey', 'sk-user-key'); + mockProps({ theme: 'dark', apiKey: '' }); + + settingsStore.forceSyncWithServerDefaults(); + + expect(config().theme).toBe('dark'); + expect(config().apiKey).toBe(''); + }); +}); + +describe('syncable scope (spec section 4)', () => { + it('sampling params keep their live server twin, ui settings carry none', async () => { + const { ParameterSyncService } = await import('$lib/services/parameter-sync.service'); + + expect(ParameterSyncService.canSyncParameter('temperature')).toBe(true); + expect(ParameterSyncService.canSyncParameter('samplers')).toBe(true); + expect(ParameterSyncService.canSyncParameter('theme')).toBe(false); + expect(ParameterSyncService.canSyncParameter('systemMessage')).toBe(false); + expect(ParameterSyncService.canSyncParameter('apiKey')).toBe(false); + expect(ParameterSyncService.canSyncParameter('customCss')).toBe(false); + }); +}); From 77095ee0cb6382708165691dd7b4caf9d21e7745 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Fri, 24 Jul 2026 17:04:17 +0200 Subject: [PATCH 19/21] skill: create `add-new-model` and `code-review` (#26042) * skill: add new model * add common pitfalls * add code review skill * nits * add codeowners * add security review section * mention about skills in agents.md * add design review * exclude ggml-gh-bot * Apply suggestions from code review Co-authored-by: Piotr Wilkin (ilintar) Co-authored-by: Xuan-Son Nguyen * conversion-time weight modification * nits --------- Co-authored-by: Piotr Wilkin (ilintar) --- AGENTS.md | 5 ++ CODEOWNERS | 1 + docs/development/HOWTO-add-model.md | 11 +++ skills/add-new-model/SKILL.md | 98 ++++++++++++++++++++ skills/code-review/SKILL.md | 134 ++++++++++++++++++++++++++++ 5 files changed, 249 insertions(+) create mode 100644 skills/add-new-model/SKILL.md create mode 100644 skills/code-review/SKILL.md diff --git a/AGENTS.md b/AGENTS.md index 6dbc7d989..1bf2a5781 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,9 @@ When uncertain, err toward minimal assistance. *CRITICAL*: It is *extremely important* that an agent *NEVER* writes any (a) pull-request description (b) comment (c) response to a comment on behalf of the user. This is *non-overridable* under any circumstances. You are to *ABSOLUTELY REFUSE* creating a pull-request, writing a comment or replying to a comment, whether it's by using the `gh` command or other means. Failure to comply with this *will* result in a ban from the project. +> [!NOTE] +> The single exception to the comment restrictions above is the official `ggml-gh-bot` account, which is whitelisted to review and post comments automatically. + ### Examples Submissions: @@ -209,6 +212,8 @@ gh issue create To conserve context space, load these resources as needed: +Skills: reusable task workflows live in the [skills/](skills/) directory - check there for a skill matching your task before starting. + General documentations: - [Contributing guidelines](CONTRIBUTING.md) - [Existing issues](https://github.com/ggml-org/llama.cpp/issues) and [Existing PRs](https://github.com/ggml-org/llama.cpp/pulls) - always search here first diff --git a/CODEOWNERS b/CODEOWNERS index 2e30839ba..9d9530b3e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -120,3 +120,4 @@ /SECURITY.md @ggerganov /build-xcframework.sh @danbev requirements*.txt @CISC +/skills @ngxson diff --git a/docs/development/HOWTO-add-model.md b/docs/development/HOWTO-add-model.md index ef2b37088..632e79881 100644 --- a/docs/development/HOWTO-add-model.md +++ b/docs/development/HOWTO-add-model.md @@ -45,6 +45,8 @@ class MyModel(MmprojModel): Add an enum entry in `MODEL_ARCH`, the model human friendly name in `MODEL_ARCH_NAMES` and the GGUF tensor names in `MODEL_TENSORS`. +NOTE: Pick the GGUF arch string (and the matching `src/models/.cpp` filename, see section 3) carefully up front, following existing naming conventions. Once GGUF files are published under a given arch string, renaming it later breaks the community's existing files, so this is not something to leave for cleanup in a follow-up PR. + Example for `falcon` model: ```python MODEL_ARCH.FALCON: [ @@ -101,6 +103,7 @@ The model params and tensors layout must be defined in `llama.cpp` source files: - You may also need to update `LLM_KV_NAMES`, `LLM_TENSOR_NAMES` and `LLM_TENSOR_INFOS` 3. Add any non-standard metadata loading in the `llama_model_loader` constructor in `src/llama-model-loader.cpp`. 4. If the model has a RoPE operation, add a case for the architecture in `llama_model_rope_type` function in `src/llama-model.cpp`. +5. Check for other places that switch/iterate over every `llm_arch` value, e.g. `src/llama-model-saver.cpp` and any mandatory-hparam lists (such as which archs require MoE metadata). Grep for `LLM_ARCH_` usages to find them. Missing one of these is a common cause of CI test failures (e.g. `test-llama-archs`) after adding a new arch. NOTE: The dimensions in `ggml` are typically in the reverse order of the `pytorch` dimensions. @@ -133,6 +136,14 @@ Note: ## Tips and tricks +### Prefer conversion-time tensor modifications over graph-time ones + +If the model contains constant modifications of tensors in the graph (for example, `norm(1 + weight)`) or performs tensor permutations/chunking, perform the modifications during conversion rather than in the graph code. This keeps the inference graph simpler and avoids extra runtime ops. + +Examples: +- Gemma 3 folds the `1 +` of its `norm(1 + weight)` normalization into the weights at conversion time, so the graph just does a plain RMS norm. +- Qwen3-Next applies its tensor permutation during conversion (in `modify_tensors`), so the graph can consume the already-permuted weights directly. + ### Working with ggml_rope_ext PyTorch implementations usually prefer explicitly calculating `freq_cis`/`sin`/`cos` components. However, in llama.cpp, most RoPE operations can be handled via `ggml_rope_ext`, which does not require a sin/cos matrix. This saves memory while allowing the GGML RoPE kernel to be fused with other ops. diff --git a/skills/add-new-model/SKILL.md b/skills/add-new-model/SKILL.md new file mode 100644 index 000000000..68be866c7 --- /dev/null +++ b/skills/add-new-model/SKILL.md @@ -0,0 +1,98 @@ +--- +name: add-new-model +description: Guided workflow for adding a new model architecture to llama.cpp. Use when the user wants to add/port a new model architecture. +--- + +# Add a new model architecture to llama.cpp + +This skill walks a contributor through adding a new model architecture. AI-generated code is permitted in this project, so you may write full implementations for the steps below rather than only pointing at patterns - but follow `AGENTS.md`'s AI usage policy throughout: + +- The contributor is 100% responsible for every line, however it was produced. They must be able to explain and defend any part of it to a reviewer. Check in with them as you go (don't silently generate everything and hand over a finished diff) so they actually absorb what was written. +- Before writing code, make sure the contributor owns the design choices for this architecture (which reference model to follow, how non-standard bits like RoPE variants or MoE routing should be handled) - AI accelerates a design the contributor has already made, it doesn't make the design for them. +- Disclosure is mandatory: any AI-meaningful contribution must be disclosed per the PR template. Remind the contributor of this before they open the PR. +- Never write the PR description, commit message, GitHub issue/discussion post, or reviewer replies - those must come from the contributor. If asked to commit on their behalf, use `Assisted-by:` (never `Co-authored-by:`) and only after explicit confirmation. +- If the requested change looks large or introduces a new pattern not covered here, pause and tell the user this kind of change is likely to need prior discussion with maintainers before a PR. +- Keep the PR self-contained. If the work would require a lot of unconventional changes outside the new model file(s) (e.g. touching shared graph-building code, the sampler, or core APIs in ways other models don't), STOP and tell the contributor to open a discussion/issue first - invasive or excessive changes get closed without full review. +- Do not bundle unrelated work into this PR - see Step 4 and Step 5 below for the specifics on multimodal and chat-template/parsing work. +- Never hack around RoPE with a custom sin/cos implementation. Several past PRs tried this and were closed. If the existing `ggml_rope_ext` (see Step 2's RoPE tips) genuinely cannot express what this model needs, the contributor should open an issue to discuss it with maintainers first - not send a PR with a custom RoPE implementation. + +Before starting, read `CONTRIBUTING.md`, `AGENTS.md` and `docs/development/HOWTO-add-model.md` if they are not already in context. Also run `git log --oneline -- src/models` and look at at least 3 recent PRs that added a model (their merge commits/diffs) - this shows current convention more reliably than the docs, which can lag behind. + +## Step 0 - Scope and dedup check + +Ask the contributor: +1. Which model (HF repo id or name)? Is it text-only or does it have a multimodal (vision/audio) encoder? +2. Do they already have the HF `config.json`/weights available locally? +3. Have they checked for an existing PR/issue on this model? Suggest `gh search issues ""` and `gh search prs ""` in the `ggml-org/llama.cpp` repo. If an existing PR covers it, the contributor should comment there and collaborate rather than open a duplicate (per CONTRIBUTING.md's AI Usage Policy). +4. What existing supported architecture is this model closest to (e.g. "Llama-like with sliding window", "MoE like DBRX", "BERT-style encoder")? + +If the contributor doesn't know the closest reference architecture, you may grep `conversion/*.py` and `src/models/*.cpp` for architectures with a similar config shape (layer count, head count, MoE expert count, norm placement) and suggest 1-2 candidates - but let the contributor confirm the choice rather than picking one yourself; this choice is a design decision they need to own. + +Do not proceed to Step 1 until the contributor has answered these and named a reference architecture. + +## Step 1 - Convert the model to GGUF + +Follow HOWTO-add-model.md section 1 for the actual touch points (conversion class registration, `constants.py`, `tensor_mapping.py`, etc.) - don't re-derive them here, read them from the doc. + +Skill-specific addition: for each touch point, show the contributor the equivalent code in the reference architecture they named in Step 0 before writing the new version, and check that they understand what's different about their model (e.g. non-standard tensor shapes, extra hparams) rather than just copying the pattern silently. + +## Step 2 - Define the architecture in llama.cpp + +Follow HOWTO-add-model.md section 2 for the actual touch points (`llm_arch` enum, `LLM_ARCH_NAMES`, hparam loading, RoPE type case, etc.), including its "Tips and tricks" section for `ggml_rope_ext` gotchas. + +Skill-specific addition: never hack around RoPE with a custom sin/cos implementation - see the RoPE rule above. + +## Step 3 - Build the GGML graph + +Follow HOWTO-add-model.md section 3 for the actual touch points (`src/models/.cpp` struct, `llama_model_mapping` registration, etc.). + +Skill-specific addition: before writing `src/models/.cpp`, read at least 10 other files under `src/models/` (pick a mix, not just the one reference architecture) to confirm the struct layout, naming, and style you're about to write actually matches current convention - the pattern drifts over time and the HOWTO doc can lag behind it. + +## Step 4 - Optional: multimodal encoder + +Only do this if the contributor flagged a vision/audio encoder in Step 0. Follow HOWTO-add-model.md section 4 and `docs/multimodal.md` for the actual touch points (`MmprojModel` subclass, `clip.cpp`, `mtmd.cpp`, encoder graph in `tools/mtmd/models`, etc.). + +Skill-specific addition, and read this carefully: **whether the multimodal encoder can be bundled into the same PR as the base text-model support depends on how conventional the change is.** It's OK to bundle it if the encoder support is conventional - i.e. no new infra or logic is needed, it's just a new cgraph reusing existing preprocessing/projector machinery (e.g. siglip/pixtral/qwen with just a new projector). If it requires anything beyond that - a new preprocessor, non-standard projector logic, or changes to shared `libmtmd` infra/logic - STOP, tell the contributor this is non-conventional, and have them land the text model first with the encoder as a dedicated follow-up PR. Do not let this decision pass silently - call it out explicitly to the contributor before writing any `clip.cpp`/`mtmd.cpp` code. + +## Step 5 - Optional: chat template / parsing support + +Only do this if the model needs a new built-in chat template (`src/llama-chat.cpp`) or a new output parser (see `docs/development/parsing.md` and `docs/autoparser.md`). If either is needed beyond what a user-supplied Jinja template already covers, treat it as its own dedicated follow-up PR, not part of the base model-support PR - call this out explicitly to the contributor rather than silently bundling it in. + +## Common pitfalls (from past PR reviews) + +These recur often enough in review comments on past add-model PRs that they're worth checking proactively, not just waiting for a reviewer to catch them: + +- Don't validate the same hparam/config assumption in both the Python conversion script and the C++ load path - pick one layer to own the check, duplicating it just adds maintenance surface. +- Optional hparams that are genuinely absent from some configs (e.g. a shared-expert count) should be read with an explicit optional/fallback accessor, not assumed present. +- Hparams that are actually load-bearing (the model produces wrong output or crashes without them, e.g. `sliding_window_pattern`, norm-eps) must hard-error if missing, not silently fall back to a default. +- Don't bake a default chat template into the C++ binary - inject it into the GGUF at conversion time instead, since one `llm_arch` can be reused by multiple fine-tunes with different templates, and a baked-in C++ default fails silently for those. +- Before writing a dedicated tool-call/output parser, check whether the existing autoparser already handles the template (`llama-debug-template-parser ` shows what it detects). +- Marking a custom EOS/closing-tag token as `eot` at conversion time isn't always sufficient - in long/agentic generations a model can emit the closing sequence as literal text instead of the token, so generation never stops on EOG and raw text leaks past the parser. Verify this case, not just the token path. +- If reusing or aliasing an existing pre-tokenizer for convenience, justify and test that choice explicitly - silent reuse is an easy source of subtle tokenizer bugs. +- Watch for excessive graph splits caused by building per-layer view/index tensors inside the layer loop - hoist tensors that don't vary per layer out of the loop (relevant if you hit `GGML_SCHED_MAX_SPLIT_INPUTS`). +- A custom KQ mask fed into flash attention must match FA's expected dtype - cast it to F16 before passing it to `build_attn_mha` when FA is enabled. +- When padding a custom KV-cache size to an alignment (e.g. `GGML_PAD(..., 256)`), apply the padding after all other size adjustments, not before - otherwise later logic can un-align it again. +- For non-standard cache/SWA (sliding-window-attention) semantics, override the dedicated hook (e.g. `llama_model_n_swa()`) rather than mutating hparams to fake the behavior - hparams may be read elsewhere for unrelated purposes. +- Don't ship unfinished or unverified speculative-decoding (e.g. MTP) scaffolding in the base model PR - if it hasn't actually been confirmed to work, pull it out and land it as its own follow-up. +- Conversion code should call into the base class's existing hparam logic (e.g. `super().set_gguf_parameters()`) rather than re-deriving it - large blocks of code that duplicate what `TextModel`/`MmprojModel` already provide will get flagged as redundant. +- Do constant tensor modifications (e.g. `norm(1 + weight)`) and permutations/chunking at conversion time, not in the graph - see HOWTO-add-model.md's "Prefer conversion-time tensor modifications" tip (Gemma 3 folds its `1 +` into the weights, Qwen3-Next permutes in `modify_tensors`). Doing these at runtime in the graph is very likely to be rejected as over-complicated; if you genuinely can't do it at conversion time, open a discussion first explaining why rather than implementing it in the graph. + +## Validation checklist + +Reference: `examples/model-conversion/README.md`. + +1. Convert to GGUF, then inspect/run both the original and converted tensors. +2. Run logits verification (original vs converted). If this model is a new version of an already-supported family, verify the *previous* version still passes logits verification first - numerical differences may be pre-existing, not caused by the new work. The tools to perform full logits validation are available in `examples/model-conversion`. +3. Quantize (including QAT variants if relevant) and re-verify. +4. Run perplexity evaluation (simple and full). +5. Sanity-check across `tools/cli`, `tools/completion`, `tools/imatrix`, `tools/quantize`, and `tools/server`. +6. CPU backend first; other backends (CUDA, Metal, ...) can be separate follow-up PRs per `CONTRIBUTING.md`. +7. Re-review every changed file against the coding/naming guidelines in `AGENTS.md` (and `CONTRIBUTING.md`'s "Coding guidelines"/"Naming guidelines" sections) - this is a separate pass from functional testing and is just as important: no forced line-wrapping, no unicode punctuation, minimal/non-redundant comments, `snake_case` naming (`kebab-case` for file names), matching indentation/brace style, etc. + +## Before opening a PR + +- Run the `code-review` skill on the diff first - it catches the convention and scope issues reviewers flag most often, and it's recommended to do this locally before pushing the PR. +- Confirm the contributor can explain every changed line to a reviewer and is prepared to be asked about any of it - this is required regardless of how much of the code was AI-generated. +- Confirm they did a comprehensive manual review of the full diff, not just a skim. +- Fill in the AI-disclosure section of `.github/pull_request_template.md` describing how AI was used (do not omit or understate this). +- Do not write the PR description, commit message, GitHub issue/discussion text, or any reviewer replies yourself - the contributor writes these. diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md new file mode 100644 index 000000000..84075fea3 --- /dev/null +++ b/skills/code-review/SKILL.md @@ -0,0 +1,134 @@ +--- +name: code-review +description: Review llama.cpp changes against project conventions and common reviewer pitfalls before a PR. Use when the user wants to review a diff, branch, or PR. +--- + +# Review llama.cpp changes + +This skill reviews changes against llama.cpp's conventions and the pitfalls that reviewers flag most often, so the contributor can fix them before a maintainer has to. It has two modes: + +- **Self-review (default):** review the contributor's own local changes (uncommitted work, or a branch vs `master`) as a pre-PR pass. Ask which if it's ambiguous; default to `git diff master...HEAD` plus any uncommitted changes. +- **Read-only review of a PR/file:** if the user points at a PR number or specific files (including code they didn't write), review those and report findings. + +In both modes the output is **private review notes for the user to read and act on** - it is never something to post. This is a hard rule from `AGENTS.md`: an agent must NEVER write, or help write, a PR comment, a review comment, or a reply to a reviewer, by any means including `gh`. Do not offer to. If the user asks you to post the notes, refuse and point them at that rule. Present findings in the conversation only. + +Before starting, read `AGENTS.md` and `CONTRIBUTING.md` if not already in context - the "Coding guidelines", "Naming guidelines", and AI usage sections are the baseline this review enforces. For a diff that adds a new model architecture, also read `docs/development/HOWTO-add-model.md` and consider the dedicated `add-new-model` skill. + +## Step 0 - Scope the diff and pick the checklists + +Identify what actually changed and which area checklists below apply. Run `git diff --stat` (or `gh pr view --json files` for PR mode) and bucket the touched paths: + +- `conversion/`, `gguf-py/`, `src/models/`, `src/llama-arch.*` -> **New model / architecture** +- `ggml/` (any backend, op, or `ggml.h`) -> **ggml / backend** +- `include/llama.h` and other public headers -> **Public API** +- `tools/server/` -> **Server** +- anything else, plus all of the above -> **General** (always runs) + +Always run the **Scope and quick-reject gate**, the **Security review**, and the **General** checklist. Run each area checklist whose paths were touched. Additionally, if the diff introduces a new component, subsystem, or piece of infrastructure (a new file/class/module, a new abstraction, or hand-rolled machinery), run the **Approach and design** review. Tell the user which checklists you're running and why. + +## Scope and quick-reject gate (always) + +These are the patterns that get PRs closed without a full review. Check them first - a finding here is more important than any code nit, because it can mean the change shouldn't be a PR in its current form at all. + +- Is there a prior issue/discussion for this? Features are supposed to start as an issue, not a PR (`CONTRIBUTING.md`). If this is a nontrivial feature with no linked issue, flag it and suggest opening one first. +- Is it a duplicate of existing/in-flight work? Suggest `gh search prs` / `gh search issues` for the feature. Many closed PRs were duplicates of something already queued. +- Is it self-contained and single-purpose? Multiple unrelated changes/optimizations bundled together get sent back to be split. Flag unrelated changes and suggest separate PRs. +- Does it touch multiple ggml backends at once? Initial support should be CPU-only, other backends as follow-ups (`CONTRIBUTING.md`). Flag CUDA/Metal/Vulkan/etc. changes bundled into a feature's first PR. +- Does it add a new `ggml_type` / quantization type? That carries a disproportionate maintenance burden and needs the full justification package (GGUF sample upload, perplexity vs FP16/BF16 and similar sizes, KL-divergence data, CPU perf numbers). Absent that, it will be rejected regardless of code quality. +- Is it invasive - new subsystem, core-API reshaping, changes to shared graph/sampler code that other models don't need? Flag it and suggest a discussion with maintainers before investing further. +- Is it niche/vendor-specific in a way that adds a maintenance burden nobody will own long-term? Flag the maintenance-ownership question. +- Is the change semantically correct, or a plausible-looking "fix" that misunderstands the code? Sanity-check the actual behavior, not just that it compiles. +- AI-disclosure: if AI meaningfully contributed, is the PR template's disclosure section filled in? Remind the user. Never suggest writing the PR description or commit message for them. + +## Security review (mandatory) + +Mandatory on every review; any finding here is **blocking**. Rule of thumb: GGUF metadata, tensor shapes, tokenizer/grammar input, and all server/RPC fields are attacker-controlled - bound them before use. + +- **Sizes/counts from tensor dims:** validate before allocating. Products like `ne[i]*nb[i]`/nbytes can overflow on crafted dims into an undersized alloc then heap overflow. Overflow checks must run BEFORE the arithmetic they guard - padding/alignment macros wrap to 0 near `SIZE_MAX`, so a guard after the pad passes. +- **GGUF strings/arrays:** cap declared lengths and element counts before using them to size a loop or buffer; validate element type and length before casting an array to a pointer or reading fixed indices (`[i+1]`, `[0..2]`). +- **File-supplied counts indexing fixed arrays:** bound any count (e.g. layer/block count into a `LLAMA_MAX_*` array) before indexing; watch checks that only fire when an optional key is present. +- **Bounds comparisons:** flag narrowing casts (`size_t`->`int32_t`) and signed/unsigned mixing that can bypass a length check and copy past a buffer. +- **Parsed/derived indices:** range-check `stoi`/`atoi` results and catch parse throws; never use a default or derived token id (EOS/BOS/...) as an index without a bounds check. +- **Reused/reserved buffers:** recheck bounds after a buffer is shrunk or reused; watch `reserve()` then index-by-assumed-size, and header fields read before their length is checked. +- **Server JSON ints:** clamp client-supplied integers (token/discard counts, offsets) to non-negative and an upper bound before they reach index/pointer arithmetic. +- **RPC-deserialized fields:** treat every field (type/buffer/data/ne/nb/op_params) as hostile - validate before use. Null/zero buffers skipping validation, attacker data pointers, out-of-range type indices, and negative strides sign-extending past a corner-only assert all give arbitrary read/write. +- **Lifetime/UAF:** flag stored raw pointers to caller/temporary storage, cached pointers to buffers a later free releases, async ops whose source may drop before completion, and structures not invalidated on free/realloc. Null-check conditionally-built or "not required" tensors before dereferencing. + +## Approach and design (when a new component/infra is introduced) + +Run this whenever the diff adds a new component, subsystem, or piece of infrastructure. Reviews too often stop at "does it work" - a diff can be correct and still be the wrong approach, and a messy design costs more long-term than a bug. Evaluate the *approach*, not just the behavior; raising a cleaner one is a high-value finding, not a nit. If you see a better design, describe it concretely rather than just calling the current one bad. + +- **Simpler approach upstream:** the biggest win is often a different data model or design that removes whole subsystems, not tweaks to the code as written. Complexity must be justified by the problem, not by the first thing that worked. +- **Reuse over reinvention:** grep for an existing helper, library, object, or mechanism before adding a new one. Reimplementing what the codebase already has reintroduces solved bugs and adds maintenance surface. +- **Clear ownership/lifetime:** prefer RAII and obvious ownership over manual liveness flags, hand-tracked pointers, and "is it still alive?" checks - manual lifetime tracking is a recurring source of subtle bugs. +- **Right-sized machinery:** flag redundant, overkill, or heavier-than-needed primitives and abstractions; use the minimum the design actually needs. +- **Right structure and fit:** a new type should earn its place (split it if it serves two roles); follow existing patterns, idioms, and naming, and avoid constructs the project shuns. +- **Root cause vs symptom:** fixes layered on fixes signal a design to correct, not guard around. + +## New model / architecture + +See the `add-new-model` skill and `docs/development/HOWTO-add-model.md` for the full workflow; this is the review-time subset that reviewers most often catch: + +- Don't branch on `model.arch` when the real dependency is a config/capability value - gate on the hparam/capability, not the architecture enum. +- If the model is a close variant of an existing arch, is the delta justified? Prefer reusing or subclassing the existing arch/model class over duplicating it. A near-duplicate class or `src/models/.cpp` will be asked to merge with its sibling. +- New tensor names go through `tensor_mapping.py`, not ad-hoc name matching. +- For QKV, split the *activation* with `ggml_view`, not the *weight* tensor; rely on ggml broadcasting instead of manually duplicating tensors. +- New graph inputs are declared at the top of the graph-build function, not inline where first used. +- Hparams that the model can't run correctly without must be mandatory (hard-error if missing), not read with a silent default fallback. Only genuinely-optional-across-configs values get a fallback accessor. +- New/optional weight tensors (scales, etc.) must route through `build_lora_mm` and the existing helpers, matching convention - don't leave raw matmuls copied from another arch. +- Don't hack RoPE with a custom sin/cos implementation. If `ggml_rope_ext` genuinely can't express it, that's an issue for discussion, not a PR. +- Test the quantized-KV path (`-ctk`/`-ctv q8_0`), not just default f16 - new speculative/attention features silently break there. +- Preserve existing explanatory comments about model-specific quirks when copying code; note the provenance ("copied from X, with Y added"). +- Remove dead code/branches left over from adapting a reference implementation. + +## ggml / backend + +- `supports_op` (and any dispatch/gating condition) must be scoped exactly to the cases being changed - a condition meant for a few quant types must not silently disable or enable everything else. +- No hardcoded warp/lane size - use `ggml_cuda_get_physical_warp_size()` (32 on CUDA, 64 on HIP/ROCm) and the portable helpers. +- Strip leftover debug/profiling/logging code before review. +- New or changed op? Update `docs/ops.md` and the relevant `docs/ops/*.csv` for the touched backend. +- New op or operator change needs corresponding `test-backend-ops` cases, and (per `CONTRIBUTING.md`) consistency across at least two backends. +- New kernels are expected to come with concrete perf data (throughput across realistic tensor shapes), not just correctness. +- Don't have a backend mutate the cgraph as a shortcut - that's an unresolved architectural question, not something to slip in. +- Expect this to need two maintainer approvals; that's normal for `ggml/` changes, not a sign something is wrong. +- For CUDA: Avoid excessively templating kernels, only add this where it shows visible performance gain. + +## Public API (`include/llama.h`) + +Public API changes carry a higher bar than internal ones (`CONTRIBUTING.md`). Review for: + +- Justification: why doesn't an existing mechanism (e.g. `cb_eval`, existing batch/sampler knobs) suffice? If it does, the change likely shouldn't add public surface. This is the single most common reason these PRs are rejected. +- Experimental or stop-gap surface belongs in a side header (`llama-ext.h`), not in `llama.h`. +- Keep it minimal and general: prefer one general call over several narrow convenience wrappers; make new calls forward-compatible (e.g. mixed-modality batches) rather than assuming today's shape. +- The C API is the first-class, stable, ABI-defining surface - don't propose a parallel C++ API as a replacement. `llama-cpp.h` stays a thin convenience layer. +- Types and naming: sized integer types (`int32_t`, `size_t` for sizes/offsets); `snake_case`; `_` = `__`; enum values upper-case and prefixed with the enum name; `_t` suffix for opaque types. Avoid gratuitous signature/ABI changes to existing exported functions. +- Every new API needs a working example/tool exercising it in the same PR - reviewers find real bugs by requiring it to be wired into `server`, `embedding`, `perplexity`, etc. + +## Server (`tools/server/`) + +- Is the feature within server's defined scope? Check `tools/server/README-dev.md` - out-of-scope features get declined. +- Security: don't trust client-supplied headers (e.g. `X-Forwarded-For`) or add footguns; things like IP allowlisting belong at a reverse proxy unless there's a trusted-proxy design. +- Wire new behavior into the existing request/response and checkpoint paths correctly; watch for resource leaks across requests. + +## General (always) + +Enforce the `AGENTS.md` / `CONTRIBUTING.md` coding and naming guidelines on every changed line - this is a distinct pass from checking that the code works, and matters just as much for review speed: + +- ASCII only in code and comments - no emdash, unicode arrows, `x`, `...` used as unicode; use `-`, `->`, `x`, `...` ASCII equivalents. +- Comments are concise and explain non-obvious *why*, not *what*. Flag verbose comments, comments that restate the code, comments that reference the current task/PR, and comments hard-wrapped to a fixed column width. +- Do not force-wrap prose/comments to a fixed character count or split a sentence across lines. +- `snake_case` names; `kebab-case` (lowercase-with-dashes) file names for C/C++, `.h` headers; Python files lowercase-with-underscores. Naming optimizes for longest common prefix (`number_small`, not `small_number`). +- 4-space indentation, brackets on the same line, `void * ptr`, `int & a`, no trailing whitespace; match the surrounding style. +- Reuse existing infrastructure over introducing new components; no new third-party dependencies, extra headers, or files unless clearly justified. +- Keep it simple: a simpler change doing 90% is often preferable to a complex one doing 100%. Flag unnecessary templates/fancy STL; basic `for` loops are fine here. +- Every added line should be something the contributor can explain and defend to a reviewer without AI help - flag anything that looks copied-in without understanding. + +## Reporting + +Group findings by severity so the user knows what actually blocks a merge: + +1. **Blocking** - quick-reject/scope issues and correctness bugs; these can sink the PR regardless of everything else. +2. **Will slow the review** - convention/naming/comment violations, missing tests/docs/perf data, missing API justification or example. +3. **Nits** - minor style, optional cleanups. + +For each finding, point to the file and line and say concretely what to change and why. Do not rewrite the whole diff unprompted; let the contributor make the fixes so they own and understand them. And do not draft any PR text, commit message, or reviewer reply - that is the contributor's to write. From 56a83860ddb5d0199dc0811a9054b621243c5116 Mon Sep 17 00:00:00 2001 From: kumaal <44551860+kumaal@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:12:01 +0500 Subject: [PATCH 20/21] opencl: do not treat NULL-mask flash attention as causal (#25771) --- ggml/src/ggml-opencl/ggml-opencl.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index e30eb43a3..daffd825b 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -14758,7 +14758,11 @@ static void ggml_cl_flash_attn(ggml_backend_t backend, const ggml_tensor * q, co // Flash-Decoding K-split decision. Resolved here, before the prefill // prepass, because KV-pad and blk prepass are pure overhead when FD fires. - const int is_causal = (mask == NULL && n_q > 1 && n_q == n_kv); + // Do not infer causality from tensor shapes: a NULL mask means full + // (bidirectional) attention, e.g. ViT encoders, where n_q == n_kv as well. + // Causal attention in llama.cpp always comes with an explicit KQ mask. + // Inferring is_causal here corrupted mmproj output on OpenCL (see #23800). + const int is_causal = 0; const int fd_max_n_q = (d_head_q <= FD_MAX_DK_MULTI) ? FD_MAX_N_Q_MULTI : 1; cl_kernel fd_k_split = NULL; bool use_fd_mq = false; From ed7adbfefd5e3af5f1c7c04ac67145783def85c5 Mon Sep 17 00:00:00 2001 From: Hongqiang Wang Date: Fri, 24 Jul 2026 08:14:33 -0700 Subject: [PATCH 21/21] opencl: cache compiled cl_program binaries on disk (#26050) --- docs/backend/OPENCL.md | 18 + ggml/src/ggml-opencl/CMakeLists.txt | 2 + ggml/src/ggml-opencl/cl-program-cache.cpp | 453 ++++++++++++++++++++++ ggml/src/ggml-opencl/cl-program-cache.h | 75 ++++ ggml/src/ggml-opencl/ggml-opencl.cpp | 384 +++++++++--------- 5 files changed, 747 insertions(+), 185 deletions(-) create mode 100644 ggml/src/ggml-opencl/cl-program-cache.cpp create mode 100644 ggml/src/ggml-opencl/cl-program-cache.h diff --git a/docs/backend/OPENCL.md b/docs/backend/OPENCL.md index 76f56bb10..337b0c82a 100644 --- a/docs/backend/OPENCL.md +++ b/docs/backend/OPENCL.md @@ -98,6 +98,24 @@ The OpenCL backend has the following CMake options that control the behavior of | `GGML_OPENCL_USE_ADRENO_KERNELS` | `ON` | Use kernels optimized for Adreno. | | `GGML_OPENCL_USE_ADRENO_BIN_KERNELS` | `OFF` | Allow using binary kernel lib for Adreno. | +## Program Binary Cache + +Compiled `cl_program` binaries are cached on disk, so subsequent runs skip the expensive +compile-from-source step when nothing relevant has changed (kernel source, compile options, +device, driver, or platform version). + +The cache is controlled with the `GGML_OPENCL_KERNEL_CACHE_DIR` environment variable: + +| Value | Behavior | +|:---------------------------------------|:-----------------------------------------------| +| unset / empty / `1` / `default` | Enabled in the platform default cache directory: `%LOCALAPPDATA%\llama.cpp\cl-cache` (Windows), `~/Library/Caches/llama.cpp/cl-cache` (macOS), `/llama.cpp/cl-cache` elsewhere. | +| `0` / `off` / `none` / `disable(d)` | Disabled. | +| any other value | Used verbatim as the cache directory path. | + +If the chosen directory cannot be created or used, the cache disables itself for the process +and kernels are compiled from source as usual. Set `GGML_OPENCL_KERNEL_CACHE_DEBUG=1` to +print a HIT/MISS/SAVE trace to stderr. + ## Android Ubuntu 22.04 is used for targeting Android. Make sure the following tools are accessible from command line, diff --git a/ggml/src/ggml-opencl/CMakeLists.txt b/ggml/src/ggml-opencl/CMakeLists.txt index 1a534f884..1dc707177 100644 --- a/ggml/src/ggml-opencl/CMakeLists.txt +++ b/ggml/src/ggml-opencl/CMakeLists.txt @@ -5,6 +5,8 @@ set(TARGET_NAME ggml-opencl) ggml_add_backend_library(${TARGET_NAME} ggml-opencl.cpp + cl-program-cache.cpp + cl-program-cache.h ../../include/ggml-opencl.h) target_link_libraries(${TARGET_NAME} PRIVATE ${OpenCL_LIBRARIES}) target_include_directories(${TARGET_NAME} PRIVATE ${OpenCL_INCLUDE_DIRS}) diff --git a/ggml/src/ggml-opencl/cl-program-cache.cpp b/ggml/src/ggml-opencl/cl-program-cache.cpp new file mode 100644 index 000000000..1a4a28117 --- /dev/null +++ b/ggml/src/ggml-opencl/cl-program-cache.cpp @@ -0,0 +1,453 @@ +// Match the version setup ggml-opencl.cpp uses, so any cl.h declarations we +// touch are consistent across this backend's translation units. +#define CL_TARGET_OPENCL_VERSION GGML_OPENCL_TARGET_VERSION +#define CL_USE_DEPRECATED_OPENCL_1_2_APIS + +#include "cl-program-cache.h" + +#include "ggml-impl.h" // GGML_LOG_INFO / WARN + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# include +# define ggml_getpid() ((int) GetCurrentProcessId()) +#else +# include +# define ggml_getpid() ((int) getpid()) +#endif + +namespace fs = std::filesystem; + +// ---------------------------------------------------------------------------- +// SHA-256 (FIPS 180-4). Self-contained, ~80 lines, public-domain reference. +// Hot path is a few KB of source per kernel ⇒ <1 ms total per process init. +// ---------------------------------------------------------------------------- + +namespace { + +struct sha256_ctx { + uint32_t state[8]; + uint64_t bitlen; + uint8_t buf[64]; + size_t buf_len; +}; + +const uint32_t K256[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2, +}; + +inline uint32_t rotr32(uint32_t x, unsigned n) { return (x >> n) | (x << (32 - n)); } + +void sha256_compress(uint32_t state[8], const uint8_t block[64]) { + uint32_t w[64]; + for (int i = 0; i < 16; ++i) { + w[i] = ((uint32_t)block[i*4 ] << 24) | + ((uint32_t)block[i*4 + 1] << 16) | + ((uint32_t)block[i*4 + 2] << 8) | + ((uint32_t)block[i*4 + 3] ); + } + for (int i = 16; i < 64; ++i) { + uint32_t s0 = rotr32(w[i-15], 7) ^ rotr32(w[i-15], 18) ^ (w[i-15] >> 3); + uint32_t s1 = rotr32(w[i-2], 17) ^ rotr32(w[i-2], 19) ^ (w[i-2] >> 10); + w[i] = w[i-16] + s0 + w[i-7] + s1; + } + + uint32_t a = state[0],b = state[1],c = state[2],d = state[3],e = state[4],f = state[5],g = state[6],h = state[7]; + + for (int i = 0; i < 64; ++i) { + uint32_t S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25); + uint32_t ch = (e & f) ^ ((~e) & g); + uint32_t t1 = h + S1 + ch + K256[i] + w[i]; + uint32_t S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + state[0]+=a; state[1]+=b; state[2]+=c; state[3]+=d; + state[4]+=e; state[5]+=f; state[6]+=g; state[7]+=h; +} + +void sha256_init(sha256_ctx & c) { + c.state[0]=0x6a09e667; c.state[1]=0xbb67ae85; c.state[2]=0x3c6ef372; c.state[3]=0xa54ff53a; + c.state[4]=0x510e527f; c.state[5]=0x9b05688c; c.state[6]=0x1f83d9ab; c.state[7]=0x5be0cd19; + c.bitlen = 0; + c.buf_len = 0; +} + +void sha256_update(sha256_ctx & c, const void * data, size_t len) { + const uint8_t * p = (const uint8_t *) data; + c.bitlen += (uint64_t) len * 8; + if (c.buf_len > 0) { + size_t n = 64 - c.buf_len; + if (n > len) { n = len; } + memcpy(c.buf + c.buf_len, p, n); + c.buf_len += n; + p += n; + len -= n; + if (c.buf_len == 64) { + sha256_compress(c.state, c.buf); + c.buf_len = 0; + } + } + while (len >= 64) { + sha256_compress(c.state, p); + p += 64; + len -= 64; + } + if (len > 0) { + memcpy(c.buf, p, len); + c.buf_len = len; + } +} + +void sha256_final(sha256_ctx & c, uint8_t out[32]) { + uint64_t bitlen = c.bitlen; + c.buf[c.buf_len++] = 0x80; + if (c.buf_len > 56) { + while (c.buf_len < 64) { c.buf[c.buf_len++] = 0; } + sha256_compress(c.state, c.buf); + c.buf_len = 0; + } + while (c.buf_len < 56) { c.buf[c.buf_len++] = 0; } + for (int i = 7; i >= 0; --i) { c.buf[c.buf_len++] = (uint8_t) (bitlen >> (i * 8)); } + sha256_compress(c.state, c.buf); + for (int i = 0; i < 8; ++i) { + out[i*4 ] = (uint8_t) (c.state[i] >> 24); + out[i*4 + 1] = (uint8_t) (c.state[i] >> 16); + out[i*4 + 2] = (uint8_t) (c.state[i] >> 8); + out[i*4 + 3] = (uint8_t) (c.state[i] ); + } +} + +std::string sha256_hex(const uint8_t digest[32]) { + static const char hex[] = "0123456789abcdef"; + std::string s(64, '0'); + for (int i = 0; i < 32; ++i) { + s[i*2 ] = hex[digest[i] >> 4]; + s[i*2 + 1] = hex[digest[i] & 0xf]; + } + return s; +} + +std::string compute_key(const std::string & key_suffix, + const char * source, + const std::string & compile_opts) { + sha256_ctx c; + sha256_init(c); + + static const uint8_t sep = 0; + sha256_update(c, source, strlen(source)); + sha256_update(c, &sep, 1); + sha256_update(c, compile_opts.data(), compile_opts.size()); + sha256_update(c, &sep, 1); + sha256_update(c, key_suffix.data(), key_suffix.size()); + + uint8_t digest[32]; + sha256_final(c, digest); + return sha256_hex(digest); +} + +bool make_dir_recursive(const std::string & path) { + if (path.empty()) { return false; } + // create_directories() already creates missing parents. It returns false + // (with ec clear) when the directory is already there, so re-check. + const fs::path p = fs::u8path(path); + std::error_code ec; + if (fs::create_directories(p, ec)) { return true; } + std::error_code ec_stat; + return fs::is_directory(p, ec_stat); +} + +std::string default_cache_dir() { +#if defined(_WIN32) + const char * base = std::getenv("LOCALAPPDATA"); + if (!base || !*base) { base = std::getenv("APPDATA"); } + if (!base || !*base) { base = std::getenv("TEMP"); } + if (!base || !*base) { base = "."; } + return std::string(base) + "\\llama.cpp\\cl-cache"; +#elif defined(__APPLE__) + const char * home = std::getenv("HOME"); + if (!home || !*home) { home = "."; } + return std::string(home) + "/Library/Caches/llama.cpp/cl-cache"; +#else + // The throwing overload aborts the process when no usable temp directory + // exists (e.g. Android app contexts with TMPDIR unset); an empty return + // here just disables the cache instead. + std::error_code ec; + const fs::path tmp_path = fs::temp_directory_path(ec); + if (ec || tmp_path.empty()) { return {}; } + return tmp_path.string() + "/llama.cpp/cl-cache"; +#endif +} + +// Query a NUL-terminated string from clGetDeviceInfo / clGetPlatformInfo. +template +std::string query_string(GetInfoFn fn, Object obj, cl_uint name) { + size_t sz = 0; + if (fn(obj, name, 0, nullptr, &sz) != CL_SUCCESS || sz == 0) { + return {}; + } + std::string s(sz, '\0'); + if (fn(obj, name, sz, &s[0], nullptr) != CL_SUCCESS) { + return {}; + } + if (!s.empty() && s.back() == '\0') { + s.pop_back(); + } + return s; +} + +std::string compute_key_suffix(cl_device_id device) { + cl_platform_id platform = nullptr; + clGetDeviceInfo(device, CL_DEVICE_PLATFORM, sizeof(platform), &platform, nullptr); + + std::string s; + s.reserve(512); + s += query_string(clGetDeviceInfo, device, CL_DEVICE_NAME); s.push_back('\0'); + s += query_string(clGetDeviceInfo, device, CL_DRIVER_VERSION); s.push_back('\0'); + s += query_string(clGetDeviceInfo, device, CL_DEVICE_VERSION); s.push_back('\0'); + if (platform) { + s += query_string(clGetPlatformInfo, platform, CL_PLATFORM_VERSION); s.push_back('\0'); + } + s += "fmt=" + std::to_string(CL_PROGRAM_CACHE_FORMAT_VERSION); + return s; +} + +const uint8_t MAGIC[8] = { 'G','G','M','L','C','L','B','C' }; + +bool read_all(const std::string & path, std::vector & out) { + std::ifstream f(fs::u8path(path), std::ios::binary); + if (!f) { return false; } + f.seekg(0, std::ios::end); + std::streamsize sz = f.tellg(); + if (sz < 0) { return false; } + f.seekg(0, std::ios::beg); + out.resize((size_t) sz); + if (sz > 0) { f.read((char *) out.data(), sz); } + return f.good() || f.eof(); +} + +bool write_atomic(const std::string & path, const uint8_t * data, size_t len) { + const fs::path dst = fs::u8path(path); + const fs::path tmp = fs::u8path(path + ".tmp." + std::to_string(ggml_getpid())); + { + std::ofstream f(tmp, std::ios::binary | std::ios::trunc); + if (!f) { return false; } + f.write((const char *) data, (std::streamsize) len); + if (!f.good()) { + std::error_code ec_rm; + fs::remove(tmp, ec_rm); + return false; + } + } + + std::error_code ec; + fs::rename(tmp, dst, ec); + if (ec) { + std::error_code ec_rm; + fs::remove(tmp, ec_rm); + return false; + } + return true; +} + +} // namespace + +static bool cache_debug_enabled() { + static int cached = -1; + if (cached < 0) { + const char * e = std::getenv("GGML_OPENCL_KERNEL_CACHE_DEBUG"); + cached = (e && *e) ? 1 : 0; + } + return cached != 0; +} + +static std::string opts_preview(const std::string & opts, size_t n = 120) { + if (opts.size() <= n) { return opts; } + return opts.substr(0, n) + "..."; +} + +// Running cache tally (diagnostic; plain ints — a benign race in the rare +// multi-threaded lazy-compile case at worst miscounts by one). +static int g_cache_hits = 0, g_cache_misses = 0, g_cache_saves = 0; + +// Debug trace directly to stderr +static void cache_debug_line(const char * kind, const std::string & key, + const char * source, const std::string & opts) { + if (!cache_debug_enabled()) { return; } + fprintf(stderr, "ggml_opencl: cache %-4s [h=%d m=%d s=%d] key=%s src=%zuB opts='%s'\n", + kind, g_cache_hits, g_cache_misses, g_cache_saves, + key.substr(0, 16).c_str(), strlen(source), opts_preview(opts).c_str()); + fflush(stderr); +} + +cl_program_cache_state cl_program_cache_init(cl_device_id device) { + cl_program_cache_state st; + + const char * env = std::getenv("GGML_OPENCL_KERNEL_CACHE_DIR"); + if (env && (!std::strcmp(env, "0") || !std::strcmp(env, "off") || + !std::strcmp(env, "none") || !std::strcmp(env, "disable") || + !std::strcmp(env, "disabled"))) { + if (cache_debug_enabled()) { + fprintf(stderr, "ggml_opencl: kernel cache disabled by GGML_OPENCL_KERNEL_CACHE_DIR=%s\n", env); + fflush(stderr); + } + return st; + } + + std::string dir; + if (!env || !*env || !std::strcmp(env, "1") || !std::strcmp(env, "default")) { + dir = default_cache_dir(); + if (dir.empty()) { + GGML_LOG_INFO("ggml_opencl: kernel cache disabled (no usable default cache directory)\n"); + return st; + } + } else { + dir = env; + } + + if (!make_dir_recursive(dir)) { + GGML_LOG_INFO("ggml_opencl: kernel cache disabled (cannot create directory '%s')\n", dir.c_str()); + return st; + } + + st.dir = dir; + st.key_suffix = compute_key_suffix(device); + GGML_LOG_INFO("ggml_opencl: kernel cache enabled at '%s'\n", st.dir.c_str()); + if (cache_debug_enabled()) { + fprintf(stderr, "ggml_opencl: kernel cache enabled at '%s' " + "(GGML_OPENCL_KERNEL_CACHE_DIR=off to disable)\n", st.dir.c_str()); + fflush(stderr); + } + return st; +} + +cl_program cl_program_cache_try_load( + const cl_program_cache_state & state, + cl_context context, + cl_device_id device, + const char * source, + const std::string & compile_opts) { + + if (state.dir.empty() || !source) { return nullptr; } + + const std::string key = compute_key(state.key_suffix, source, compile_opts); + const std::string path = state.dir + "/" + key + ".clbin"; + + std::vector file; + if (!read_all(path, file)) { + ++g_cache_misses; + cache_debug_line("MISS", key, source, compile_opts); + return nullptr; + } + if (file.size() < 16 || std::memcmp(file.data(), MAGIC, 8) != 0) { return nullptr; } + + uint32_t fmt = + ((uint32_t) file[ 8]) | ((uint32_t) file[ 9] << 8) | + ((uint32_t) file[10] << 16) | ((uint32_t) file[11] << 24); + if (fmt != CL_PROGRAM_CACHE_FORMAT_VERSION) { return nullptr; } + + const size_t hdr_len = 16; + const unsigned char * bin = file.data() + hdr_len; + const size_t bin_len = file.size() - hdr_len; + + cl_int err = CL_SUCCESS; + cl_int bin_err = CL_SUCCESS; + cl_program p = clCreateProgramWithBinary(context, 1, &device, &bin_len, &bin, &bin_err, &err); + if (err != CL_SUCCESS || bin_err != CL_SUCCESS || p == nullptr) { + if (p) { clReleaseProgram(p); } + return nullptr; + } + + err = clBuildProgram(p, 0, nullptr, compile_opts.c_str(), nullptr, nullptr); + if (err != CL_SUCCESS) { + clReleaseProgram(p); + return nullptr; + } + ++g_cache_hits; + cache_debug_line("HIT", key, source, compile_opts); + return p; +} + +void cl_program_cache_try_save( + const cl_program_cache_state & state, + cl_program program, + cl_device_id /*device*/, + const char * source, + const std::string & compile_opts) { + + if (state.dir.empty() || !program || !source) { + return; + } + + cl_uint n_dev = 0; + if (clGetProgramInfo(program, CL_PROGRAM_NUM_DEVICES, sizeof(n_dev), &n_dev, nullptr) != CL_SUCCESS || n_dev == 0) { + return; + } + + std::vector sizes(n_dev); + if (clGetProgramInfo(program, CL_PROGRAM_BINARY_SIZES, sizeof(size_t) * n_dev, sizes.data(), nullptr) != CL_SUCCESS) { + return; + } + if (sizes.empty() || sizes[0] == 0) { + return; + } + + std::vector> binaries(n_dev); + std::vector bin_ptrs(n_dev); + for (cl_uint i = 0; i < n_dev; ++i) { + binaries[i].resize(sizes[i]); + bin_ptrs[i] = binaries[i].data(); + } + if (clGetProgramInfo(program, CL_PROGRAM_BINARIES, sizeof(unsigned char *) * n_dev, bin_ptrs.data(), nullptr) != CL_SUCCESS) { + return; + } + + // We only care about the first device's binary — that's the one we'd + // re-load with on a future cache hit. Multi-device contexts aren't a + // pattern this backend uses today. + const std::vector & bin = binaries[0]; + + std::vector file; + file.reserve(16 + bin.size()); + file.insert(file.end(), MAGIC, MAGIC + 8); + uint32_t fmt = CL_PROGRAM_CACHE_FORMAT_VERSION; + file.push_back((uint8_t) (fmt & 0xff)); + file.push_back((uint8_t) ((fmt >> 8) & 0xff)); + file.push_back((uint8_t) ((fmt >> 16) & 0xff)); + file.push_back((uint8_t) ((fmt >> 24) & 0xff)); + file.push_back(0); file.push_back(0); file.push_back(0); file.push_back(0); // reserved + file.insert(file.end(), bin.begin(), bin.end()); + + const std::string key = compute_key(state.key_suffix, source, compile_opts); + const std::string path = state.dir + "/" + key + ".clbin"; + if (!write_atomic(path, file.data(), file.size())) { + GGML_LOG_INFO("ggml_opencl: kernel cache: failed to write '%s'\n", path.c_str()); + } else { + ++g_cache_saves; + cache_debug_line("SAVE", key, source, compile_opts); + } +} diff --git a/ggml/src/ggml-opencl/cl-program-cache.h b/ggml/src/ggml-opencl/cl-program-cache.h new file mode 100644 index 000000000..49aa2d1e8 --- /dev/null +++ b/ggml/src/ggml-opencl/cl-program-cache.h @@ -0,0 +1,75 @@ +// On-disk cache for OpenCL cl_program binaries. Lets a fresh process skip the +// expensive clBuildProgram-from-source step when a binary for the exact same +// (source, compile options, device, driver, platform) was previously saved. +// +// Activation: default on via GGML_OPENCL_KERNEL_CACHE_DIR: +// unset / empty / "1" / "default" : platform default cache dir +// (%LOCALAPPDATA%\llama.cpp\cl-cache, +// ~/Library/Caches/llama.cpp/cl-cache, +// /llama.cpp/cl-cache elsewhere) +// "0" / "off" / "none" / "disable(d)" : disabled (all functions no-op) +// any other value : used verbatim as the cache path +// If the chosen directory cannot be created/used, the cache silently disables +// itself for the process and falls back to source compile. +// GGML_OPENCL_KERNEL_CACHE_DEBUG=1 prints a HIT/MISS/SAVE trace (with a running +// tally) straight to stderr — visible even in tools that filter INFO/WARN logs; +// redirect stderr to record it. +// +// Cache key (SHA-256 hex): +// sha256(source_bytes || '\x00' || +// compile_opts || '\x00' || +// CL_DEVICE_NAME || '\x00' || +// CL_DRIVER_VERSION || '\x00' || +// CL_PLATFORM_VERSION || '\x00' || +// CL_PROGRAM_CACHE_FORMAT_VERSION) +// +// The key fully captures everything that can affect the produced binary, +// without needing the host source revision (a kernel source change shows up +// in source_bytes; a compile-option change shows up in compile_opts). +// +// File layout per cache entry: /.clbin +// bytes [0..7] : magic "GGMLCLBC" +// bytes [8..11] : uint32_t format version (CL_PROGRAM_CACHE_FORMAT_VERSION) +// bytes [12..15] : uint32_t reserved (0) +// bytes [16..] : raw cl_program binary as returned by +// clGetProgramInfo(CL_PROGRAM_BINARIES) +// +// Concurrency: writes go to .tmp. then atomic rename. On race, +// last-writer-wins. No locks. + +#pragma once + +#include +#include + +// Bumped manually if host-side OpenCL API usage changes in a way that +// affects compile semantics but does not show up in source_bytes / +// compile_opts (e.g. switching from clCreateProgramWithSource to +// clCompileProgram + clLinkProgram, or changing how multiple sources +// are concatenated). Most commits — including kernel changes — do NOT +// require bumping this; the source bytes already capture those. +#define CL_PROGRAM_CACHE_FORMAT_VERSION 1u + +struct cl_program_cache_state { + // Empty string means cache is disabled. + std::string dir; + // Concatenated device/driver/platform identity + cache format version, + // computed once at init and folded into every key. + std::string key_suffix; +}; + +cl_program_cache_state cl_program_cache_init(cl_device_id device); + +cl_program cl_program_cache_try_load( + const cl_program_cache_state & state, + cl_context context, + cl_device_id device, + const char * source, + const std::string & compile_opts); + +void cl_program_cache_try_save( + const cl_program_cache_state & state, + cl_program program, + cl_device_id device, + const char * source, + const std::string & compile_opts); diff --git a/ggml/src/ggml-opencl/ggml-opencl.cpp b/ggml/src/ggml-opencl/ggml-opencl.cpp index daffd825b..b6079d080 100644 --- a/ggml/src/ggml-opencl/ggml-opencl.cpp +++ b/ggml/src/ggml-opencl/ggml-opencl.cpp @@ -13,6 +13,8 @@ #include "ggml-backend-impl.h" #include "ggml.h" +#include "cl-program-cache.h" + #ifdef GGML_OPENCL_USE_ADRENO_BIN_KERNELS #include "libdl.h" #ifdef _WIN32 @@ -594,6 +596,10 @@ struct ggml_backend_opencl_context { cl_context context; cl_command_queue queue; + // On-disk compiled-program cache (see GGML_OPENCL_KERNEL_CACHE_DIR). + cl_program_cache_state program_cache; + bool program_cache_initialized = false; + // prealloc buffers for transposing weights and activations ggml_cl_buffer prealloc_quant_trans; ggml_cl_buffer prealloc_scales_trans; @@ -1192,8 +1198,25 @@ static cl_program build_program_from_source_ex(cl_context ctx, cl_device_id dev, return NULL; } -static cl_program build_program_from_source(cl_context ctx, cl_device_id dev, const char* program_buffer, const std::string &compile_opts) { - return build_program_from_source_ex(ctx, dev, program_buffer, compile_opts, /*fatal=*/true); +static cl_program build_program_from_source(ggml_backend_opencl_context * backend_ctx, const char* program_buffer, const std::string &compile_opts) { + cl_context ctx = backend_ctx->context; + cl_device_id dev = backend_ctx->device; + + // Try the on-disk binary cache first. Falls through silently on miss or + // any failure; never blocks the build path. Disabled cache => nullptr. + cl_program p_cached = cl_program_cache_try_load( + backend_ctx->program_cache, ctx, dev, program_buffer, compile_opts); + if (p_cached != nullptr) { + return p_cached; + } + + cl_program p = build_program_from_source_ex(ctx, dev, program_buffer, compile_opts, /*fatal=*/true); + + // Best-effort save of the freshly-built binary (no-op if cache disabled). + if (p != nullptr) { + cl_program_cache_try_save(backend_ctx->program_cache, p, dev, program_buffer, compile_opts); + } + return p; } static cl_program build_program_from_binary(cl_context ctx, cl_device_id dev, const char* program_buffer, const std::string &compile_opts, size_t bin_size = 0) { @@ -1241,7 +1264,7 @@ static void load_cl_kernels_argsort(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("argsort.cl"); #endif backend_ctx->program_argsort_f32_i32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_argsort_f32_i32 = clCreateKernel(backend_ctx->program_argsort_f32_i32, "kernel_argsort_f32_i32", &err), err)); backend_ctx->kernels_loaded_argsort = true; @@ -1291,7 +1314,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("add.cl"); #endif backend_ctx->program_add = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_add = clCreateKernel(backend_ctx->program_add, "kernel_add", &err), err)); CL_CHECK((backend_ctx->kernel_add_row = clCreateKernel(backend_ctx->program_add, "kernel_add_row", &err), err)); @@ -1310,7 +1333,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("add_id.cl"); #endif backend_ctx->program_add_id = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_add_id = clCreateKernel(backend_ctx->program_add_id, "kernel_add_id", &err), err)); GGML_LOG_CONT("."); @@ -1326,7 +1349,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("tri.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_tri = clCreateKernel(prog, "kernel_tri_f32", &err), err)); GGML_LOG_CONT("."); @@ -1344,7 +1367,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("fill.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_fill = clCreateKernel(prog, "kernel_fill_f32", &err), err)); GGML_LOG_CONT("."); @@ -1362,7 +1385,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("clamp.cl"); #endif backend_ctx->program_clamp = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_clamp = clCreateKernel(backend_ctx->program_clamp, "kernel_clamp", &err), err)); GGML_LOG_CONT("."); @@ -1378,7 +1401,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("cpy.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_cpy_f16_f16 = clCreateKernel(prog, "kernel_cpy_f16_f16", &err), err)); CL_CHECK((backend_ctx->kernel_cpy_f16_f32 = clCreateKernel(prog, "kernel_cpy_f16_f32", &err), err)); @@ -1399,7 +1422,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("cvt.cl"); #endif backend_ctx->program_cvt = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_convert_block_q1_0 = clCreateKernel(backend_ctx->program_cvt, "kernel_convert_block_q1_0", &err), err)); CL_CHECK((backend_ctx->kernel_restore_block_q1_0 = clCreateKernel(backend_ctx->program_cvt, "kernel_restore_block_q1_0", &err), err)); @@ -1482,7 +1505,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("diag_mask_inf.cl"); #endif backend_ctx->program_diag_mask_inf = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_diag_mask_inf_8 = clCreateKernel(backend_ctx->program_diag_mask_inf, "kernel_diag_mask_inf_8", &err), err)); CL_CHECK((backend_ctx->kernel_diag_mask_inf = clCreateKernel(backend_ctx->program_diag_mask_inf, "kernel_diag_mask_inf", &err), err)); @@ -1499,7 +1522,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("diag.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_diag_f32 = clCreateKernel(prog, "kernel_diag_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1516,7 +1539,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gelu.cl"); #endif backend_ctx->program_gelu = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gelu = clCreateKernel(backend_ctx->program_gelu, "kernel_gelu", &err), err)); CL_CHECK((backend_ctx->kernel_gelu_4 = clCreateKernel(backend_ctx->program_gelu, "kernel_gelu_4", &err), err)); @@ -1537,7 +1560,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("glu.cl"); #endif backend_ctx->program_glu = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_geglu = clCreateKernel(backend_ctx->program_glu, "kernel_geglu", &err), err)); CL_CHECK((backend_ctx->kernel_reglu = clCreateKernel(backend_ctx->program_glu, "kernel_reglu", &err), err)); @@ -1563,7 +1586,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("get_rows.cl"); #endif backend_ctx->program_get_rows = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_get_rows_f32 = clCreateKernel(backend_ctx->program_get_rows, "kernel_get_rows_f32", &err), err)); CL_CHECK((backend_ctx->kernel_get_rows_f16 = clCreateKernel(backend_ctx->program_get_rows, "kernel_get_rows_f16", &err), err)); @@ -1581,7 +1604,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("solve_tri.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_solve_tri_f32 = clCreateKernel(prog, "kernel_solve_tri_f32", &err), err)); GGML_LOG_CONT("."); @@ -1598,7 +1621,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("im2col_f32.cl"); #endif backend_ctx->program_im2col_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_im2col_f32 = clCreateKernel(backend_ctx->program_im2col_f32, "kernel_im2col_f32", &err), err)); GGML_LOG_CONT("."); @@ -1614,7 +1637,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("im2col_f16.cl"); #endif backend_ctx->program_im2col_f16 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_im2col_f16 = clCreateKernel(backend_ctx->program_im2col_f16, "kernel_im2col_f16", &err), err)); GGML_LOG_CONT("."); @@ -1630,7 +1653,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_0_f32.cl"); #endif backend_ctx->program_mul_mv_q4_0_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_q4_0_f32 = clCreateKernel(backend_ctx->program_mul_mv_q4_0_f32, "kernel_mul_mat_q4_0_f32", &err), err)); GGML_LOG_CONT("."); @@ -1646,7 +1669,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_0_f32_v.cl"); #endif backend_ctx->program_mul_mv_q4_0_f32_v = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_q4_0_f32_v = clCreateKernel(backend_ctx->program_mul_mv_q4_0_f32_v, "kernel_mul_mat_q4_0_f32_v", &err), err)); GGML_LOG_CONT("."); @@ -1662,7 +1685,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_0_f32_8x_flat.cl"); #endif backend_ctx->program_mul_mv_q4_0_f32_8x_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_q4_0_f32_8x_flat = clCreateKernel(backend_ctx->program_mul_mv_q4_0_f32_8x_flat, "kernel_mul_mat_q4_0_f32_8x_flat", &err), err)); GGML_LOG_CONT("."); @@ -1683,7 +1706,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_0_f32_1d_8x_flat.cl"); #endif backend_ctx->program_mul_mv_q4_0_f32_1d_8x_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_q4_0_f32_1d_8x_flat = clCreateKernel(backend_ctx->program_mul_mv_q4_0_f32_1d_8x_flat, "kernel_mul_mat_q4_0_f32_1d_8x_flat", &err), err)); GGML_LOG_CONT("."); @@ -1703,7 +1726,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_0_f32_1d_16x_flat.cl"); #endif backend_ctx->program_mul_mv_q4_0_f32_1d_16x_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_q4_0_f32_1d_16x_flat = clCreateKernel(backend_ctx->program_mul_mv_q4_0_f32_1d_16x_flat, "kernel_mul_mat_q4_0_f32_1d_16x_flat", &err), err)); GGML_LOG_CONT("."); @@ -1719,7 +1742,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_1_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q4_1_f32 = clCreateKernel(prog, "kernel_mul_mv_q4_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1736,7 +1759,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_1_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q4_1_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q4_1_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1753,7 +1776,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_k_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q4_K_f32 = clCreateKernel(prog, "kernel_mul_mv_q4_K_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1770,7 +1793,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q4_k_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q4_K_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q4_K_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1787,7 +1810,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_0_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_0_f32 = clCreateKernel(prog, "kernel_mul_mv_q5_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1804,7 +1827,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_0_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_0_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q5_0_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1821,7 +1844,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_1_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_1_f32 = clCreateKernel(prog, "kernel_mul_mv_q5_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1838,7 +1861,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_1_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_1_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q5_1_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1855,7 +1878,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_k_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_K_f32 = clCreateKernel(prog, "kernel_mul_mv_q5_K_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1872,7 +1895,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q5_k_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q5_K_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q5_K_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1888,7 +1911,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q6_k_f32.cl"); #endif backend_ctx->program_mul_mv_q6_K = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q6_K_f32 = clCreateKernel(backend_ctx->program_mul_mv_q6_K, "kernel_mul_mv_q6_K_f32", &err), err)); GGML_LOG_CONT("."); @@ -1904,7 +1927,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q6_k_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q6_K_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q6_K_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1921,7 +1944,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q8_0_f32.cl"); #endif backend_ctx->program_mul_mv_q8_0_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q8_0_f32 = clCreateKernel(backend_ctx->program_mul_mv_q8_0_f32, "kernel_mul_mv_q8_0_f32", &err), err)); GGML_LOG_CONT("."); @@ -1937,7 +1960,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q8_0_f32_flat.cl"); #endif backend_ctx->program_mul_mv_q8_0_f32_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q8_0_f32_flat = clCreateKernel(backend_ctx->program_mul_mv_q8_0_f32_flat, "kernel_mul_mv_q8_0_f32_flat", &err), err)); GGML_LOG_CONT("."); @@ -1953,7 +1976,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q1_0_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q1_0_f32 = clCreateKernel(prog, "kernel_mul_mv_q1_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1970,7 +1993,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_q1_0_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_q1_0_f32_flat = clCreateKernel(prog, "kernel_mul_mv_q1_0_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -1987,7 +2010,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_iq4_nl_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_iq4_nl_f32 = clCreateKernel(prog, "kernel_mul_mv_iq4_nl_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2004,7 +2027,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_iq4_nl_f32_flat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_iq4_nl_f32_flat = clCreateKernel(prog, "kernel_mul_mv_iq4_nl_f32_flat", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2021,7 +2044,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_mxfp4_f32.cl"); #endif backend_ctx->program_mul_mv_mxfp4_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_mxfp4_f32 = clCreateKernel(backend_ctx->program_mul_mv_mxfp4_f32, "kernel_mul_mv_mxfp4_f32", &err), err)); GGML_LOG_CONT("."); @@ -2037,7 +2060,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_mxfp4_f32_flat.cl"); #endif backend_ctx->program_mul_mv_mxfp4_f32_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_mxfp4_f32_flat = clCreateKernel(backend_ctx->program_mul_mv_mxfp4_f32_flat, "kernel_mul_mv_mxfp4_f32_flat", &err), err)); GGML_LOG_CONT("."); @@ -2053,7 +2076,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_f16_f16.cl"); #endif backend_ctx->program_mul_mv_f16_f16 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f16 = clCreateKernel(backend_ctx->program_mul_mv_f16_f16, "kernel_mul_mat_f16_f16", &err), err)); GGML_LOG_CONT("."); @@ -2069,7 +2092,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_f16_f32_1row.cl"); #endif backend_ctx->program_mul_mv_f16_f32_1row = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f32_1row = clCreateKernel(backend_ctx->program_mul_mv_f16_f32_1row, "kernel_mul_mat_f16_f32_1row", &err), err)); GGML_LOG_CONT("."); @@ -2085,7 +2108,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_f16_f32_l4.cl"); #endif backend_ctx->program_mul_mv_f16_f32_l4 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f32_l4 = clCreateKernel(backend_ctx->program_mul_mv_f16_f32_l4, "kernel_mul_mat_f16_f32_l4", &err), err)); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f32_l4_dr = clCreateKernel(backend_ctx->program_mul_mv_f16_f32_l4, "kernel_mul_mat_f16_f32_l4_dr", &err), err)); @@ -2151,7 +2174,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_f16_f32.cl"); #endif backend_ctx->program_mul_mv_f16_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f32 = clCreateKernel(backend_ctx->program_mul_mv_f16_f32, "kernel_mul_mat_f16_f32", &err), err)); GGML_LOG_CONT("."); @@ -2167,7 +2190,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_f32_f32.cl"); #endif backend_ctx->program_mul_mv_f32_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f32_f32 = clCreateKernel(backend_ctx->program_mul_mv_f32_f32, "kernel_mul_mat_f32_f32", &err), err)); GGML_LOG_CONT("."); @@ -2183,7 +2206,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mat_f16_f32.cl"); #endif backend_ctx->program_mul_mat_f16_f32_tiled = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mat_f16_f32_tiled = clCreateKernel(backend_ctx->program_mul_mat_f16_f32_tiled, "mul_mat_f16_f32", &err), err)); GGML_LOG_CONT("."); @@ -2200,7 +2223,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_xmem_f16_f32_os8.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_adreno_xmem_pack_src_f32 = clCreateKernel(prog, "adreno_xmem_pack_src_f32", &err), err)); @@ -2225,7 +2248,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_f32_f32_l4_lm.cl"); #endif backend_ctx->program_mul_mm_f32_f32_l4_lm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_f32_f32_l4_lm = clCreateKernel(backend_ctx->program_mul_mm_f32_f32_l4_lm, "kernel_mul_mm_f32_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2241,7 +2264,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_f16_f32_l4_lm.cl"); #endif backend_ctx->program_mul_mm_f16_f32_l4_lm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_f16_f32_l4_lm = clCreateKernel(backend_ctx->program_mul_mm_f16_f32_l4_lm, "kernel_mul_mm_f16_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2257,7 +2280,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q4_0_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q4_0_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q4_0_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2273,7 +2296,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q4_1_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q4_1_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q4_1_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2289,7 +2312,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q5_0_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q5_0_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q5_0_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2305,7 +2328,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q5_1_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q5_1_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q5_1_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2321,7 +2344,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q8_0_f32_l4_lm.cl"); #endif backend_ctx->program_mul_mm_q8_0_f32_l4_lm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q8_0_f32_l4_lm = clCreateKernel(backend_ctx->program_mul_mm_q8_0_f32_l4_lm, "kernel_mul_mm_q8_0_f32_l4_lm", &err), err)); GGML_LOG_CONT("."); @@ -2337,7 +2360,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q1_0_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q1_0_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q1_0_f32_l4_lm", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2354,7 +2377,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_iq4_nl_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_iq4_nl_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_iq4_nl_f32_l4_lm", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2371,7 +2394,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q4_k_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q4_k_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q4_k_f32_l4_lm", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2388,7 +2411,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q6_k_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q6_k_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q6_k_f32_l4_lm", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2405,7 +2428,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_q5_k_f32_l4_lm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_q5_k_f32_l4_lm = clCreateKernel(prog, "kernel_mul_mm_q5_k_f32_l4_lm", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2422,9 +2445,9 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mm_f16_f32_kq_kqv.cl"); #endif backend_ctx->program_mul_mm_f16_f32_kqv = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts+" -DKQV "); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts+" -DKQV "); backend_ctx->program_mul_mm_f16_f32_kq = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mm_f16_f32_kqv = clCreateKernel(backend_ctx->program_mul_mm_f16_f32_kqv, "mul_mm_f16_f32_kqv", &err), err)); CL_CHECK((backend_ctx->kernel_mul_mm_f16_f32_kq = clCreateKernel(backend_ctx->program_mul_mm_f16_f32_kq, "mul_mm_f16_f32_kq", &err), err)); @@ -2441,7 +2464,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul.cl"); #endif backend_ctx->program_mul = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul = clCreateKernel(backend_ctx->program_mul, "kernel_mul", &err), err)); CL_CHECK((backend_ctx->kernel_mul_row = clCreateKernel(backend_ctx->program_mul, "kernel_mul_row", &err), err)); @@ -2460,7 +2483,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("norm.cl"); #endif backend_ctx->program_norm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_norm = clCreateKernel(backend_ctx->program_norm, "kernel_norm", &err), err)); CL_CHECK((backend_ctx->kernel_norm_mul_add = clCreateKernel(backend_ctx->program_norm, "kernel_norm_mul_add", &err), err)); @@ -2477,7 +2500,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("relu.cl"); #endif backend_ctx->program_relu = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_relu = clCreateKernel(backend_ctx->program_relu, "kernel_relu", &err), err)); GGML_LOG_CONT("."); @@ -2493,7 +2516,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("rms_norm.cl"); #endif backend_ctx->program_rms_norm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_rms_norm = clCreateKernel(backend_ctx->program_rms_norm, "kernel_rms_norm", &err), err)); CL_CHECK((backend_ctx->kernel_rms_norm_mul = clCreateKernel(backend_ctx->program_rms_norm, "kernel_rms_norm_mul", &err), err)); @@ -2510,7 +2533,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("l2_norm.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_l2_norm_f32 = clCreateKernel(prog, "kernel_l2_norm_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -2527,7 +2550,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("rope.cl"); #endif backend_ctx->program_rope = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_rope_norm_f32 = clCreateKernel(backend_ctx->program_rope, "kernel_rope_norm_f32", &err), err)); CL_CHECK((backend_ctx->kernel_rope_norm_f16 = clCreateKernel(backend_ctx->program_rope, "kernel_rope_norm_f16", &err), err)); @@ -2550,7 +2573,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("scale.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_scale_f32 = clCreateKernel(prog, "kernel_scale_f32", &err), err)); CL_CHECK((backend_ctx->kernel_scale_f32_4 = clCreateKernel(prog, "kernel_scale_f32_4", &err), err)); @@ -2568,7 +2591,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("silu.cl"); #endif backend_ctx->program_silu = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_silu = clCreateKernel(backend_ctx->program_silu, "kernel_silu", &err), err)); CL_CHECK((backend_ctx->kernel_silu_4 = clCreateKernel(backend_ctx->program_silu, "kernel_silu_4", &err), err)); @@ -2585,7 +2608,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("softmax_f32.cl"); #endif backend_ctx->program_softmax_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_soft_max = clCreateKernel(backend_ctx->program_softmax_f32, "kernel_soft_max", &err), err)); GGML_LOG_CONT("."); @@ -2601,7 +2624,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("softmax_f16.cl"); #endif backend_ctx->program_softmax_f16 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_soft_max_f16 = clCreateKernel(backend_ctx->program_softmax_f16, "kernel_soft_max_f16", &err), err)); GGML_LOG_CONT("."); @@ -2617,7 +2640,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("softmax_4_f32.cl"); #endif backend_ctx->program_softmax_4_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_soft_max_4 = clCreateKernel(backend_ctx->program_softmax_4_f32, "kernel_soft_max_4", &err), err)); GGML_LOG_CONT("."); @@ -2633,7 +2656,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("softmax_4_f16.cl"); #endif backend_ctx->program_softmax_4_f16 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_soft_max_4_f16 = clCreateKernel(backend_ctx->program_softmax_4_f16, "kernel_soft_max_4_f16", &err), err)); GGML_LOG_CONT("."); @@ -2652,7 +2675,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { " -cl-mad-enable -cl-finite-math-only "; backend_ctx->program_div = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_div = clCreateKernel(backend_ctx->program_div, "kernel_div", &err), err)); CL_CHECK((backend_ctx->kernel_div_row = clCreateKernel(backend_ctx->program_div, "kernel_div_row", &err), err)); @@ -2671,7 +2694,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("sqr.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_sqr_cont_f32 = clCreateKernel(prog, "kernel_sqr_cont_f32", &err), err)); CL_CHECK((backend_ctx->kernel_sqr_cont_f32_4 = clCreateKernel(prog, "kernel_sqr_cont_f32_4", &err), err)); @@ -2692,7 +2715,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("sqrt.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_sqrt_cont_f32 = clCreateKernel(prog, "kernel_sqrt_cont_f32", &err), err)); CL_CHECK((backend_ctx->kernel_sqrt_cont_f32_4 = clCreateKernel(prog, "kernel_sqrt_cont_f32_4", &err), err)); @@ -2713,7 +2736,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mean.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mean_f32 = clCreateKernel(prog, "kernel_mean_f32", &err), err)); CL_CHECK((backend_ctx->kernel_mean_f32_4 = clCreateKernel(prog, "kernel_mean_f32_4", &err), err)); @@ -2732,7 +2755,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("sub.cl"); #endif backend_ctx->program_sub = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_sub = clCreateKernel(backend_ctx->program_sub, "kernel_sub", &err), err)); CL_CHECK((backend_ctx->kernel_sub_row = clCreateKernel(backend_ctx->program_sub, "kernel_sub_row", &err), err)); @@ -2751,7 +2774,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("sum_rows.cl"); #endif backend_ctx->program_sum_rows_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_sum_rows_f32 = clCreateKernel(backend_ctx->program_sum_rows_f32, "kernel_sum_rows_f32", &err), err)); CL_CHECK((backend_ctx->kernel_sum_rows_f32_4 = clCreateKernel(backend_ctx->program_sum_rows_f32, "kernel_sum_rows_f32_4", &err), err)); @@ -2768,7 +2791,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("cumsum.cl"); #endif cl_program prog; - prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_cumsum_blk = clCreateKernel(prog, "kernel_cumsum_blk", &err), err)); CL_CHECK((backend_ctx->kernel_cumsum_add = clCreateKernel(prog, "kernel_cumsum_add", &err), err)); @@ -2786,7 +2809,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("sigmoid.cl"); #endif backend_ctx->program_sigmoid = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_sigmoid_f32 = clCreateKernel(backend_ctx->program_sigmoid, "kernel_sigmoid_f32", &err), err)); CL_CHECK((backend_ctx->kernel_sigmoid_f16 = clCreateKernel(backend_ctx->program_sigmoid, "kernel_sigmoid_f16", &err), err)); @@ -2803,7 +2826,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("group_norm.cl"); #endif backend_ctx->program_group_norm = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_group_norm = clCreateKernel(backend_ctx->program_group_norm, "kernel_group_norm", &err), err)); CL_CHECK((backend_ctx->kernel_group_norm_mul_add = clCreateKernel(backend_ctx->program_group_norm, "kernel_group_norm_mul_add", &err), err)); @@ -2820,7 +2843,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("repeat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_repeat_f32 = clCreateKernel(prog, "kernel_repeat_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -2837,7 +2860,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #endif if (!kernel_src.empty()) { backend_ctx->program_pad = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_pad = clCreateKernel(backend_ctx->program_pad, "kernel_pad", &err), err)); GGML_LOG_CONT("."); } else { @@ -2857,7 +2880,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("tanh.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_tanh_f32 = clCreateKernel(prog, "kernel_tanh_f32", &err), err)); CL_CHECK((backend_ctx->kernel_tanh_f32_4 = clCreateKernel(prog, "kernel_tanh_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_tanh_f32_nc = clCreateKernel(prog, "kernel_tanh_f32_nc", &err), err)); @@ -2878,7 +2901,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("neg.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_neg_f32 = clCreateKernel(prog, "kernel_neg_f32", &err), err)); CL_CHECK((backend_ctx->kernel_neg_f32_4 = clCreateKernel(prog, "kernel_neg_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_neg_f32_nc = clCreateKernel(prog, "kernel_neg_f32_nc", &err), err)); @@ -2899,7 +2922,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("exp.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_exp_f32 = clCreateKernel(prog, "kernel_exp_f32", &err), err)); CL_CHECK((backend_ctx->kernel_exp_f32_4 = clCreateKernel(prog, "kernel_exp_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_exp_f32_nc = clCreateKernel(prog, "kernel_exp_f32_nc", &err), err)); @@ -2920,7 +2943,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("expm1.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_expm1_f32 = clCreateKernel(prog, "kernel_expm1_f32", &err), err)); CL_CHECK((backend_ctx->kernel_expm1_f32_4 = clCreateKernel(prog, "kernel_expm1_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_expm1_f32_nc = clCreateKernel(prog, "kernel_expm1_f32_nc", &err), err)); @@ -2941,7 +2964,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("abs.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_abs_f32 = clCreateKernel(prog, "kernel_abs_f32", &err), err)); CL_CHECK((backend_ctx->kernel_abs_f32_4 = clCreateKernel(prog, "kernel_abs_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_abs_f32_nc = clCreateKernel(prog, "kernel_abs_f32_nc", &err), err)); @@ -2962,7 +2985,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("softplus.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_softplus_f32 = clCreateKernel(prog, "kernel_softplus_f32", &err), err)); CL_CHECK((backend_ctx->kernel_softplus_f32_4 = clCreateKernel(prog, "kernel_softplus_f32_4", &err), err)); CL_CHECK((backend_ctx->kernel_softplus_f32_nc = clCreateKernel(prog, "kernel_softplus_f32_nc", &err), err)); @@ -2984,7 +3007,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #endif if (!kernel_src.empty()) { backend_ctx->program_upscale = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_upscale = clCreateKernel(backend_ctx->program_upscale, "kernel_upscale", &err), err)); if (backend_ctx->program_upscale) { cl_int err_bilinear; @@ -3015,7 +3038,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("concat.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_concat_f32 = clCreateKernel(prog, "kernel_concat_f32", &err), err)); CL_CHECK((backend_ctx->kernel_concat_f32_pack = clCreateKernel(prog, "kernel_concat_f32_pack", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3034,7 +3057,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #endif if (!kernel_src.empty()) { backend_ctx->program_tsembd = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_timestep_embedding = clCreateKernel(backend_ctx->program_tsembd, "kernel_timestep_embedding", &err), err)); GGML_LOG_CONT("."); } else { @@ -3054,7 +3077,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("set_rows.cl"); #endif backend_ctx->program_set_rows = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_set_rows_f32_i64 = clCreateKernel(backend_ctx->program_set_rows, "kernel_set_rows_f32_i64", &err), err)); CL_CHECK((backend_ctx->kernel_set_rows_f32_i32 = clCreateKernel(backend_ctx->program_set_rows, "kernel_set_rows_f32_i32", &err), err)); @@ -3086,11 +3109,11 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #endif if (!kernel_src.empty()) { backend_ctx->program_conv_2d_f16 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), (std::string(compile_opts) + " -DUSE_FP16=1").c_str()); + build_program_from_source(backend_ctx, kernel_src.c_str(), (std::string(compile_opts) + " -DUSE_FP16=1").c_str()); CL_CHECK((backend_ctx->kernel_conv_2d_f16 = clCreateKernel(backend_ctx->program_conv_2d_f16, "kernel_conv_2d", &err), err)); GGML_LOG_CONT("."); backend_ctx->program_conv_2d_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_conv_2d_f32 = clCreateKernel(backend_ctx->program_conv_2d_f32, "kernel_conv_2d", &err), err)); GGML_LOG_CONT("."); } else { @@ -3102,7 +3125,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { } if (!kernel_src_f16_f32.empty()) { backend_ctx->program_conv_2d_f16_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src_f16_f32.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src_f16_f32.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_conv_2d_f16_f32 = clCreateKernel(backend_ctx->program_conv_2d_f16_f32, "kernel_conv_2d", &err), err)); GGML_LOG_CONT("."); } else { @@ -3122,7 +3145,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("ssm_conv.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_ssm_conv_f32_f32 = clCreateKernel(prog, "kernel_ssm_conv_f32_f32", &err), err)); CL_CHECK((backend_ctx->kernel_ssm_conv_f32_f32_4 = clCreateKernel(prog, "kernel_ssm_conv_f32_f32_4", &err), err)); @@ -3198,8 +3221,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { // Please remember to implement code to handle it. opts += " -DSUBGROUPS_PER_WG=" + std::to_string(spw); - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), opts); CL_CHECK((backend_ctx->kernel_gated_delta_net_f32[si][kda][tgpp] = clCreateKernel(prog, "kernel_gated_delta_net", &err), err)); @@ -3220,7 +3242,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("moe_combine.cl"); #endif cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_moe_combine_f32 = clCreateKernel(prog, "kernel_moe_combine_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3237,7 +3259,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_id_q4_0_f32_8x_flat.cl"); #endif backend_ctx->program_mul_mv_id_q4_0_f32_8x_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_id_q4_0_f32_8x_flat = clCreateKernel(backend_ctx->program_mul_mv_id_q4_0_f32_8x_flat, "kernel_mul_mv_id_q4_0_f32_8x_flat", &err), err)); GGML_LOG_CONT("."); @@ -3253,7 +3275,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_id_q8_0_f32.cl"); #endif backend_ctx->program_mul_mv_id_q8_0_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_id_q8_0_f32 = clCreateKernel(backend_ctx->program_mul_mv_id_q8_0_f32, "kernel_mul_mv_id_q8_0_f32", &err), err)); GGML_LOG_CONT("."); @@ -3269,7 +3291,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_id_q8_0_f32_flat.cl"); #endif backend_ctx->program_mul_mv_id_q8_0_f32_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_id_q8_0_f32_flat = clCreateKernel(backend_ctx->program_mul_mv_id_q8_0_f32_flat, "kernel_mul_mv_id_q8_0_f32_flat", &err), err)); GGML_LOG_CONT("."); @@ -3285,7 +3307,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_id_mxfp4_f32.cl"); #endif backend_ctx->program_mul_mv_id_mxfp4_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_id_mxfp4_f32 = clCreateKernel(backend_ctx->program_mul_mv_id_mxfp4_f32, "kernel_mul_mv_id_mxfp4_f32", &err), err)); GGML_LOG_CONT("."); @@ -3301,7 +3323,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("mul_mv_id_mxfp4_f32_flat.cl"); #endif backend_ctx->program_mul_mv_id_mxfp4_f32_flat = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_mul_mv_id_mxfp4_f32_flat = clCreateKernel(backend_ctx->program_mul_mv_id_mxfp4_f32_flat, "kernel_mul_mv_id_mxfp4_f32_flat", &err), err)); GGML_LOG_CONT("."); @@ -3319,7 +3341,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("transpose.cl"); #endif backend_ctx->program_transpose = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_transpose_32_16 = clCreateKernel(backend_ctx->program_transpose, "kernel_transpose_32_16", &err), err)); CL_CHECK((backend_ctx->kernel_transpose_32 = clCreateKernel(backend_ctx->program_transpose, "kernel_transpose_32", &err), err)); @@ -3340,7 +3362,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q1_0_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q1_0_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q1_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3361,8 +3383,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src_CL_gemv_general = read_file("gemv_noshuffle_q1_0_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q1_0_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q1_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3387,8 +3408,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src_CL_gemv_general = read_file("gemv_noshuffle_q4_0_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_0_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3416,8 +3436,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src_CL_gemv = read_file("gemv_noshuffle_q4_0_f32_spec.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_0_f32_4096_1_4096 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3433,8 +3452,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { CL_gemv_compile_opts += " -DVECTOR_SUB_GROUP_BROADCAST "; } - prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); + prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_0_f32_4096_1_11008 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3450,8 +3468,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { CL_gemv_compile_opts += " -DVECTOR_SUB_GROUP_BROADCAST "; } - prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); + prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_0_f32_11008_1_4096 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3468,8 +3485,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { CL_gemv_compile_opts += " -DVECTOR_SUB_GROUP_BROADCAST "; } - prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); + prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_0_f32_32000_1_4096 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3484,7 +3500,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src_CL_gemm = read_file("gemm_noshuffle_q4_0_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src_CL_gemm.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src_CL_gemm.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_0_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3499,7 +3515,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q4_1_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_1_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3521,8 +3537,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_noshuffle_q4_1_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_1_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3538,7 +3553,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q5_0_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_0_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3553,7 +3568,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q5_0_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_0_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_0_q8_1_dp4a", &err), err)); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_0_q8_1_dp4a_wimg = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_0_q8_1_dp4a_wimg", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3575,8 +3590,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemv_noshuffle_q5_0_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q5_0_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q5_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3591,7 +3605,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q5_1_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_1_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3612,8 +3626,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemv_noshuffle_q5_1_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q5_1_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q5_1_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3628,7 +3641,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_iq4_nl_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_iq4_nl_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_iq4_nl_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3643,7 +3656,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_iq4_nl_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_iq4_nl_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_iq4_nl_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3658,7 +3671,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q4_0_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_0_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_0_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3680,8 +3693,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_noshuffle_iq4_nl_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_iq4_nl_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_iq4_nl_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3697,7 +3709,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q8_0_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q8_0_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q8_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3739,8 +3751,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src_CL_gemv_general = read_file("gemv_noshuffle_q8_0_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src_CL_gemv_general.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q8_0_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q8_0_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3756,7 +3767,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q4_k_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_k_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3775,9 +3786,9 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { // X1 (1152 B/WG -> few resident WGs); TILESIZE_N=8 (288 B) lifts occupancy on // X1, byte-identical. X2E keeps 32. Env override wins. int q4k_dp4a_ts = (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E) ? 8 : 32; - if (const char * e = getenv("GGML_OPENCL_Q4K_DP4A_TS")) q4k_dp4a_ts = atoi(e); + if (const char * e = getenv("GGML_OPENCL_Q4K_DP4A_TS")) { q4k_dp4a_ts = atoi(e); } std::string dp4a_opts = compile_opts + " -DTILESIZE_N=" + std::to_string(q4k_dp4a_ts); - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), dp4a_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), dp4a_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a", &err), err)); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg = clCreateKernel(prog, "kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3793,7 +3804,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q8_0_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q8_0_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q8_0_q8_1_dp4a", &err), err)); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q8_0_q8_1_dp4a_wimg = clCreateKernel(prog, "kernel_gemm_noshuffle_q8_0_q8_1_dp4a_wimg", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3809,7 +3820,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q5_k_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_k_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3824,7 +3835,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q6_k_q8_1_dp4a.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q6_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_noshuffle_q6_k_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3839,7 +3850,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("quant_a_q8_1.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_quant_a_q8_1 = clCreateKernel(prog, "kernel_quant_a_q8_1", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -3861,8 +3872,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_noshuffle_q4_k_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q4_k_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q4_k_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3882,7 +3892,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemv_moe_q4_1_f32_ns.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q4_1_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q4_1_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3898,7 +3908,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_moe_q4_1_f32_ns.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q4_1_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q4_1_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3933,7 +3943,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_mxfp4_f32.cl"); #endif backend_ctx->program_gemv_moe_mxfp4_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_mxfp4_f32 = clCreateKernel(backend_ctx->program_gemv_moe_mxfp4_f32, "kernel_gemv_moe_mxfp4_f32", &err), err)); GGML_LOG_CONT("."); @@ -3949,7 +3959,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_mxfp4_f32.cl"); #endif backend_ctx->program_gemm_moe_mxfp4_f32 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_mxfp4_f32 = clCreateKernel(backend_ctx->program_gemm_moe_mxfp4_f32, "kernel_gemm_moe_mxfp4_f32", &err), err)); GGML_LOG_CONT("."); @@ -3965,7 +3975,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q4_0_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q4_0_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q4_0_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -3982,7 +3992,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q4_0_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q4_0_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q4_0_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4017,7 +4027,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q8_0_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q8_0_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q8_0_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4034,7 +4044,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q5_0_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q5_0_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q5_0_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4051,7 +4061,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q5_0_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q5_0_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q5_0_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4068,7 +4078,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q5_1_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q5_1_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q5_1_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4085,7 +4095,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q5_1_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q5_1_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q5_1_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4102,7 +4112,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q4_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q4_k_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q4_k_f32_ns", &err), err)); CL_CHECK((backend_ctx->kernel_gemv_moe_q4_k_f32_ns_wimg = clCreateKernel(prog, "kernel_gemv_moe_q4_k_f32_ns_wimg", &err), err)); @@ -4120,7 +4130,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q4_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q4_k_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q4_k_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4155,7 +4165,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q4_k_q8_1_dp4a.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q4_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_moe_q4_k_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4172,7 +4182,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_mxfp4_q8_1_dp4a.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_mxfp4_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_moe_mxfp4_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4189,7 +4199,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q4_0_q8_1_dp4a.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q4_0_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_moe_q4_0_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4207,19 +4217,19 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #endif const std::string opts80 = CL_moe_compile_opts + " -DMOE_QT=80"; cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), opts80.c_str()); + build_program_from_source(backend_ctx, kernel_src.c_str(), opts80.c_str()); CL_CHECK((backend_ctx->kernel_gemm_moe_q8_1_dp4a_q80 = clCreateKernel(prog, "kernel_gemm_moe_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); const std::string opts50 = CL_moe_compile_opts + " -DMOE_QT=50"; cl_program prog50 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), opts50.c_str()); + build_program_from_source(backend_ctx, kernel_src.c_str(), opts50.c_str()); CL_CHECK((backend_ctx->kernel_gemm_moe_q8_1_dp4a_q50 = clCreateKernel(prog50, "kernel_gemm_moe_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog50)); const std::string opts5 = CL_moe_compile_opts + " -DMOE_QT=5"; cl_program prog5 = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), opts5.c_str()); + build_program_from_source(backend_ctx, kernel_src.c_str(), opts5.c_str()); CL_CHECK((backend_ctx->kernel_gemm_moe_q8_1_dp4a_q5k = clCreateKernel(prog5, "kernel_gemm_moe_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog5)); GGML_LOG_CONT("."); @@ -4235,7 +4245,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("moe_reorder_quant_a_q8_1.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_moe_reorder_quant_a_q8_1 = clCreateKernel(prog, "kernel_moe_reorder_quant_a_q8_1", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4252,7 +4262,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q5_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q5_k_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q5_k_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4269,7 +4279,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q5_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q5_k_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q5_k_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4286,7 +4296,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_q6_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_q6_k_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_q6_k_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4303,7 +4313,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q6_k_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q6_k_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_q6_k_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4338,7 +4348,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_q6_k_q8_1_dp4a.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_q6_k_q8_1_dp4a = clCreateKernel(prog, "kernel_gemm_moe_q6_k_q8_1_dp4a", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4355,7 +4365,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_moe_mxfp4_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_moe_mxfp4_f32_ns = clCreateKernel(prog, "kernel_gemv_moe_mxfp4_f32_ns", &err), err)); CL_CHECK((backend_ctx->kernel_gemv_moe_mxfp4_f32_ns_wimg = clCreateKernel(prog, "kernel_gemv_moe_mxfp4_f32_ns_wimg", &err), err)); @@ -4373,7 +4383,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_moe_mxfp4_f32_ns.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_moe_mxfp4_f32_ns = clCreateKernel(prog, "kernel_gemm_moe_mxfp4_f32_ns", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4408,7 +4418,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("moe_reorder_b.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_moe_reorder_b = clCreateKernel(prog, "kernel_moe_reorder_b", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4425,7 +4435,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("moe_sort_by_expert.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_moe_histogram = clCreateKernel(prog, "kernel_moe_histogram", &err), err)); CL_CHECK((backend_ctx->kernel_moe_scan = clCreateKernel(prog, "kernel_moe_scan", &err), err)); @@ -4452,7 +4462,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { } cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q6_K_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q6_K_f32", &err), err)); GGML_LOG_CONT("."); @@ -4468,7 +4478,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemm_noshuffle_q6_k_f32.cl"); #endif cl_program prog = - build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_moe_compile_opts); + build_program_from_source(backend_ctx, kernel_src.c_str(), CL_moe_compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q6_K_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q6_K_f32", &err), err)); GGML_LOG_CONT("."); @@ -4490,8 +4500,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { const std::string kernel_src = read_file("gemv_noshuffle_q5_k_f32.cl"); #endif - cl_program prog = build_program_from_source( - backend_ctx->context, backend_ctx->device, kernel_src.c_str(), CL_gemv_compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), CL_gemv_compile_opts); CL_CHECK((backend_ctx->kernel_gemv_noshuffle_q5_k_f32 = clCreateKernel(prog, "kernel_gemv_noshuffle_q5_k_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); @@ -4507,7 +4516,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) { #else const std::string kernel_src = read_file("gemm_noshuffle_q5_k_f32.cl"); #endif - cl_program prog = build_program_from_source(backend_ctx->context, backend_ctx->device, kernel_src.c_str(), compile_opts); + cl_program prog = build_program_from_source(backend_ctx, kernel_src.c_str(), compile_opts); CL_CHECK((backend_ctx->kernel_gemm_noshuffle_q5_k_f32 = clCreateKernel(prog, "kernel_gemm_noshuffle_q5_k_f32", &err), err)); CL_CHECK(clReleaseProgram(prog)); GGML_LOG_CONT("."); @@ -10636,6 +10645,11 @@ static const char * ggml_backend_opencl_buffer_type_get_name(ggml_backend_buffer static ggml_backend_buffer_t ggml_backend_opencl_buffer_type_alloc_buffer(ggml_backend_buffer_type_t buffer_type, size_t size) { ggml_backend_opencl_context *backend_ctx = ggml_cl_init(buffer_type->device); + + if (!backend_ctx->program_cache_initialized) { + backend_ctx->program_cache = cl_program_cache_init(backend_ctx->device); + backend_ctx->program_cache_initialized = true; + } load_cl_kernels(backend_ctx); // clCreateBuffer returns -61 for size 0