]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
feat(ui): add symbolic math support to JS sandbox via nerdamer (#25948)
authorrankaiyx <redacted>
Wed, 22 Jul 2026 15:52:55 +0000 (23:52 +0800)
committerGitHub <redacted>
Wed, 22 Jul 2026 15:52:55 +0000 (17:52 +0200)
* 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 <redacted>
29 files changed:
tools/ui/.gitignore
tools/ui/.prettierignore
tools/ui/eslint.config.js
tools/ui/scripts/vite-plugin-nerdamer.ts [new file with mode: 0644]
tools/ui/src/lib/constants/sandbox.ts
tools/ui/src/lib/constants/settings-keys.ts
tools/ui/src/lib/constants/settings-registry.ts
tools/ui/src/lib/services/index.ts
tools/ui/src/lib/services/sandbox-harness.ts
tools/ui/src/lib/services/sandbox-worker.js
tools/ui/src/lib/services/sandbox.service.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/vendors/big-integer/BigInteger.js [new file with mode: 0644]
tools/ui/src/lib/vendors/big-integer/LICENSE [new file with mode: 0644]
tools/ui/src/lib/vendors/decimal.js/LICENCE.md [new file with mode: 0644]
tools/ui/src/lib/vendors/decimal.js/decimal.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/Algebra.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/Calculus.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/Extra.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/LICENSE [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/Solve.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/all.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/constants.js [new file with mode: 0644]
tools/ui/src/lib/vendors/nerdamer-prime/nerdamer.core.js [new file with mode: 0644]
tools/ui/src/lib/vendors/package.json [new file with mode: 0644]
tools/ui/src/virtual-nerdamer.d.ts [new file with mode: 0644]
tools/ui/tests/client/sandbox.service.svelte.test.ts [new file with mode: 0644]
tools/ui/tsconfig.json
tools/ui/vite.config.ts

index 0bb8c9b3c2189143e3982c0ec6d1f67de3ed87a3..7cd35376e1991cb5c7d71b6e9a991131eff43b44 100644 (file)
@@ -36,3 +36,7 @@ static/favicon*
 *storybook.log
 storybook-static
 *.code-workspace
+
+# Vitest browser mode failure artifacts
+.vitest-attachments/
+tests/**/__screenshots__/
index 7bbdcf6a0963f9568d05ad8e75a65794fff47d33..635cf99c7e9c3b6c33144e277a62ba8cd010ce7b 100644 (file)
@@ -16,3 +16,6 @@ build/
 /build/
 /.svelte-kit/
 test-results
+
+# Vendored third party sources, kept byte identical to upstream
+src/lib/vendors/
index 54679a05be17a9ed9bbef01a2b4422f3650c0310..fcbf7ee954899045b50efd0a8851f1ff17dea99a 100644 (file)
@@ -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 (file)
index 0000000..218c2fa
--- /dev/null
@@ -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)};`;
+               }
+       };
+}
index de49f05847c1a3c0aaa9f1d17ac6fb6da8a66cf8..58242678d98b2cbd0fb3ea7b792b33a5a535d427 100644 (file)
@@ -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);
index fab343659d9b56978e233a40edecd2895c47c151..265507a5b7a040caaec03bb56768e107ca90a3b9 100644 (file)
@@ -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'
index f511b751de9f3c54b59971936d133605c99c10aa..742b5f5c67754c9e3ae23296aff5aa60d856731d 100644 (file)
@@ -724,6 +724,15 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
                                        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',
index 386f740b8f04a4f1d5bcb11a9edae7ebf0f08e85..8704b1bd6ce52624264ce88f75701c9820faaeb7 100644 (file)
@@ -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';
index 27b05e24b4b4a33416328dfa79b2fac88b47cab8..40502121fb37e09baee6109ae396c1e89c344a30 100644 (file)
@@ -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 = `<!doctype html><script>
-const SHIM = ${JSON.stringify(WORKER_SHIM)};
+export function buildSandboxHarness(preludeJs: string): string {
+       return `<!doctype html><meta http-equiv="Content-Security-Policy" content="${HARNESS_CSP}"><script>
+const SHIM = ${JSON.stringify(preludeJs + NEWLINE + WORKER_SHIM)};
 addEventListener('message', (event) => {
        const respond = (payload) => parent.postMessage(payload, '*');
        let worker;
@@ -23,3 +34,4 @@ addEventListener('message', (event) => {
        worker.postMessage({ code: event.data.code });
 });
 </script>`;
+}
index 689b9211eb5403749353eeaceab8fcf4d94746e1..838a97db19d648a44a71e76f7e0702d3a1cf80a8 100644 (file)
@@ -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);
index 36bf2d0203bbbdce90e3e90665b625c5805fcde0..f49a774a08e911280c66e14e2b0e567c43d8e1b6 100644 (file)
@@ -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<string, string> = {};
+
+/**
+ * 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<string> {
+       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<string, unknown>,
                signal?: AbortSignal
        ): Promise<ToolExecutionResult> {
                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;
 
index 53b6ad9ee7994463ad780783e9d73fdff3ce8b6d..bd69c4d30f38d324b22449cd6d4c220aa7a31195 100644 (file)
@@ -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 (file)
index 0000000..87e43df
--- /dev/null
@@ -0,0 +1,1453 @@
+var bigInt = (function (undefined) {\r
+    "use strict";\r
+\r
+    var BASE = 1e7,\r
+        LOG_BASE = 7,\r
+        MAX_INT = 9007199254740992,\r
+        MAX_INT_ARR = smallToArray(MAX_INT),\r
+        DEFAULT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";\r
+\r
+    var supportsNativeBigInt = typeof BigInt === "function";\r
+\r
+    function Integer(v, radix, alphabet, caseSensitive) {\r
+        if (typeof v === "undefined") return Integer[0];\r
+        if (typeof radix !== "undefined") return +radix === 10 && !alphabet ? parseValue(v) : parseBase(v, radix, alphabet, caseSensitive);\r
+        return parseValue(v);\r
+    }\r
+\r
+    function BigInteger(value, sign) {\r
+        this.value = value;\r
+        this.sign = sign;\r
+        this.isSmall = false;\r
+    }\r
+    BigInteger.prototype = Object.create(Integer.prototype);\r
+\r
+    function SmallInteger(value) {\r
+        this.value = value;\r
+        this.sign = value < 0;\r
+        this.isSmall = true;\r
+    }\r
+    SmallInteger.prototype = Object.create(Integer.prototype);\r
+\r
+    function NativeBigInt(value) {\r
+        this.value = value;\r
+    }\r
+    NativeBigInt.prototype = Object.create(Integer.prototype);\r
+\r
+    function isPrecise(n) {\r
+        return -MAX_INT < n && n < MAX_INT;\r
+    }\r
+\r
+    function smallToArray(n) { // For performance reasons doesn't reference BASE, need to change this function if BASE changes\r
+        if (n < 1e7)\r
+            return [n];\r
+        if (n < 1e14)\r
+            return [n % 1e7, Math.floor(n / 1e7)];\r
+        return [n % 1e7, Math.floor(n / 1e7) % 1e7, Math.floor(n / 1e14)];\r
+    }\r
+\r
+    function arrayToSmall(arr) { // If BASE changes this function may need to change\r
+        trim(arr);\r
+        var length = arr.length;\r
+        if (length < 4 && compareAbs(arr, MAX_INT_ARR) < 0) {\r
+            switch (length) {\r
+                case 0: return 0;\r
+                case 1: return arr[0];\r
+                case 2: return arr[0] + arr[1] * BASE;\r
+                default: return arr[0] + (arr[1] + arr[2] * BASE) * BASE;\r
+            }\r
+        }\r
+        return arr;\r
+    }\r
+\r
+    function trim(v) {\r
+        var i = v.length;\r
+        while (v[--i] === 0);\r
+        v.length = i + 1;\r
+    }\r
+\r
+    function createArray(length) { // function shamelessly stolen from Yaffle's library https://github.com/Yaffle/BigInteger\r
+        var x = new Array(length);\r
+        var i = -1;\r
+        while (++i < length) {\r
+            x[i] = 0;\r
+        }\r
+        return x;\r
+    }\r
+\r
+    function truncate(n) {\r
+        if (n > 0) return Math.floor(n);\r
+        return Math.ceil(n);\r
+    }\r
+\r
+    function add(a, b) { // assumes a and b are arrays with a.length >= b.length\r
+        var l_a = a.length,\r
+            l_b = b.length,\r
+            r = new Array(l_a),\r
+            carry = 0,\r
+            base = BASE,\r
+            sum, i;\r
+        for (i = 0; i < l_b; i++) {\r
+            sum = a[i] + b[i] + carry;\r
+            carry = sum >= base ? 1 : 0;\r
+            r[i] = sum - carry * base;\r
+        }\r
+        while (i < l_a) {\r
+            sum = a[i] + carry;\r
+            carry = sum === base ? 1 : 0;\r
+            r[i++] = sum - carry * base;\r
+        }\r
+        if (carry > 0) r.push(carry);\r
+        return r;\r
+    }\r
+\r
+    function addAny(a, b) {\r
+        if (a.length >= b.length) return add(a, b);\r
+        return add(b, a);\r
+    }\r
+\r
+    function addSmall(a, carry) { // assumes a is array, carry is number with 0 <= carry < MAX_INT\r
+        var l = a.length,\r
+            r = new Array(l),\r
+            base = BASE,\r
+            sum, i;\r
+        for (i = 0; i < l; i++) {\r
+            sum = a[i] - base + carry;\r
+            carry = Math.floor(sum / base);\r
+            r[i] = sum - carry * base;\r
+            carry += 1;\r
+        }\r
+        while (carry > 0) {\r
+            r[i++] = carry % base;\r
+            carry = Math.floor(carry / base);\r
+        }\r
+        return r;\r
+    }\r
+\r
+    BigInteger.prototype.add = function (v) {\r
+        var n = parseValue(v);\r
+        if (this.sign !== n.sign) {\r
+            return this.subtract(n.negate());\r
+        }\r
+        var a = this.value, b = n.value;\r
+        if (n.isSmall) {\r
+            return new BigInteger(addSmall(a, Math.abs(b)), this.sign);\r
+        }\r
+        return new BigInteger(addAny(a, b), this.sign);\r
+    };\r
+    BigInteger.prototype.plus = BigInteger.prototype.add;\r
+\r
+    SmallInteger.prototype.add = function (v) {\r
+        var n = parseValue(v);\r
+        var a = this.value;\r
+        if (a < 0 !== n.sign) {\r
+            return this.subtract(n.negate());\r
+        }\r
+        var b = n.value;\r
+        if (n.isSmall) {\r
+            if (isPrecise(a + b)) return new SmallInteger(a + b);\r
+            b = smallToArray(Math.abs(b));\r
+        }\r
+        return new BigInteger(addSmall(b, Math.abs(a)), a < 0);\r
+    };\r
+    SmallInteger.prototype.plus = SmallInteger.prototype.add;\r
+\r
+    NativeBigInt.prototype.add = function (v) {\r
+        return new NativeBigInt(this.value + parseValue(v).value);\r
+    }\r
+    NativeBigInt.prototype.plus = NativeBigInt.prototype.add;\r
+\r
+    function subtract(a, b) { // assumes a and b are arrays with a >= b\r
+        var a_l = a.length,\r
+            b_l = b.length,\r
+            r = new Array(a_l),\r
+            borrow = 0,\r
+            base = BASE,\r
+            i, difference;\r
+        for (i = 0; i < b_l; i++) {\r
+            difference = a[i] - borrow - b[i];\r
+            if (difference < 0) {\r
+                difference += base;\r
+                borrow = 1;\r
+            } else borrow = 0;\r
+            r[i] = difference;\r
+        }\r
+        for (i = b_l; i < a_l; i++) {\r
+            difference = a[i] - borrow;\r
+            if (difference < 0) difference += base;\r
+            else {\r
+                r[i++] = difference;\r
+                break;\r
+            }\r
+            r[i] = difference;\r
+        }\r
+        for (; i < a_l; i++) {\r
+            r[i] = a[i];\r
+        }\r
+        trim(r);\r
+        return r;\r
+    }\r
+\r
+    function subtractAny(a, b, sign) {\r
+        var value;\r
+        if (compareAbs(a, b) >= 0) {\r
+            value = subtract(a, b);\r
+        } else {\r
+            value = subtract(b, a);\r
+            sign = !sign;\r
+        }\r
+        value = arrayToSmall(value);\r
+        if (typeof value === "number") {\r
+            if (sign) value = -value;\r
+            return new SmallInteger(value);\r
+        }\r
+        return new BigInteger(value, sign);\r
+    }\r
+\r
+    function subtractSmall(a, b, sign) { // assumes a is array, b is number with 0 <= b < MAX_INT\r
+        var l = a.length,\r
+            r = new Array(l),\r
+            carry = -b,\r
+            base = BASE,\r
+            i, difference;\r
+        for (i = 0; i < l; i++) {\r
+            difference = a[i] + carry;\r
+            carry = Math.floor(difference / base);\r
+            difference %= base;\r
+            r[i] = difference < 0 ? difference + base : difference;\r
+        }\r
+        r = arrayToSmall(r);\r
+        if (typeof r === "number") {\r
+            if (sign) r = -r;\r
+            return new SmallInteger(r);\r
+        } return new BigInteger(r, sign);\r
+    }\r
+\r
+    BigInteger.prototype.subtract = function (v) {\r
+        var n = parseValue(v);\r
+        if (this.sign !== n.sign) {\r
+            return this.add(n.negate());\r
+        }\r
+        var a = this.value, b = n.value;\r
+        if (n.isSmall)\r
+            return subtractSmall(a, Math.abs(b), this.sign);\r
+        return subtractAny(a, b, this.sign);\r
+    };\r
+    BigInteger.prototype.minus = BigInteger.prototype.subtract;\r
+\r
+    SmallInteger.prototype.subtract = function (v) {\r
+        var n = parseValue(v);\r
+        var a = this.value;\r
+        if (a < 0 !== n.sign) {\r
+            return this.add(n.negate());\r
+        }\r
+        var b = n.value;\r
+        if (n.isSmall) {\r
+            return new SmallInteger(a - b);\r
+        }\r
+        return subtractSmall(b, Math.abs(a), a >= 0);\r
+    };\r
+    SmallInteger.prototype.minus = SmallInteger.prototype.subtract;\r
+\r
+    NativeBigInt.prototype.subtract = function (v) {\r
+        return new NativeBigInt(this.value - parseValue(v).value);\r
+    }\r
+    NativeBigInt.prototype.minus = NativeBigInt.prototype.subtract;\r
+\r
+    BigInteger.prototype.negate = function () {\r
+        return new BigInteger(this.value, !this.sign);\r
+    };\r
+    SmallInteger.prototype.negate = function () {\r
+        var sign = this.sign;\r
+        var small = new SmallInteger(-this.value);\r
+        small.sign = !sign;\r
+        return small;\r
+    };\r
+    NativeBigInt.prototype.negate = function () {\r
+        return new NativeBigInt(-this.value);\r
+    }\r
+\r
+    BigInteger.prototype.abs = function () {\r
+        return new BigInteger(this.value, false);\r
+    };\r
+    SmallInteger.prototype.abs = function () {\r
+        return new SmallInteger(Math.abs(this.value));\r
+    };\r
+    NativeBigInt.prototype.abs = function () {\r
+        return new NativeBigInt(this.value >= 0 ? this.value : -this.value);\r
+    }\r
+\r
+\r
+    function multiplyLong(a, b) {\r
+        var a_l = a.length,\r
+            b_l = b.length,\r
+            l = a_l + b_l,\r
+            r = createArray(l),\r
+            base = BASE,\r
+            product, carry, i, a_i, b_j;\r
+        for (i = 0; i < a_l; ++i) {\r
+            a_i = a[i];\r
+            for (var j = 0; j < b_l; ++j) {\r
+                b_j = b[j];\r
+                product = a_i * b_j + r[i + j];\r
+                carry = Math.floor(product / base);\r
+                r[i + j] = product - carry * base;\r
+                r[i + j + 1] += carry;\r
+            }\r
+        }\r
+        trim(r);\r
+        return r;\r
+    }\r
+\r
+    function multiplySmall(a, b) { // assumes a is array, b is number with |b| < BASE\r
+        var l = a.length,\r
+            r = new Array(l),\r
+            base = BASE,\r
+            carry = 0,\r
+            product, i;\r
+        for (i = 0; i < l; i++) {\r
+            product = a[i] * b + carry;\r
+            carry = Math.floor(product / base);\r
+            r[i] = product - carry * base;\r
+        }\r
+        while (carry > 0) {\r
+            r[i++] = carry % base;\r
+            carry = Math.floor(carry / base);\r
+        }\r
+        return r;\r
+    }\r
+\r
+    function shiftLeft(x, n) {\r
+        var r = [];\r
+        while (n-- > 0) r.push(0);\r
+        return r.concat(x);\r
+    }\r
+\r
+    function multiplyKaratsuba(x, y) {\r
+        var n = Math.max(x.length, y.length);\r
+\r
+        if (n <= 30) return multiplyLong(x, y);\r
+        n = Math.ceil(n / 2);\r
+\r
+        var b = x.slice(n),\r
+            a = x.slice(0, n),\r
+            d = y.slice(n),\r
+            c = y.slice(0, n);\r
+\r
+        var ac = multiplyKaratsuba(a, c),\r
+            bd = multiplyKaratsuba(b, d),\r
+            abcd = multiplyKaratsuba(addAny(a, b), addAny(c, d));\r
+\r
+        var product = addAny(addAny(ac, shiftLeft(subtract(subtract(abcd, ac), bd), n)), shiftLeft(bd, 2 * n));\r
+        trim(product);\r
+        return product;\r
+    }\r
+\r
+    // The following function is derived from a surface fit of a graph plotting the performance difference\r
+    // between long multiplication and karatsuba multiplication versus the lengths of the two arrays.\r
+    function useKaratsuba(l1, l2) {\r
+        return -0.012 * l1 - 0.012 * l2 + 0.000015 * l1 * l2 > 0;\r
+    }\r
+\r
+    BigInteger.prototype.multiply = function (v) {\r
+        var n = parseValue(v),\r
+            a = this.value, b = n.value,\r
+            sign = this.sign !== n.sign,\r
+            abs;\r
+        if (n.isSmall) {\r
+            if (b === 0) return Integer[0];\r
+            if (b === 1) return this;\r
+            if (b === -1) return this.negate();\r
+            abs = Math.abs(b);\r
+            if (abs < BASE) {\r
+                return new BigInteger(multiplySmall(a, abs), sign);\r
+            }\r
+            b = smallToArray(abs);\r
+        }\r
+        if (useKaratsuba(a.length, b.length)) // Karatsuba is only faster for certain array sizes\r
+            return new BigInteger(multiplyKaratsuba(a, b), sign);\r
+        return new BigInteger(multiplyLong(a, b), sign);\r
+    };\r
+\r
+    BigInteger.prototype.times = BigInteger.prototype.multiply;\r
+\r
+    function multiplySmallAndArray(a, b, sign) { // a >= 0\r
+        if (a < BASE) {\r
+            return new BigInteger(multiplySmall(b, a), sign);\r
+        }\r
+        return new BigInteger(multiplyLong(b, smallToArray(a)), sign);\r
+    }\r
+    SmallInteger.prototype._multiplyBySmall = function (a) {\r
+        if (isPrecise(a.value * this.value)) {\r
+            return new SmallInteger(a.value * this.value);\r
+        }\r
+        return multiplySmallAndArray(Math.abs(a.value), smallToArray(Math.abs(this.value)), this.sign !== a.sign);\r
+    };\r
+    BigInteger.prototype._multiplyBySmall = function (a) {\r
+        if (a.value === 0) return Integer[0];\r
+        if (a.value === 1) return this;\r
+        if (a.value === -1) return this.negate();\r
+        return multiplySmallAndArray(Math.abs(a.value), this.value, this.sign !== a.sign);\r
+    };\r
+    SmallInteger.prototype.multiply = function (v) {\r
+        return parseValue(v)._multiplyBySmall(this);\r
+    };\r
+    SmallInteger.prototype.times = SmallInteger.prototype.multiply;\r
+\r
+    NativeBigInt.prototype.multiply = function (v) {\r
+        return new NativeBigInt(this.value * parseValue(v).value);\r
+    }\r
+    NativeBigInt.prototype.times = NativeBigInt.prototype.multiply;\r
+\r
+    function square(a) {\r
+        //console.assert(2 * BASE * BASE < MAX_INT);\r
+        var l = a.length,\r
+            r = createArray(l + l),\r
+            base = BASE,\r
+            product, carry, i, a_i, a_j;\r
+        for (i = 0; i < l; i++) {\r
+            a_i = a[i];\r
+            carry = 0 - a_i * a_i;\r
+            for (var j = i; j < l; j++) {\r
+                a_j = a[j];\r
+                product = 2 * (a_i * a_j) + r[i + j] + carry;\r
+                carry = Math.floor(product / base);\r
+                r[i + j] = product - carry * base;\r
+            }\r
+            r[i + l] = carry;\r
+        }\r
+        trim(r);\r
+        return r;\r
+    }\r
+\r
+    BigInteger.prototype.square = function () {\r
+        return new BigInteger(square(this.value), false);\r
+    };\r
+\r
+    SmallInteger.prototype.square = function () {\r
+        var value = this.value * this.value;\r
+        if (isPrecise(value)) return new SmallInteger(value);\r
+        return new BigInteger(square(smallToArray(Math.abs(this.value))), false);\r
+    };\r
+\r
+    NativeBigInt.prototype.square = function (v) {\r
+        return new NativeBigInt(this.value * this.value);\r
+    }\r
+\r
+    function divMod1(a, b) { // Left over from previous version. Performs faster than divMod2 on smaller input sizes.\r
+        var a_l = a.length,\r
+            b_l = b.length,\r
+            base = BASE,\r
+            result = createArray(b.length),\r
+            divisorMostSignificantDigit = b[b_l - 1],\r
+            // normalization\r
+            lambda = Math.ceil(base / (2 * divisorMostSignificantDigit)),\r
+            remainder = multiplySmall(a, lambda),\r
+            divisor = multiplySmall(b, lambda),\r
+            quotientDigit, shift, carry, borrow, i, l, q;\r
+        if (remainder.length <= a_l) remainder.push(0);\r
+        divisor.push(0);\r
+        divisorMostSignificantDigit = divisor[b_l - 1];\r
+        for (shift = a_l - b_l; shift >= 0; shift--) {\r
+            quotientDigit = base - 1;\r
+            if (remainder[shift + b_l] !== divisorMostSignificantDigit) {\r
+                quotientDigit = Math.floor((remainder[shift + b_l] * base + remainder[shift + b_l - 1]) / divisorMostSignificantDigit);\r
+            }\r
+            // quotientDigit <= base - 1\r
+            carry = 0;\r
+            borrow = 0;\r
+            l = divisor.length;\r
+            for (i = 0; i < l; i++) {\r
+                carry += quotientDigit * divisor[i];\r
+                q = Math.floor(carry / base);\r
+                borrow += remainder[shift + i] - (carry - q * base);\r
+                carry = q;\r
+                if (borrow < 0) {\r
+                    remainder[shift + i] = borrow + base;\r
+                    borrow = -1;\r
+                } else {\r
+                    remainder[shift + i] = borrow;\r
+                    borrow = 0;\r
+                }\r
+            }\r
+            while (borrow !== 0) {\r
+                quotientDigit -= 1;\r
+                carry = 0;\r
+                for (i = 0; i < l; i++) {\r
+                    carry += remainder[shift + i] - base + divisor[i];\r
+                    if (carry < 0) {\r
+                        remainder[shift + i] = carry + base;\r
+                        carry = 0;\r
+                    } else {\r
+                        remainder[shift + i] = carry;\r
+                        carry = 1;\r
+                    }\r
+                }\r
+                borrow += carry;\r
+            }\r
+            result[shift] = quotientDigit;\r
+        }\r
+        // denormalization\r
+        remainder = divModSmall(remainder, lambda)[0];\r
+        return [arrayToSmall(result), arrayToSmall(remainder)];\r
+    }\r
+\r
+    function divMod2(a, b) { // Implementation idea shamelessly stolen from Silent Matt's library http://silentmatt.com/biginteger/\r
+        // Performs faster than divMod1 on larger input sizes.\r
+        var a_l = a.length,\r
+            b_l = b.length,\r
+            result = [],\r
+            part = [],\r
+            base = BASE,\r
+            guess, xlen, highx, highy, check;\r
+        while (a_l) {\r
+            part.unshift(a[--a_l]);\r
+            trim(part);\r
+            if (compareAbs(part, b) < 0) {\r
+                result.push(0);\r
+                continue;\r
+            }\r
+            xlen = part.length;\r
+            highx = part[xlen - 1] * base + part[xlen - 2];\r
+            highy = b[b_l - 1] * base + b[b_l - 2];\r
+            if (xlen > b_l) {\r
+                highx = (highx + 1) * base;\r
+            }\r
+            guess = Math.ceil(highx / highy);\r
+            do {\r
+                check = multiplySmall(b, guess);\r
+                if (compareAbs(check, part) <= 0) break;\r
+                guess--;\r
+            } while (guess);\r
+            result.push(guess);\r
+            part = subtract(part, check);\r
+        }\r
+        result.reverse();\r
+        return [arrayToSmall(result), arrayToSmall(part)];\r
+    }\r
+\r
+    function divModSmall(value, lambda) {\r
+        var length = value.length,\r
+            quotient = createArray(length),\r
+            base = BASE,\r
+            i, q, remainder, divisor;\r
+        remainder = 0;\r
+        for (i = length - 1; i >= 0; --i) {\r
+            divisor = remainder * base + value[i];\r
+            q = truncate(divisor / lambda);\r
+            remainder = divisor - q * lambda;\r
+            quotient[i] = q | 0;\r
+        }\r
+        return [quotient, remainder | 0];\r
+    }\r
+\r
+    function divModAny(self, v) {\r
+        var value, n = parseValue(v);\r
+        if (supportsNativeBigInt) {\r
+            return [new NativeBigInt(self.value / n.value), new NativeBigInt(self.value % n.value)];\r
+        }\r
+        var a = self.value, b = n.value;\r
+        var quotient;\r
+        if (b === 0) throw new Error("Cannot divide by zero");\r
+        if (self.isSmall) {\r
+            if (n.isSmall) {\r
+                return [new SmallInteger(truncate(a / b)), new SmallInteger(a % b)];\r
+            }\r
+            return [Integer[0], self];\r
+        }\r
+        if (n.isSmall) {\r
+            if (b === 1) return [self, Integer[0]];\r
+            if (b == -1) return [self.negate(), Integer[0]];\r
+            var abs = Math.abs(b);\r
+            if (abs < BASE) {\r
+                value = divModSmall(a, abs);\r
+                quotient = arrayToSmall(value[0]);\r
+                var remainder = value[1];\r
+                if (self.sign) remainder = -remainder;\r
+                if (typeof quotient === "number") {\r
+                    if (self.sign !== n.sign) quotient = -quotient;\r
+                    return [new SmallInteger(quotient), new SmallInteger(remainder)];\r
+                }\r
+                return [new BigInteger(quotient, self.sign !== n.sign), new SmallInteger(remainder)];\r
+            }\r
+            b = smallToArray(abs);\r
+        }\r
+        var comparison = compareAbs(a, b);\r
+        if (comparison === -1) return [Integer[0], self];\r
+        if (comparison === 0) return [Integer[self.sign === n.sign ? 1 : -1], Integer[0]];\r
+\r
+        // divMod1 is faster on smaller input sizes\r
+        if (a.length + b.length <= 200)\r
+            value = divMod1(a, b);\r
+        else value = divMod2(a, b);\r
+\r
+        quotient = value[0];\r
+        var qSign = self.sign !== n.sign,\r
+            mod = value[1],\r
+            mSign = self.sign;\r
+        if (typeof quotient === "number") {\r
+            if (qSign) quotient = -quotient;\r
+            quotient = new SmallInteger(quotient);\r
+        } else quotient = new BigInteger(quotient, qSign);\r
+        if (typeof mod === "number") {\r
+            if (mSign) mod = -mod;\r
+            mod = new SmallInteger(mod);\r
+        } else mod = new BigInteger(mod, mSign);\r
+        return [quotient, mod];\r
+    }\r
+\r
+    BigInteger.prototype.divmod = function (v) {\r
+        var result = divModAny(this, v);\r
+        return {\r
+            quotient: result[0],\r
+            remainder: result[1]\r
+        };\r
+    };\r
+    NativeBigInt.prototype.divmod = SmallInteger.prototype.divmod = BigInteger.prototype.divmod;\r
+\r
+\r
+    BigInteger.prototype.divide = function (v) {\r
+        return divModAny(this, v)[0];\r
+    };\r
+    NativeBigInt.prototype.over = NativeBigInt.prototype.divide = function (v) {\r
+        return new NativeBigInt(this.value / parseValue(v).value);\r
+    };\r
+    SmallInteger.prototype.over = SmallInteger.prototype.divide = BigInteger.prototype.over = BigInteger.prototype.divide;\r
+\r
+    BigInteger.prototype.mod = function (v) {\r
+        return divModAny(this, v)[1];\r
+    };\r
+    NativeBigInt.prototype.mod = NativeBigInt.prototype.remainder = function (v) {\r
+        return new NativeBigInt(this.value % parseValue(v).value);\r
+    };\r
+    SmallInteger.prototype.remainder = SmallInteger.prototype.mod = BigInteger.prototype.remainder = BigInteger.prototype.mod;\r
+\r
+    BigInteger.prototype.pow = function (v) {\r
+        var n = parseValue(v),\r
+            a = this.value,\r
+            b = n.value,\r
+            value, x, y;\r
+        if (b === 0) return Integer[1];\r
+        if (a === 0) return Integer[0];\r
+        if (a === 1) return Integer[1];\r
+        if (a === -1) return n.isEven() ? Integer[1] : Integer[-1];\r
+        if (n.sign) {\r
+            return Integer[0];\r
+        }\r
+        if (!n.isSmall) throw new Error("The exponent " + n.toString() + " is too large.");\r
+        if (this.isSmall) {\r
+            if (isPrecise(value = Math.pow(a, b)))\r
+                return new SmallInteger(truncate(value));\r
+        }\r
+        x = this;\r
+        y = Integer[1];\r
+        while (true) {\r
+            if (b & 1 === 1) {\r
+                y = y.times(x);\r
+                --b;\r
+            }\r
+            if (b === 0) break;\r
+            b /= 2;\r
+            x = x.square();\r
+        }\r
+        return y;\r
+    };\r
+    SmallInteger.prototype.pow = BigInteger.prototype.pow;\r
+\r
+    NativeBigInt.prototype.pow = function (v) {\r
+        var n = parseValue(v);\r
+        var a = this.value, b = n.value;\r
+        var _0 = BigInt(0), _1 = BigInt(1), _2 = BigInt(2);\r
+        if (b === _0) return Integer[1];\r
+        if (a === _0) return Integer[0];\r
+        if (a === _1) return Integer[1];\r
+        if (a === BigInt(-1)) return n.isEven() ? Integer[1] : Integer[-1];\r
+        if (n.isNegative()) return new NativeBigInt(_0);\r
+        var x = this;\r
+        var y = Integer[1];\r
+        while (true) {\r
+            if ((b & _1) === _1) {\r
+                y = y.times(x);\r
+                --b;\r
+            }\r
+            if (b === _0) break;\r
+            b /= _2;\r
+            x = x.square();\r
+        }\r
+        return y;\r
+    }\r
+\r
+    BigInteger.prototype.modPow = function (exp, mod) {\r
+        exp = parseValue(exp);\r
+        mod = parseValue(mod);\r
+        if (mod.isZero()) throw new Error("Cannot take modPow with modulus 0");\r
+        var r = Integer[1],\r
+            base = this.mod(mod);\r
+        if (exp.isNegative()) {\r
+            exp = exp.multiply(Integer[-1]);\r
+            base = base.modInv(mod);\r
+        }\r
+        while (exp.isPositive()) {\r
+            if (base.isZero()) return Integer[0];\r
+            if (exp.isOdd()) r = r.multiply(base).mod(mod);\r
+            exp = exp.divide(2);\r
+            base = base.square().mod(mod);\r
+        }\r
+        return r;\r
+    };\r
+    NativeBigInt.prototype.modPow = SmallInteger.prototype.modPow = BigInteger.prototype.modPow;\r
+\r
+    function compareAbs(a, b) {\r
+        if (a.length !== b.length) {\r
+            return a.length > b.length ? 1 : -1;\r
+        }\r
+        for (var i = a.length - 1; i >= 0; i--) {\r
+            if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;\r
+        }\r
+        return 0;\r
+    }\r
+\r
+    BigInteger.prototype.compareAbs = function (v) {\r
+        var n = parseValue(v),\r
+            a = this.value,\r
+            b = n.value;\r
+        if (n.isSmall) return 1;\r
+        return compareAbs(a, b);\r
+    };\r
+    SmallInteger.prototype.compareAbs = function (v) {\r
+        var n = parseValue(v),\r
+            a = Math.abs(this.value),\r
+            b = n.value;\r
+        if (n.isSmall) {\r
+            b = Math.abs(b);\r
+            return a === b ? 0 : a > b ? 1 : -1;\r
+        }\r
+        return -1;\r
+    };\r
+    NativeBigInt.prototype.compareAbs = function (v) {\r
+        var a = this.value;\r
+        var b = parseValue(v).value;\r
+        a = a >= 0 ? a : -a;\r
+        b = b >= 0 ? b : -b;\r
+        return a === b ? 0 : a > b ? 1 : -1;\r
+    }\r
+\r
+    BigInteger.prototype.compare = function (v) {\r
+        // See discussion about comparison with Infinity:\r
+        // https://github.com/peterolson/BigInteger.js/issues/61\r
+        if (v === Infinity) {\r
+            return -1;\r
+        }\r
+        if (v === -Infinity) {\r
+            return 1;\r
+        }\r
+\r
+        var n = parseValue(v),\r
+            a = this.value,\r
+            b = n.value;\r
+        if (this.sign !== n.sign) {\r
+            return n.sign ? 1 : -1;\r
+        }\r
+        if (n.isSmall) {\r
+            return this.sign ? -1 : 1;\r
+        }\r
+        return compareAbs(a, b) * (this.sign ? -1 : 1);\r
+    };\r
+    BigInteger.prototype.compareTo = BigInteger.prototype.compare;\r
+\r
+    SmallInteger.prototype.compare = function (v) {\r
+        if (v === Infinity) {\r
+            return -1;\r
+        }\r
+        if (v === -Infinity) {\r
+            return 1;\r
+        }\r
+\r
+        var n = parseValue(v),\r
+            a = this.value,\r
+            b = n.value;\r
+        if (n.isSmall) {\r
+            return a == b ? 0 : a > b ? 1 : -1;\r
+        }\r
+        if (a < 0 !== n.sign) {\r
+            return a < 0 ? -1 : 1;\r
+        }\r
+        return a < 0 ? 1 : -1;\r
+    };\r
+    SmallInteger.prototype.compareTo = SmallInteger.prototype.compare;\r
+\r
+    NativeBigInt.prototype.compare = function (v) {\r
+        if (v === Infinity) {\r
+            return -1;\r
+        }\r
+        if (v === -Infinity) {\r
+            return 1;\r
+        }\r
+        var a = this.value;\r
+        var b = parseValue(v).value;\r
+        return a === b ? 0 : a > b ? 1 : -1;\r
+    }\r
+    NativeBigInt.prototype.compareTo = NativeBigInt.prototype.compare;\r
+\r
+    BigInteger.prototype.equals = function (v) {\r
+        return this.compare(v) === 0;\r
+    };\r
+    NativeBigInt.prototype.eq = NativeBigInt.prototype.equals = SmallInteger.prototype.eq = SmallInteger.prototype.equals = BigInteger.prototype.eq = BigInteger.prototype.equals;\r
+\r
+    BigInteger.prototype.notEquals = function (v) {\r
+        return this.compare(v) !== 0;\r
+    };\r
+    NativeBigInt.prototype.neq = NativeBigInt.prototype.notEquals = SmallInteger.prototype.neq = SmallInteger.prototype.notEquals = BigInteger.prototype.neq = BigInteger.prototype.notEquals;\r
+\r
+    BigInteger.prototype.greater = function (v) {\r
+        return this.compare(v) > 0;\r
+    };\r
+    NativeBigInt.prototype.gt = NativeBigInt.prototype.greater = SmallInteger.prototype.gt = SmallInteger.prototype.greater = BigInteger.prototype.gt = BigInteger.prototype.greater;\r
+\r
+    BigInteger.prototype.lesser = function (v) {\r
+        return this.compare(v) < 0;\r
+    };\r
+    NativeBigInt.prototype.lt = NativeBigInt.prototype.lesser = SmallInteger.prototype.lt = SmallInteger.prototype.lesser = BigInteger.prototype.lt = BigInteger.prototype.lesser;\r
+\r
+    BigInteger.prototype.greaterOrEquals = function (v) {\r
+        return this.compare(v) >= 0;\r
+    };\r
+    NativeBigInt.prototype.geq = NativeBigInt.prototype.greaterOrEquals = SmallInteger.prototype.geq = SmallInteger.prototype.greaterOrEquals = BigInteger.prototype.geq = BigInteger.prototype.greaterOrEquals;\r
+\r
+    BigInteger.prototype.lesserOrEquals = function (v) {\r
+        return this.compare(v) <= 0;\r
+    };\r
+    NativeBigInt.prototype.leq = NativeBigInt.prototype.lesserOrEquals = SmallInteger.prototype.leq = SmallInteger.prototype.lesserOrEquals = BigInteger.prototype.leq = BigInteger.prototype.lesserOrEquals;\r
+\r
+    BigInteger.prototype.isEven = function () {\r
+        return (this.value[0] & 1) === 0;\r
+    };\r
+    SmallInteger.prototype.isEven = function () {\r
+        return (this.value & 1) === 0;\r
+    };\r
+    NativeBigInt.prototype.isEven = function () {\r
+        return (this.value & BigInt(1)) === BigInt(0);\r
+    }\r
+\r
+    BigInteger.prototype.isOdd = function () {\r
+        return (this.value[0] & 1) === 1;\r
+    };\r
+    SmallInteger.prototype.isOdd = function () {\r
+        return (this.value & 1) === 1;\r
+    };\r
+    NativeBigInt.prototype.isOdd = function () {\r
+        return (this.value & BigInt(1)) === BigInt(1);\r
+    }\r
+\r
+    BigInteger.prototype.isPositive = function () {\r
+        return !this.sign;\r
+    };\r
+    SmallInteger.prototype.isPositive = function () {\r
+        return this.value > 0;\r
+    };\r
+    NativeBigInt.prototype.isPositive = SmallInteger.prototype.isPositive;\r
+\r
+    BigInteger.prototype.isNegative = function () {\r
+        return this.sign;\r
+    };\r
+    SmallInteger.prototype.isNegative = function () {\r
+        return this.value < 0;\r
+    };\r
+    NativeBigInt.prototype.isNegative = SmallInteger.prototype.isNegative;\r
+\r
+    BigInteger.prototype.isUnit = function () {\r
+        return false;\r
+    };\r
+    SmallInteger.prototype.isUnit = function () {\r
+        return Math.abs(this.value) === 1;\r
+    };\r
+    NativeBigInt.prototype.isUnit = function () {\r
+        return this.abs().value === BigInt(1);\r
+    }\r
+\r
+    BigInteger.prototype.isZero = function () {\r
+        return false;\r
+    };\r
+    SmallInteger.prototype.isZero = function () {\r
+        return this.value === 0;\r
+    };\r
+    NativeBigInt.prototype.isZero = function () {\r
+        return this.value === BigInt(0);\r
+    }\r
+\r
+    BigInteger.prototype.isDivisibleBy = function (v) {\r
+        var n = parseValue(v);\r
+        if (n.isZero()) return false;\r
+        if (n.isUnit()) return true;\r
+        if (n.compareAbs(2) === 0) return this.isEven();\r
+        return this.mod(n).isZero();\r
+    };\r
+    NativeBigInt.prototype.isDivisibleBy = SmallInteger.prototype.isDivisibleBy = BigInteger.prototype.isDivisibleBy;\r
+\r
+    function isBasicPrime(v) {\r
+        var n = v.abs();\r
+        if (n.isUnit()) return false;\r
+        if (n.equals(2) || n.equals(3) || n.equals(5)) return true;\r
+        if (n.isEven() || n.isDivisibleBy(3) || n.isDivisibleBy(5)) return false;\r
+        if (n.lesser(49)) return true;\r
+        // we don't know if it's prime: let the other functions figure it out\r
+    }\r
+\r
+    function millerRabinTest(n, a) {\r
+        var nPrev = n.prev(),\r
+            b = nPrev,\r
+            r = 0,\r
+            d, t, i, x;\r
+        while (b.isEven()) b = b.divide(2), r++;\r
+        next: for (i = 0; i < a.length; i++) {\r
+            if (n.lesser(a[i])) continue;\r
+            x = bigInt(a[i]).modPow(b, n);\r
+            if (x.isUnit() || x.equals(nPrev)) continue;\r
+            for (d = r - 1; d != 0; d--) {\r
+                x = x.square().mod(n);\r
+                if (x.isUnit()) return false;\r
+                if (x.equals(nPrev)) continue next;\r
+            }\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
+\r
+    // Set "strict" to true to force GRH-supported lower bound of 2*log(N)^2\r
+    BigInteger.prototype.isPrime = function (strict) {\r
+        var isPrime = isBasicPrime(this);\r
+        if (isPrime !== undefined) return isPrime;\r
+        var n = this.abs();\r
+        var bits = n.bitLength();\r
+        if (bits <= 64)\r
+            return millerRabinTest(n, [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]);\r
+        var logN = Math.log(2) * bits.toJSNumber();\r
+        var t = Math.ceil((strict === true) ? (2 * Math.pow(logN, 2)) : logN);\r
+        for (var a = [], i = 0; i < t; i++) {\r
+            a.push(bigInt(i + 2));\r
+        }\r
+        return millerRabinTest(n, a);\r
+    };\r
+    NativeBigInt.prototype.isPrime = SmallInteger.prototype.isPrime = BigInteger.prototype.isPrime;\r
+\r
+    BigInteger.prototype.isProbablePrime = function (iterations, rng) {\r
+        var isPrime = isBasicPrime(this);\r
+        if (isPrime !== undefined) return isPrime;\r
+        var n = this.abs();\r
+        var t = iterations === undefined ? 5 : iterations;\r
+        for (var a = [], i = 0; i < t; i++) {\r
+            a.push(bigInt.randBetween(2, n.minus(2), rng));\r
+        }\r
+        return millerRabinTest(n, a);\r
+    };\r
+    NativeBigInt.prototype.isProbablePrime = SmallInteger.prototype.isProbablePrime = BigInteger.prototype.isProbablePrime;\r
+\r
+    BigInteger.prototype.modInv = function (n) {\r
+        var t = bigInt.zero, newT = bigInt.one, r = parseValue(n), newR = this.abs(), q, lastT, lastR;\r
+        while (!newR.isZero()) {\r
+            q = r.divide(newR);\r
+            lastT = t;\r
+            lastR = r;\r
+            t = newT;\r
+            r = newR;\r
+            newT = lastT.subtract(q.multiply(newT));\r
+            newR = lastR.subtract(q.multiply(newR));\r
+        }\r
+        if (!r.isUnit()) throw new Error(this.toString() + " and " + n.toString() + " are not co-prime");\r
+        if (t.compare(0) === -1) {\r
+            t = t.add(n);\r
+        }\r
+        if (this.isNegative()) {\r
+            return t.negate();\r
+        }\r
+        return t;\r
+    };\r
+\r
+    NativeBigInt.prototype.modInv = SmallInteger.prototype.modInv = BigInteger.prototype.modInv;\r
+\r
+    BigInteger.prototype.next = function () {\r
+        var value = this.value;\r
+        if (this.sign) {\r
+            return subtractSmall(value, 1, this.sign);\r
+        }\r
+        return new BigInteger(addSmall(value, 1), this.sign);\r
+    };\r
+    SmallInteger.prototype.next = function () {\r
+        var value = this.value;\r
+        if (value + 1 < MAX_INT) return new SmallInteger(value + 1);\r
+        return new BigInteger(MAX_INT_ARR, false);\r
+    };\r
+    NativeBigInt.prototype.next = function () {\r
+        return new NativeBigInt(this.value + BigInt(1));\r
+    }\r
+\r
+    BigInteger.prototype.prev = function () {\r
+        var value = this.value;\r
+        if (this.sign) {\r
+            return new BigInteger(addSmall(value, 1), true);\r
+        }\r
+        return subtractSmall(value, 1, this.sign);\r
+    };\r
+    SmallInteger.prototype.prev = function () {\r
+        var value = this.value;\r
+        if (value - 1 > -MAX_INT) return new SmallInteger(value - 1);\r
+        return new BigInteger(MAX_INT_ARR, true);\r
+    };\r
+    NativeBigInt.prototype.prev = function () {\r
+        return new NativeBigInt(this.value - BigInt(1));\r
+    }\r
+\r
+    var powersOfTwo = [1];\r
+    while (2 * powersOfTwo[powersOfTwo.length - 1] <= BASE) powersOfTwo.push(2 * powersOfTwo[powersOfTwo.length - 1]);\r
+    var powers2Length = powersOfTwo.length, highestPower2 = powersOfTwo[powers2Length - 1];\r
+\r
+    function shift_isSmall(n) {\r
+        return Math.abs(n) <= BASE;\r
+    }\r
+\r
+    BigInteger.prototype.shiftLeft = function (v) {\r
+        var n = parseValue(v).toJSNumber();\r
+        if (!shift_isSmall(n)) {\r
+            throw new Error(String(n) + " is too large for shifting.");\r
+        }\r
+        if (n < 0) return this.shiftRight(-n);\r
+        var result = this;\r
+        if (result.isZero()) return result;\r
+        while (n >= powers2Length) {\r
+            result = result.multiply(highestPower2);\r
+            n -= powers2Length - 1;\r
+        }\r
+        return result.multiply(powersOfTwo[n]);\r
+    };\r
+    NativeBigInt.prototype.shiftLeft = SmallInteger.prototype.shiftLeft = BigInteger.prototype.shiftLeft;\r
+\r
+    BigInteger.prototype.shiftRight = function (v) {\r
+        var remQuo;\r
+        var n = parseValue(v).toJSNumber();\r
+        if (!shift_isSmall(n)) {\r
+            throw new Error(String(n) + " is too large for shifting.");\r
+        }\r
+        if (n < 0) return this.shiftLeft(-n);\r
+        var result = this;\r
+        while (n >= powers2Length) {\r
+            if (result.isZero() || (result.isNegative() && result.isUnit())) return result;\r
+            remQuo = divModAny(result, highestPower2);\r
+            result = remQuo[1].isNegative() ? remQuo[0].prev() : remQuo[0];\r
+            n -= powers2Length - 1;\r
+        }\r
+        remQuo = divModAny(result, powersOfTwo[n]);\r
+        return remQuo[1].isNegative() ? remQuo[0].prev() : remQuo[0];\r
+    };\r
+    NativeBigInt.prototype.shiftRight = SmallInteger.prototype.shiftRight = BigInteger.prototype.shiftRight;\r
+\r
+    function bitwise(x, y, fn) {\r
+        y = parseValue(y);\r
+        var xSign = x.isNegative(), ySign = y.isNegative();\r
+        var xRem = xSign ? x.not() : x,\r
+            yRem = ySign ? y.not() : y;\r
+        var xDigit = 0, yDigit = 0;\r
+        var xDivMod = null, yDivMod = null;\r
+        var result = [];\r
+        while (!xRem.isZero() || !yRem.isZero()) {\r
+            xDivMod = divModAny(xRem, highestPower2);\r
+            xDigit = xDivMod[1].toJSNumber();\r
+            if (xSign) {\r
+                xDigit = highestPower2 - 1 - xDigit; // two's complement for negative numbers\r
+            }\r
+\r
+            yDivMod = divModAny(yRem, highestPower2);\r
+            yDigit = yDivMod[1].toJSNumber();\r
+            if (ySign) {\r
+                yDigit = highestPower2 - 1 - yDigit; // two's complement for negative numbers\r
+            }\r
+\r
+            xRem = xDivMod[0];\r
+            yRem = yDivMod[0];\r
+            result.push(fn(xDigit, yDigit));\r
+        }\r
+        var sum = fn(xSign ? 1 : 0, ySign ? 1 : 0) !== 0 ? bigInt(-1) : bigInt(0);\r
+        for (var i = result.length - 1; i >= 0; i -= 1) {\r
+            sum = sum.multiply(highestPower2).add(bigInt(result[i]));\r
+        }\r
+        return sum;\r
+    }\r
+\r
+    BigInteger.prototype.not = function () {\r
+        return this.negate().prev();\r
+    };\r
+    NativeBigInt.prototype.not = SmallInteger.prototype.not = BigInteger.prototype.not;\r
+\r
+    BigInteger.prototype.and = function (n) {\r
+        return bitwise(this, n, function (a, b) { return a & b; });\r
+    };\r
+    NativeBigInt.prototype.and = SmallInteger.prototype.and = BigInteger.prototype.and;\r
+\r
+    BigInteger.prototype.or = function (n) {\r
+        return bitwise(this, n, function (a, b) { return a | b; });\r
+    };\r
+    NativeBigInt.prototype.or = SmallInteger.prototype.or = BigInteger.prototype.or;\r
+\r
+    BigInteger.prototype.xor = function (n) {\r
+        return bitwise(this, n, function (a, b) { return a ^ b; });\r
+    };\r
+    NativeBigInt.prototype.xor = SmallInteger.prototype.xor = BigInteger.prototype.xor;\r
+\r
+    var LOBMASK_I = 1 << 30, LOBMASK_BI = (BASE & -BASE) * (BASE & -BASE) | LOBMASK_I;\r
+    function roughLOB(n) { // get lowestOneBit (rough)\r
+        // SmallInteger: return Min(lowestOneBit(n), 1 << 30)\r
+        // BigInteger: return Min(lowestOneBit(n), 1 << 14) [BASE=1e7]\r
+        var v = n.value,\r
+            x = typeof v === "number" ? v | LOBMASK_I :\r
+                typeof v === "bigint" ? v | BigInt(LOBMASK_I) :\r
+                    v[0] + v[1] * BASE | LOBMASK_BI;\r
+        return x & -x;\r
+    }\r
+\r
+    function integerLogarithm(value, base) {\r
+        if (base.compareTo(value) <= 0) {\r
+            var tmp = integerLogarithm(value, base.square(base));\r
+            var p = tmp.p;\r
+            var e = tmp.e;\r
+            var t = p.multiply(base);\r
+            return t.compareTo(value) <= 0 ? { p: t, e: e * 2 + 1 } : { p: p, e: e * 2 };\r
+        }\r
+        return { p: bigInt(1), e: 0 };\r
+    }\r
+\r
+    BigInteger.prototype.bitLength = function () {\r
+        var n = this;\r
+        if (n.compareTo(bigInt(0)) < 0) {\r
+            n = n.negate().subtract(bigInt(1));\r
+        }\r
+        if (n.compareTo(bigInt(0)) === 0) {\r
+            return bigInt(0);\r
+        }\r
+        return bigInt(integerLogarithm(n, bigInt(2)).e).add(bigInt(1));\r
+    }\r
+    NativeBigInt.prototype.bitLength = SmallInteger.prototype.bitLength = BigInteger.prototype.bitLength;\r
+\r
+    function max(a, b) {\r
+        a = parseValue(a);\r
+        b = parseValue(b);\r
+        return a.greater(b) ? a : b;\r
+    }\r
+    function min(a, b) {\r
+        a = parseValue(a);\r
+        b = parseValue(b);\r
+        return a.lesser(b) ? a : b;\r
+    }\r
+    function gcd(a, b) {\r
+        a = parseValue(a).abs();\r
+        b = parseValue(b).abs();\r
+        if (a.equals(b)) return a;\r
+        if (a.isZero()) return b;\r
+        if (b.isZero()) return a;\r
+        var c = Integer[1], d, t;\r
+        while (a.isEven() && b.isEven()) {\r
+            d = min(roughLOB(a), roughLOB(b));\r
+            a = a.divide(d);\r
+            b = b.divide(d);\r
+            c = c.multiply(d);\r
+        }\r
+        while (a.isEven()) {\r
+            a = a.divide(roughLOB(a));\r
+        }\r
+        do {\r
+            while (b.isEven()) {\r
+                b = b.divide(roughLOB(b));\r
+            }\r
+            if (a.greater(b)) {\r
+                t = b; b = a; a = t;\r
+            }\r
+            b = b.subtract(a);\r
+        } while (!b.isZero());\r
+        return c.isUnit() ? a : a.multiply(c);\r
+    }\r
+    function lcm(a, b) {\r
+        a = parseValue(a).abs();\r
+        b = parseValue(b).abs();\r
+        return a.divide(gcd(a, b)).multiply(b);\r
+    }\r
+    function randBetween(a, b, rng) {\r
+        a = parseValue(a);\r
+        b = parseValue(b);\r
+        var usedRNG = rng || Math.random;\r
+        var low = min(a, b), high = max(a, b);\r
+        var range = high.subtract(low).add(1);\r
+        if (range.isSmall) return low.add(Math.floor(usedRNG() * range));\r
+        var digits = toBase(range, BASE).value;\r
+        var result = [], restricted = true;\r
+        for (var i = 0; i < digits.length; i++) {\r
+            var top = restricted ? digits[i] + (i + 1 < digits.length ? digits[i + 1] / BASE : 0) : BASE;\r
+            var digit = truncate(usedRNG() * top);\r
+            result.push(digit);\r
+            if (digit < digits[i]) restricted = false;\r
+        }\r
+        return low.add(Integer.fromArray(result, BASE, false));\r
+    }\r
+\r
+    var parseBase = function (text, base, alphabet, caseSensitive) {\r
+        alphabet = alphabet || DEFAULT_ALPHABET;\r
+        text = String(text);\r
+        if (!caseSensitive) {\r
+            text = text.toLowerCase();\r
+            alphabet = alphabet.toLowerCase();\r
+        }\r
+        var length = text.length;\r
+        var i;\r
+        var absBase = Math.abs(base);\r
+        var alphabetValues = {};\r
+        for (i = 0; i < alphabet.length; i++) {\r
+            alphabetValues[alphabet[i]] = i;\r
+        }\r
+        for (i = 0; i < length; i++) {\r
+            var c = text[i];\r
+            if (c === "-") continue;\r
+            if (c in alphabetValues) {\r
+                if (alphabetValues[c] >= absBase) {\r
+                    if (c === "1" && absBase === 1) continue;\r
+                    throw new Error(c + " is not a valid digit in base " + base + ".");\r
+                }\r
+            }\r
+        }\r
+        base = parseValue(base);\r
+        var digits = [];\r
+        var isNegative = text[0] === "-";\r
+        for (i = isNegative ? 1 : 0; i < text.length; i++) {\r
+            var c = text[i];\r
+            if (c in alphabetValues) digits.push(parseValue(alphabetValues[c]));\r
+            else if (c === "<") {\r
+                var start = i;\r
+                do { i++; } while (text[i] !== ">" && i < text.length);\r
+                digits.push(parseValue(text.slice(start + 1, i)));\r
+            }\r
+            else throw new Error(c + " is not a valid character");\r
+        }\r
+        return parseBaseFromArray(digits, base, isNegative);\r
+    };\r
+\r
+    function parseBaseFromArray(digits, base, isNegative) {\r
+        var val = Integer[0], pow = Integer[1], i;\r
+        for (i = digits.length - 1; i >= 0; i--) {\r
+            val = val.add(digits[i].times(pow));\r
+            pow = pow.times(base);\r
+        }\r
+        return isNegative ? val.negate() : val;\r
+    }\r
+\r
+    function stringify(digit, alphabet) {\r
+        alphabet = alphabet || DEFAULT_ALPHABET;\r
+        if (digit < alphabet.length) {\r
+            return alphabet[digit];\r
+        }\r
+        return "<" + digit + ">";\r
+    }\r
+\r
+    function toBase(n, base) {\r
+        base = bigInt(base);\r
+        if (base.isZero()) {\r
+            if (n.isZero()) return { value: [0], isNegative: false };\r
+            throw new Error("Cannot convert nonzero numbers to base 0.");\r
+        }\r
+        if (base.equals(-1)) {\r
+            if (n.isZero()) return { value: [0], isNegative: false };\r
+            if (n.isNegative())\r
+                return {\r
+                    value: [].concat.apply([], Array.apply(null, Array(-n.toJSNumber()))\r
+                        .map(Array.prototype.valueOf, [1, 0])\r
+                    ),\r
+                    isNegative: false\r
+                };\r
+\r
+            var arr = Array.apply(null, Array(n.toJSNumber() - 1))\r
+                .map(Array.prototype.valueOf, [0, 1]);\r
+            arr.unshift([1]);\r
+            return {\r
+                value: [].concat.apply([], arr),\r
+                isNegative: false\r
+            };\r
+        }\r
+\r
+        var neg = false;\r
+        if (n.isNegative() && base.isPositive()) {\r
+            neg = true;\r
+            n = n.abs();\r
+        }\r
+        if (base.isUnit()) {\r
+            if (n.isZero()) return { value: [0], isNegative: false };\r
+\r
+            return {\r
+                value: Array.apply(null, Array(n.toJSNumber()))\r
+                    .map(Number.prototype.valueOf, 1),\r
+                isNegative: neg\r
+            };\r
+        }\r
+        var out = [];\r
+        var left = n, divmod;\r
+        while (left.isNegative() || left.compareAbs(base) >= 0) {\r
+            divmod = left.divmod(base);\r
+            left = divmod.quotient;\r
+            var digit = divmod.remainder;\r
+            if (digit.isNegative()) {\r
+                digit = base.minus(digit).abs();\r
+                left = left.next();\r
+            }\r
+            out.push(digit.toJSNumber());\r
+        }\r
+        out.push(left.toJSNumber());\r
+        return { value: out.reverse(), isNegative: neg };\r
+    }\r
+\r
+    function toBaseString(n, base, alphabet) {\r
+        var arr = toBase(n, base);\r
+        return (arr.isNegative ? "-" : "") + arr.value.map(function (x) {\r
+            return stringify(x, alphabet);\r
+        }).join('');\r
+    }\r
+\r
+    BigInteger.prototype.toArray = function (radix) {\r
+        return toBase(this, radix);\r
+    };\r
+\r
+    SmallInteger.prototype.toArray = function (radix) {\r
+        return toBase(this, radix);\r
+    };\r
+\r
+    NativeBigInt.prototype.toArray = function (radix) {\r
+        return toBase(this, radix);\r
+    };\r
+\r
+    BigInteger.prototype.toString = function (radix, alphabet) {\r
+        if (radix === undefined) radix = 10;\r
+        if (radix !== 10 || alphabet) return toBaseString(this, radix, alphabet);\r
+        var v = this.value, l = v.length, str = String(v[--l]), zeros = "0000000", digit;\r
+        while (--l >= 0) {\r
+            digit = String(v[l]);\r
+            str += zeros.slice(digit.length) + digit;\r
+        }\r
+        var sign = this.sign ? "-" : "";\r
+        return sign + str;\r
+    };\r
+\r
+    SmallInteger.prototype.toString = function (radix, alphabet) {\r
+        if (radix === undefined) radix = 10;\r
+        if (radix != 10 || alphabet) return toBaseString(this, radix, alphabet);\r
+        return String(this.value);\r
+    };\r
+\r
+    NativeBigInt.prototype.toString = SmallInteger.prototype.toString;\r
+\r
+    NativeBigInt.prototype.toJSON = BigInteger.prototype.toJSON = SmallInteger.prototype.toJSON = function () { return this.toString(); }\r
+\r
+    BigInteger.prototype.valueOf = function () {\r
+        return parseInt(this.toString(), 10);\r
+    };\r
+    BigInteger.prototype.toJSNumber = BigInteger.prototype.valueOf;\r
+\r
+    SmallInteger.prototype.valueOf = function () {\r
+        return this.value;\r
+    };\r
+    SmallInteger.prototype.toJSNumber = SmallInteger.prototype.valueOf;\r
+    NativeBigInt.prototype.valueOf = NativeBigInt.prototype.toJSNumber = function () {\r
+        return parseInt(this.toString(), 10);\r
+    }\r
+\r
+    function parseStringValue(v) {\r
+        if (isPrecise(+v)) {\r
+            var x = +v;\r
+            if (x === truncate(x))\r
+                return supportsNativeBigInt ? new NativeBigInt(BigInt(x)) : new SmallInteger(x);\r
+            throw new Error("Invalid integer: " + v);\r
+        }\r
+        var sign = v[0] === "-";\r
+        if (sign) v = v.slice(1);\r
+        var split = v.split(/e/i);\r
+        if (split.length > 2) throw new Error("Invalid integer: " + split.join("e"));\r
+        if (split.length === 2) {\r
+            var exp = split[1];\r
+            if (exp[0] === "+") exp = exp.slice(1);\r
+            exp = +exp;\r
+            if (exp !== truncate(exp) || !isPrecise(exp)) throw new Error("Invalid integer: " + exp + " is not a valid exponent.");\r
+            var text = split[0];\r
+            var decimalPlace = text.indexOf(".");\r
+            if (decimalPlace >= 0) {\r
+                exp -= text.length - decimalPlace - 1;\r
+                text = text.slice(0, decimalPlace) + text.slice(decimalPlace + 1);\r
+            }\r
+            if (exp < 0) throw new Error("Cannot include negative exponent part for integers");\r
+            text += (new Array(exp + 1)).join("0");\r
+            v = text;\r
+        }\r
+        var isValid = /^([0-9][0-9]*)$/.test(v);\r
+        if (!isValid) throw new Error("Invalid integer: " + v);\r
+        if (supportsNativeBigInt) {\r
+            return new NativeBigInt(BigInt(sign ? "-" + v : v));\r
+        }\r
+        var r = [], max = v.length, l = LOG_BASE, min = max - l;\r
+        while (max > 0) {\r
+            r.push(+v.slice(min, max));\r
+            min -= l;\r
+            if (min < 0) min = 0;\r
+            max -= l;\r
+        }\r
+        trim(r);\r
+        return new BigInteger(r, sign);\r
+    }\r
+\r
+    function parseNumberValue(v) {\r
+        if (supportsNativeBigInt) {\r
+            return new NativeBigInt(BigInt(v));\r
+        }\r
+        if (isPrecise(v)) {\r
+            if (v !== truncate(v)) throw new Error(v + " is not an integer.");\r
+            return new SmallInteger(v);\r
+        }\r
+        return parseStringValue(v.toString());\r
+    }\r
+\r
+    function parseValue(v) {\r
+        if (typeof v === "number") {\r
+            return parseNumberValue(v);\r
+        }\r
+        if (typeof v === "string") {\r
+            return parseStringValue(v);\r
+        }\r
+        if (typeof v === "bigint") {\r
+            return new NativeBigInt(v);\r
+        }\r
+        return v;\r
+    }\r
+    // Pre-define numbers in range [-999,999]\r
+    for (var i = 0; i < 1000; i++) {\r
+        Integer[i] = parseValue(i);\r
+        if (i > 0) Integer[-i] = parseValue(-i);\r
+    }\r
+    // Backwards compatibility\r
+    Integer.one = Integer[1];\r
+    Integer.zero = Integer[0];\r
+    Integer.minusOne = Integer[-1];\r
+    Integer.max = max;\r
+    Integer.min = min;\r
+    Integer.gcd = gcd;\r
+    Integer.lcm = lcm;\r
+    Integer.isInstance = function (x) { return x instanceof BigInteger || x instanceof SmallInteger || x instanceof NativeBigInt; };\r
+    Integer.randBetween = randBetween;\r
+\r
+    Integer.fromArray = function (digits, base, isNegative) {\r
+        return parseBaseFromArray(digits.map(parseValue), parseValue(base || 10), isNegative);\r
+    };\r
+\r
+    return Integer;\r
+})();\r
+\r
+// Node.js check\r
+if (typeof module !== "undefined" && module.hasOwnProperty("exports")) {\r
+    module.exports = bigInt;\r
+}\r
+\r
+//amd check\r
+if (typeof define === "function" && define.amd) {\r
+    define( function () {\r
+        return bigInt;\r
+    });\r
+}\r
diff --git a/tools/ui/src/lib/vendors/big-integer/LICENSE b/tools/ui/src/lib/vendors/big-integer/LICENSE
new file mode 100644 (file)
index 0000000..3ce22da
--- /dev/null
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.\r
+\r
+Anyone is free to copy, modify, publish, use, compile, sell, or\r
+distribute this software, either in source code form or as a compiled\r
+binary, for any purpose, commercial or non-commercial, and by any\r
+means.\r
+\r
+In jurisdictions that recognize copyright laws, the author or authors\r
+of this software dedicate any and all copyright interest in the\r
+software to the public domain. We make this dedication for the benefit\r
+of the public at large and to the detriment of our heirs and\r
+successors. We intend this dedication to be an overt act of\r
+relinquishment in perpetuity of all present and future rights to this\r
+software under copyright law.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\r
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\r
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\r
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\r
+OTHER DEALINGS IN THE SOFTWARE.\r
+\r
+For more information, please refer to <http://unlicense.org>\r
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 (file)
index 0000000..57740b9
--- /dev/null
@@ -0,0 +1,23 @@
+The MIT Licence.\r
+\r
+Copyright (c) 2025 Michael Mclaughlin\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining\r
+a copy of this software and associated documentation files (the\r
+'Software'), to deal in the Software without restriction, including\r
+without limitation the rights to use, copy, modify, merge, publish,\r
+distribute, sublicense, and/or sell copies of the Software, and to\r
+permit persons to whom the Software is furnished to do so, subject to\r
+the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be\r
+included in all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,\r
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\r
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\r
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\r
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\r
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
+\r
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 (file)
index 0000000..23295c6
--- /dev/null
@@ -0,0 +1,4951 @@
+;(function (globalScope) {\r
+  'use strict';\r
+\r
+\r
+  /*!\r
+   *  decimal.js v10.6.0\r
+   *  An arbitrary-precision Decimal type for JavaScript.\r
+   *  https://github.com/MikeMcl/decimal.js\r
+   *  Copyright (c) 2025 Michael Mclaughlin <M8ch88l@gmail.com>\r
+   *  MIT Licence\r
+   */\r
+\r
+\r
+  // -----------------------------------  EDITABLE DEFAULTS  ------------------------------------ //\r
+\r
+\r
+    // The maximum exponent magnitude.\r
+    // The limit on the value of `toExpNeg`, `toExpPos`, `minE` and `maxE`.\r
+  var EXP_LIMIT = 9e15,                      // 0 to 9e15\r
+\r
+    // The limit on the value of `precision`, and on the value of the first argument to\r
+    // `toDecimalPlaces`, `toExponential`, `toFixed`, `toPrecision` and `toSignificantDigits`.\r
+    MAX_DIGITS = 1e9,                        // 0 to 1e9\r
+\r
+    // Base conversion alphabet.\r
+    NUMERALS = '0123456789abcdef',\r
+\r
+    // The natural logarithm of 10 (1025 digits).\r
+    LN10 = '2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058',\r
+\r
+    // Pi (1025 digits).\r
+    PI = '3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789',\r
+\r
+\r
+    // The initial configuration properties of the Decimal constructor.\r
+    DEFAULTS = {\r
+\r
+      // These values must be integers within the stated ranges (inclusive).\r
+      // Most of these values can be changed at run-time using the `Decimal.config` method.\r
+\r
+      // The maximum number of significant digits of the result of a calculation or base conversion.\r
+      // E.g. `Decimal.config({ precision: 20 });`\r
+      precision: 20,                         // 1 to MAX_DIGITS\r
+\r
+      // The rounding mode used when rounding to `precision`.\r
+      //\r
+      // ROUND_UP         0 Away from zero.\r
+      // ROUND_DOWN       1 Towards zero.\r
+      // ROUND_CEIL       2 Towards +Infinity.\r
+      // ROUND_FLOOR      3 Towards -Infinity.\r
+      // ROUND_HALF_UP    4 Towards nearest neighbour. If equidistant, up.\r
+      // ROUND_HALF_DOWN  5 Towards nearest neighbour. If equidistant, down.\r
+      // ROUND_HALF_EVEN  6 Towards nearest neighbour. If equidistant, towards even neighbour.\r
+      // ROUND_HALF_CEIL  7 Towards nearest neighbour. If equidistant, towards +Infinity.\r
+      // ROUND_HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.\r
+      //\r
+      // E.g.\r
+      // `Decimal.rounding = 4;`\r
+      // `Decimal.rounding = Decimal.ROUND_HALF_UP;`\r
+      rounding: 4,                           // 0 to 8\r
+\r
+      // The modulo mode used when calculating the modulus: a mod n.\r
+      // The quotient (q = a / n) is calculated according to the corresponding rounding mode.\r
+      // The remainder (r) is calculated as: r = a - n * q.\r
+      //\r
+      // UP         0 The remainder is positive if the dividend is negative, else is negative.\r
+      // DOWN       1 The remainder has the same sign as the dividend (JavaScript %).\r
+      // FLOOR      3 The remainder has the same sign as the divisor (Python %).\r
+      // HALF_EVEN  6 The IEEE 754 remainder function.\r
+      // EUCLID     9 Euclidian division. q = sign(n) * floor(a / abs(n)). Always positive.\r
+      //\r
+      // Truncated division (1), floored division (3), the IEEE 754 remainder (6), and Euclidian\r
+      // division (9) are commonly used for the modulus operation. The other rounding modes can also\r
+      // be used, but they may not give useful results.\r
+      modulo: 1,                             // 0 to 9\r
+\r
+      // The exponent value at and beneath which `toString` returns exponential notation.\r
+      // JavaScript numbers: -7\r
+      toExpNeg: -7,                          // 0 to -EXP_LIMIT\r
+\r
+      // The exponent value at and above which `toString` returns exponential notation.\r
+      // JavaScript numbers: 21\r
+      toExpPos:  21,                         // 0 to EXP_LIMIT\r
+\r
+      // The minimum exponent value, beneath which underflow to zero occurs.\r
+      // JavaScript numbers: -324  (5e-324)\r
+      minE: -EXP_LIMIT,                      // -1 to -EXP_LIMIT\r
+\r
+      // The maximum exponent value, above which overflow to Infinity occurs.\r
+      // JavaScript numbers: 308  (1.7976931348623157e+308)\r
+      maxE: EXP_LIMIT,                       // 1 to EXP_LIMIT\r
+\r
+      // Whether to use cryptographically-secure random number generation, if available.\r
+      crypto: false                          // true/false\r
+    },\r
+\r
+\r
+  // ----------------------------------- END OF EDITABLE DEFAULTS ------------------------------- //\r
+\r
+\r
+    Decimal, inexact, noConflict, quadrant,\r
+    external = true,\r
+\r
+    decimalError = '[DecimalError] ',\r
+    invalidArgument = decimalError + 'Invalid argument: ',\r
+    precisionLimitExceeded = decimalError + 'Precision limit exceeded',\r
+    cryptoUnavailable = decimalError + 'crypto unavailable',\r
+    tag = '[object Decimal]',\r
+\r
+    mathfloor = Math.floor,\r
+    mathpow = Math.pow,\r
+\r
+    isBinary = /^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,\r
+    isHex = /^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,\r
+    isOctal = /^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,\r
+    isDecimal = /^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,\r
+\r
+    BASE = 1e7,\r
+    LOG_BASE = 7,\r
+    MAX_SAFE_INTEGER = 9007199254740991,\r
+\r
+    LN10_PRECISION = LN10.length - 1,\r
+    PI_PRECISION = PI.length - 1,\r
+\r
+    // Decimal.prototype object\r
+    P = { toStringTag: tag };\r
+\r
+\r
+  // Decimal prototype methods\r
+\r
+\r
+  /*\r
+   *  absoluteValue             abs\r
+   *  ceil\r
+   *  clampedTo                 clamp\r
+   *  comparedTo                cmp\r
+   *  cosine                    cos\r
+   *  cubeRoot                  cbrt\r
+   *  decimalPlaces             dp\r
+   *  dividedBy                 div\r
+   *  dividedToIntegerBy        divToInt\r
+   *  equals                    eq\r
+   *  floor\r
+   *  greaterThan               gt\r
+   *  greaterThanOrEqualTo      gte\r
+   *  hyperbolicCosine          cosh\r
+   *  hyperbolicSine            sinh\r
+   *  hyperbolicTangent         tanh\r
+   *  inverseCosine             acos\r
+   *  inverseHyperbolicCosine   acosh\r
+   *  inverseHyperbolicSine     asinh\r
+   *  inverseHyperbolicTangent  atanh\r
+   *  inverseSine               asin\r
+   *  inverseTangent            atan\r
+   *  isFinite\r
+   *  isInteger                 isInt\r
+   *  isNaN\r
+   *  isNegative                isNeg\r
+   *  isPositive                isPos\r
+   *  isZero\r
+   *  lessThan                  lt\r
+   *  lessThanOrEqualTo         lte\r
+   *  logarithm                 log\r
+   *  [maximum]                 [max]\r
+   *  [minimum]                 [min]\r
+   *  minus                     sub\r
+   *  modulo                    mod\r
+   *  naturalExponential        exp\r
+   *  naturalLogarithm          ln\r
+   *  negated                   neg\r
+   *  plus                      add\r
+   *  precision                 sd\r
+   *  round\r
+   *  sine                      sin\r
+   *  squareRoot                sqrt\r
+   *  tangent                   tan\r
+   *  times                     mul\r
+   *  toBinary\r
+   *  toDecimalPlaces           toDP\r
+   *  toExponential\r
+   *  toFixed\r
+   *  toFraction\r
+   *  toHexadecimal             toHex\r
+   *  toNearest\r
+   *  toNumber\r
+   *  toOctal\r
+   *  toPower                   pow\r
+   *  toPrecision\r
+   *  toSignificantDigits       toSD\r
+   *  toString\r
+   *  truncated                 trunc\r
+   *  valueOf                   toJSON\r
+   */\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the absolute value of this Decimal.\r
+   *\r
+   */\r
+  P.absoluteValue = P.abs = function () {\r
+    var x = new this.constructor(this);\r
+    if (x.s < 0) x.s = 1;\r
+    return finalise(x);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the\r
+   * direction of positive Infinity.\r
+   *\r
+   */\r
+  P.ceil = function () {\r
+    return finalise(new this.constructor(this), this.e + 1, 2);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal clamped to the range\r
+   * delineated by `min` and `max`.\r
+   *\r
+   * min {number|string|bigint|Decimal}\r
+   * max {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  P.clampedTo = P.clamp = function (min, max) {\r
+    var k,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+    min = new Ctor(min);\r
+    max = new Ctor(max);\r
+    if (!min.s || !max.s) return new Ctor(NaN);\r
+    if (min.gt(max)) throw Error(invalidArgument + max);\r
+    k = x.cmp(min);\r
+    return k < 0 ? min : x.cmp(max) > 0 ? max : new Ctor(x);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return\r
+   *   1    if the value of this Decimal is greater than the value of `y`,\r
+   *  -1    if the value of this Decimal is less than the value of `y`,\r
+   *   0    if they have the same value,\r
+   *   NaN  if the value of either Decimal is NaN.\r
+   *\r
+   */\r
+  P.comparedTo = P.cmp = function (y) {\r
+    var i, j, xdL, ydL,\r
+      x = this,\r
+      xd = x.d,\r
+      yd = (y = new x.constructor(y)).d,\r
+      xs = x.s,\r
+      ys = y.s;\r
+\r
+    // Either NaN or ±Infinity?\r
+    if (!xd || !yd) {\r
+      return !xs || !ys ? NaN : xs !== ys ? xs : xd === yd ? 0 : !xd ^ xs < 0 ? 1 : -1;\r
+    }\r
+\r
+    // Either zero?\r
+    if (!xd[0] || !yd[0]) return xd[0] ? xs : yd[0] ? -ys : 0;\r
+\r
+    // Signs differ?\r
+    if (xs !== ys) return xs;\r
+\r
+    // Compare exponents.\r
+    if (x.e !== y.e) return x.e > y.e ^ xs < 0 ? 1 : -1;\r
+\r
+    xdL = xd.length;\r
+    ydL = yd.length;\r
+\r
+    // Compare digit by digit.\r
+    for (i = 0, j = xdL < ydL ? xdL : ydL; i < j; ++i) {\r
+      if (xd[i] !== yd[i]) return xd[i] > yd[i] ^ xs < 0 ? 1 : -1;\r
+    }\r
+\r
+    // Compare lengths.\r
+    return xdL === ydL ? 0 : xdL > ydL ^ xs < 0 ? 1 : -1;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the cosine of the value in radians of this Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-1, 1]\r
+   *\r
+   * cos(0)         = 1\r
+   * cos(-0)        = 1\r
+   * cos(Infinity)  = NaN\r
+   * cos(-Infinity) = NaN\r
+   * cos(NaN)       = NaN\r
+   *\r
+   */\r
+  P.cosine = P.cos = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.d) return new Ctor(NaN);\r
+\r
+    // cos(0) = cos(-0) = 1\r
+    if (!x.d[0]) return new Ctor(1);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;\r
+    Ctor.rounding = 1;\r
+\r
+    x = cosine(Ctor, toLessThanHalfPi(Ctor, x));\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return finalise(quadrant == 2 || quadrant == 3 ? x.neg() : x, pr, rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   *\r
+   * Return a new Decimal whose value is the cube root of the value of this Decimal, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   *  cbrt(0)  =  0\r
+   *  cbrt(-0) = -0\r
+   *  cbrt(1)  =  1\r
+   *  cbrt(-1) = -1\r
+   *  cbrt(N)  =  N\r
+   *  cbrt(-I) = -I\r
+   *  cbrt(I)  =  I\r
+   *\r
+   * Math.cbrt(x) = (x < 0 ? -Math.pow(-x, 1/3) : Math.pow(x, 1/3))\r
+   *\r
+   */\r
+  P.cubeRoot = P.cbrt = function () {\r
+    var e, m, n, r, rep, s, sd, t, t3, t3plusx,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite() || x.isZero()) return new Ctor(x);\r
+    external = false;\r
+\r
+    // Initial estimate.\r
+    s = x.s * mathpow(x.s * x, 1 / 3);\r
+\r
+     // Math.cbrt underflow/overflow?\r
+     // Pass x to Math.pow as integer, then adjust the exponent of the result.\r
+    if (!s || Math.abs(s) == 1 / 0) {\r
+      n = digitsToString(x.d);\r
+      e = x.e;\r
+\r
+      // Adjust n exponent so it is a multiple of 3 away from x exponent.\r
+      if (s = (e - n.length + 1) % 3) n += (s == 1 || s == -2 ? '0' : '00');\r
+      s = mathpow(n, 1 / 3);\r
+\r
+      // Rarely, e may be one less than the result exponent value.\r
+      e = mathfloor((e + 1) / 3) - (e % 3 == (e < 0 ? -1 : 2));\r
+\r
+      if (s == 1 / 0) {\r
+        n = '5e' + e;\r
+      } else {\r
+        n = s.toExponential();\r
+        n = n.slice(0, n.indexOf('e') + 1) + e;\r
+      }\r
+\r
+      r = new Ctor(n);\r
+      r.s = x.s;\r
+    } else {\r
+      r = new Ctor(s.toString());\r
+    }\r
+\r
+    sd = (e = Ctor.precision) + 3;\r
+\r
+    // Halley's method.\r
+    // TODO? Compare Newton's method.\r
+    for (;;) {\r
+      t = r;\r
+      t3 = t.times(t).times(t);\r
+      t3plusx = t3.plus(x);\r
+      r = divide(t3plusx.plus(x).times(t), t3plusx.plus(t3), sd + 2, 1);\r
+\r
+      // TODO? Replace with for-loop and checkRoundingDigits.\r
+      if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {\r
+        n = n.slice(sd - 3, sd + 1);\r
+\r
+        // The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or 4999\r
+        // , i.e. approaching a rounding boundary, continue the iteration.\r
+        if (n == '9999' || !rep && n == '4999') {\r
+\r
+          // On the first iteration only, check to see if rounding up gives the exact result as the\r
+          // nines may infinitely repeat.\r
+          if (!rep) {\r
+            finalise(t, e + 1, 0);\r
+\r
+            if (t.times(t).times(t).eq(x)) {\r
+              r = t;\r
+              break;\r
+            }\r
+          }\r
+\r
+          sd += 4;\r
+          rep = 1;\r
+        } else {\r
+\r
+          // If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result.\r
+          // If not, then there are further digits and m will be truthy.\r
+          if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r
+\r
+            // Truncate to the first rounding digit.\r
+            finalise(r, e + 1, 1);\r
+            m = !r.times(r).times(r).eq(x);\r
+          }\r
+\r
+          break;\r
+        }\r
+      }\r
+    }\r
+\r
+    external = true;\r
+\r
+    return finalise(r, e, Ctor.rounding, m);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return the number of decimal places of the value of this Decimal.\r
+   *\r
+   */\r
+  P.decimalPlaces = P.dp = function () {\r
+    var w,\r
+      d = this.d,\r
+      n = NaN;\r
+\r
+    if (d) {\r
+      w = d.length - 1;\r
+      n = (w - mathfloor(this.e / LOG_BASE)) * LOG_BASE;\r
+\r
+      // Subtract the number of trailing zeros of the last word.\r
+      w = d[w];\r
+      if (w) for (; w % 10 == 0; w /= 10) n--;\r
+      if (n < 0) n = 0;\r
+    }\r
+\r
+    return n;\r
+  };\r
+\r
+\r
+  /*\r
+   *  n / 0 = I\r
+   *  n / N = N\r
+   *  n / I = 0\r
+   *  0 / n = 0\r
+   *  0 / 0 = N\r
+   *  0 / N = N\r
+   *  0 / I = 0\r
+   *  N / n = N\r
+   *  N / 0 = N\r
+   *  N / N = N\r
+   *  N / I = N\r
+   *  I / n = I\r
+   *  I / 0 = I\r
+   *  I / N = N\r
+   *  I / I = N\r
+   *\r
+   * Return a new Decimal whose value is the value of this Decimal divided by `y`, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.dividedBy = P.div = function (y) {\r
+    return divide(this, new this.constructor(y));\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the integer part of dividing the value of this Decimal\r
+   * by the value of `y`, rounded to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.dividedToIntegerBy = P.divToInt = function (y) {\r
+    var x = this,\r
+      Ctor = x.constructor;\r
+    return finalise(divide(x, new Ctor(y), 0, 1, 1), Ctor.precision, Ctor.rounding);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is equal to the value of `y`, otherwise return false.\r
+   *\r
+   */\r
+  P.equals = P.eq = function (y) {\r
+    return this.cmp(y) === 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal rounded to a whole number in the\r
+   * direction of negative Infinity.\r
+   *\r
+   */\r
+  P.floor = function () {\r
+    return finalise(new this.constructor(this), this.e + 1, 3);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is greater than the value of `y`, otherwise return\r
+   * false.\r
+   *\r
+   */\r
+  P.greaterThan = P.gt = function (y) {\r
+    return this.cmp(y) > 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is greater than or equal to the value of `y`,\r
+   * otherwise return false.\r
+   *\r
+   */\r
+  P.greaterThanOrEqualTo = P.gte = function (y) {\r
+    var k = this.cmp(y);\r
+    return k == 1 || k === 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic cosine of the value in radians of this\r
+   * Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [1, Infinity]\r
+   *\r
+   * cosh(x) = 1 + x^2/2! + x^4/4! + x^6/6! + ...\r
+   *\r
+   * cosh(0)         = 1\r
+   * cosh(-0)        = 1\r
+   * cosh(Infinity)  = Infinity\r
+   * cosh(-Infinity) = Infinity\r
+   * cosh(NaN)       = NaN\r
+   *\r
+   *  x        time taken (ms)   result\r
+   * 1000      9                 9.8503555700852349694e+433\r
+   * 10000     25                4.4034091128314607936e+4342\r
+   * 100000    171               1.4033316802130615897e+43429\r
+   * 1000000   3817              1.5166076984010437725e+434294\r
+   * 10000000  abandoned after 2 minute wait\r
+   *\r
+   * TODO? Compare performance of cosh(x) = 0.5 * (exp(x) + exp(-x))\r
+   *\r
+   */\r
+  P.hyperbolicCosine = P.cosh = function () {\r
+    var k, n, pr, rm, len,\r
+      x = this,\r
+      Ctor = x.constructor,\r
+      one = new Ctor(1);\r
+\r
+    if (!x.isFinite()) return new Ctor(x.s ? 1 / 0 : NaN);\r
+    if (x.isZero()) return one;\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;\r
+    Ctor.rounding = 1;\r
+    len = x.d.length;\r
+\r
+    // Argument reduction: cos(4x) = 1 - 8cos^2(x) + 8cos^4(x) + 1\r
+    // i.e. cos(x) = 1 - cos^2(x/4)(8 - 8cos^2(x/4))\r
+\r
+    // Estimate the optimum number of times to use the argument reduction.\r
+    // TODO? Estimation reused from cosine() and may not be optimal here.\r
+    if (len < 32) {\r
+      k = Math.ceil(len / 3);\r
+      n = (1 / tinyPow(4, k)).toString();\r
+    } else {\r
+      k = 16;\r
+      n = '2.3283064365386962890625e-10';\r
+    }\r
+\r
+    x = taylorSeries(Ctor, 1, x.times(n), new Ctor(1), true);\r
+\r
+    // Reverse argument reduction\r
+    var cosh2_x,\r
+      i = k,\r
+      d8 = new Ctor(8);\r
+    for (; i--;) {\r
+      cosh2_x = x.times(x);\r
+      x = one.minus(cosh2_x.times(d8.minus(cosh2_x.times(d8))));\r
+    }\r
+\r
+    return finalise(x, Ctor.precision = pr, Ctor.rounding = rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic sine of the value in radians of this\r
+   * Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-Infinity, Infinity]\r
+   *\r
+   * sinh(x) = x + x^3/3! + x^5/5! + x^7/7! + ...\r
+   *\r
+   * sinh(0)         = 0\r
+   * sinh(-0)        = -0\r
+   * sinh(Infinity)  = Infinity\r
+   * sinh(-Infinity) = -Infinity\r
+   * sinh(NaN)       = NaN\r
+   *\r
+   * x        time taken (ms)\r
+   * 10       2 ms\r
+   * 100      5 ms\r
+   * 1000     14 ms\r
+   * 10000    82 ms\r
+   * 100000   886 ms            1.4033316802130615897e+43429\r
+   * 200000   2613 ms\r
+   * 300000   5407 ms\r
+   * 400000   8824 ms\r
+   * 500000   13026 ms          8.7080643612718084129e+217146\r
+   * 1000000  48543 ms\r
+   *\r
+   * TODO? Compare performance of sinh(x) = 0.5 * (exp(x) - exp(-x))\r
+   *\r
+   */\r
+  P.hyperbolicSine = P.sinh = function () {\r
+    var k, pr, rm, len,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite() || x.isZero()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + Math.max(x.e, x.sd()) + 4;\r
+    Ctor.rounding = 1;\r
+    len = x.d.length;\r
+\r
+    if (len < 3) {\r
+      x = taylorSeries(Ctor, 2, x, x, true);\r
+    } else {\r
+\r
+      // Alternative argument reduction: sinh(3x) = sinh(x)(3 + 4sinh^2(x))\r
+      // i.e. sinh(x) = sinh(x/3)(3 + 4sinh^2(x/3))\r
+      // 3 multiplications and 1 addition\r
+\r
+      // Argument reduction: sinh(5x) = sinh(x)(5 + sinh^2(x)(20 + 16sinh^2(x)))\r
+      // i.e. sinh(x) = sinh(x/5)(5 + sinh^2(x/5)(20 + 16sinh^2(x/5)))\r
+      // 4 multiplications and 2 additions\r
+\r
+      // Estimate the optimum number of times to use the argument reduction.\r
+      k = 1.4 * Math.sqrt(len);\r
+      k = k > 16 ? 16 : k | 0;\r
+\r
+      x = x.times(1 / tinyPow(5, k));\r
+      x = taylorSeries(Ctor, 2, x, x, true);\r
+\r
+      // Reverse argument reduction\r
+      var sinh2_x,\r
+        d5 = new Ctor(5),\r
+        d16 = new Ctor(16),\r
+        d20 = new Ctor(20);\r
+      for (; k--;) {\r
+        sinh2_x = x.times(x);\r
+        x = x.times(d5.plus(sinh2_x.times(d16.times(sinh2_x).plus(d20))));\r
+      }\r
+    }\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return finalise(x, pr, rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic tangent of the value in radians of this\r
+   * Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-1, 1]\r
+   *\r
+   * tanh(x) = sinh(x) / cosh(x)\r
+   *\r
+   * tanh(0)         = 0\r
+   * tanh(-0)        = -0\r
+   * tanh(Infinity)  = 1\r
+   * tanh(-Infinity) = -1\r
+   * tanh(NaN)       = NaN\r
+   *\r
+   */\r
+  P.hyperbolicTangent = P.tanh = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite()) return new Ctor(x.s);\r
+    if (x.isZero()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + 7;\r
+    Ctor.rounding = 1;\r
+\r
+    return divide(x.sinh(), x.cosh(), Ctor.precision = pr, Ctor.rounding = rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arccosine (inverse cosine) in radians of the value of\r
+   * this Decimal.\r
+   *\r
+   * Domain: [-1, 1]\r
+   * Range: [0, pi]\r
+   *\r
+   * acos(x) = pi/2 - asin(x)\r
+   *\r
+   * acos(0)       = pi/2\r
+   * acos(-0)      = pi/2\r
+   * acos(1)       = 0\r
+   * acos(-1)      = pi\r
+   * acos(1/2)     = pi/3\r
+   * acos(-1/2)    = 2*pi/3\r
+   * acos(|x| > 1) = NaN\r
+   * acos(NaN)     = NaN\r
+   *\r
+   */\r
+  P.inverseCosine = P.acos = function () {\r
+    var x = this,\r
+      Ctor = x.constructor,\r
+      k = x.abs().cmp(1),\r
+      pr = Ctor.precision,\r
+      rm = Ctor.rounding;\r
+\r
+    if (k !== -1) {\r
+      return k === 0\r
+        // |x| is 1\r
+        ? x.isNeg() ? getPi(Ctor, pr, rm) : new Ctor(0)\r
+        // |x| > 1 or x is NaN\r
+        : new Ctor(NaN);\r
+    }\r
+\r
+    if (x.isZero()) return getPi(Ctor, pr + 4, rm).times(0.5);\r
+\r
+    // TODO? Special case acos(0.5) = pi/3 and acos(-0.5) = 2*pi/3\r
+\r
+    Ctor.precision = pr + 6;\r
+    Ctor.rounding = 1;\r
+\r
+    // See https://github.com/MikeMcl/decimal.js/pull/217\r
+    x = new Ctor(1).minus(x).div(x.plus(1)).sqrt().atan();\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return x.times(2);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic cosine in radians of the\r
+   * value of this Decimal.\r
+   *\r
+   * Domain: [1, Infinity]\r
+   * Range: [0, Infinity]\r
+   *\r
+   * acosh(x) = ln(x + sqrt(x^2 - 1))\r
+   *\r
+   * acosh(x < 1)     = NaN\r
+   * acosh(NaN)       = NaN\r
+   * acosh(Infinity)  = Infinity\r
+   * acosh(-Infinity) = NaN\r
+   * acosh(0)         = NaN\r
+   * acosh(-0)        = NaN\r
+   * acosh(1)         = 0\r
+   * acosh(-1)        = NaN\r
+   *\r
+   */\r
+  P.inverseHyperbolicCosine = P.acosh = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (x.lte(1)) return new Ctor(x.eq(1) ? 0 : NaN);\r
+    if (!x.isFinite()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + Math.max(Math.abs(x.e), x.sd()) + 4;\r
+    Ctor.rounding = 1;\r
+    external = false;\r
+\r
+    x = x.times(x).minus(1).sqrt().plus(x);\r
+\r
+    external = true;\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return x.ln();\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic sine in radians of the value\r
+   * of this Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-Infinity, Infinity]\r
+   *\r
+   * asinh(x) = ln(x + sqrt(x^2 + 1))\r
+   *\r
+   * asinh(NaN)       = NaN\r
+   * asinh(Infinity)  = Infinity\r
+   * asinh(-Infinity) = -Infinity\r
+   * asinh(0)         = 0\r
+   * asinh(-0)        = -0\r
+   *\r
+   */\r
+  P.inverseHyperbolicSine = P.asinh = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite() || x.isZero()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + 2 * Math.max(Math.abs(x.e), x.sd()) + 6;\r
+    Ctor.rounding = 1;\r
+    external = false;\r
+\r
+    x = x.times(x).plus(1).sqrt().plus(x);\r
+\r
+    external = true;\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return x.ln();\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic tangent in radians of the\r
+   * value of this Decimal.\r
+   *\r
+   * Domain: [-1, 1]\r
+   * Range: [-Infinity, Infinity]\r
+   *\r
+   * atanh(x) = 0.5 * ln((1 + x) / (1 - x))\r
+   *\r
+   * atanh(|x| > 1)   = NaN\r
+   * atanh(NaN)       = NaN\r
+   * atanh(Infinity)  = NaN\r
+   * atanh(-Infinity) = NaN\r
+   * atanh(0)         = 0\r
+   * atanh(-0)        = -0\r
+   * atanh(1)         = Infinity\r
+   * atanh(-1)        = -Infinity\r
+   *\r
+   */\r
+  P.inverseHyperbolicTangent = P.atanh = function () {\r
+    var pr, rm, wpr, xsd,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite()) return new Ctor(NaN);\r
+    if (x.e >= 0) return new Ctor(x.abs().eq(1) ? x.s / 0 : x.isZero() ? x : NaN);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    xsd = x.sd();\r
+\r
+    if (Math.max(xsd, pr) < 2 * -x.e - 1) return finalise(new Ctor(x), pr, rm, true);\r
+\r
+    Ctor.precision = wpr = xsd - x.e;\r
+\r
+    x = divide(x.plus(1), new Ctor(1).minus(x), wpr + pr, 1);\r
+\r
+    Ctor.precision = pr + 4;\r
+    Ctor.rounding = 1;\r
+\r
+    x = x.ln();\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return x.times(0.5);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arcsine (inverse sine) in radians of the value of this\r
+   * Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-pi/2, pi/2]\r
+   *\r
+   * asin(x) = 2*atan(x/(1 + sqrt(1 - x^2)))\r
+   *\r
+   * asin(0)       = 0\r
+   * asin(-0)      = -0\r
+   * asin(1/2)     = pi/6\r
+   * asin(-1/2)    = -pi/6\r
+   * asin(1)       = pi/2\r
+   * asin(-1)      = -pi/2\r
+   * asin(|x| > 1) = NaN\r
+   * asin(NaN)     = NaN\r
+   *\r
+   * TODO? Compare performance of Taylor series.\r
+   *\r
+   */\r
+  P.inverseSine = P.asin = function () {\r
+    var halfPi, k,\r
+      pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (x.isZero()) return new Ctor(x);\r
+\r
+    k = x.abs().cmp(1);\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+\r
+    if (k !== -1) {\r
+\r
+      // |x| is 1\r
+      if (k === 0) {\r
+        halfPi = getPi(Ctor, pr + 4, rm).times(0.5);\r
+        halfPi.s = x.s;\r
+        return halfPi;\r
+      }\r
+\r
+      // |x| > 1 or x is NaN\r
+      return new Ctor(NaN);\r
+    }\r
+\r
+    // TODO? Special case asin(1/2) = pi/6 and asin(-1/2) = -pi/6\r
+\r
+    Ctor.precision = pr + 6;\r
+    Ctor.rounding = 1;\r
+\r
+    x = x.div(new Ctor(1).minus(x.times(x)).sqrt().plus(1)).atan();\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return x.times(2);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arctangent (inverse tangent) in radians of the value\r
+   * of this Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-pi/2, pi/2]\r
+   *\r
+   * atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...\r
+   *\r
+   * atan(0)         = 0\r
+   * atan(-0)        = -0\r
+   * atan(1)         = pi/4\r
+   * atan(-1)        = -pi/4\r
+   * atan(Infinity)  = pi/2\r
+   * atan(-Infinity) = -pi/2\r
+   * atan(NaN)       = NaN\r
+   *\r
+   */\r
+  P.inverseTangent = P.atan = function () {\r
+    var i, j, k, n, px, t, r, wpr, x2,\r
+      x = this,\r
+      Ctor = x.constructor,\r
+      pr = Ctor.precision,\r
+      rm = Ctor.rounding;\r
+\r
+    if (!x.isFinite()) {\r
+      if (!x.s) return new Ctor(NaN);\r
+      if (pr + 4 <= PI_PRECISION) {\r
+        r = getPi(Ctor, pr + 4, rm).times(0.5);\r
+        r.s = x.s;\r
+        return r;\r
+      }\r
+    } else if (x.isZero()) {\r
+      return new Ctor(x);\r
+    } else if (x.abs().eq(1) && pr + 4 <= PI_PRECISION) {\r
+      r = getPi(Ctor, pr + 4, rm).times(0.25);\r
+      r.s = x.s;\r
+      return r;\r
+    }\r
+\r
+    Ctor.precision = wpr = pr + 10;\r
+    Ctor.rounding = 1;\r
+\r
+    // TODO? if (x >= 1 && pr <= PI_PRECISION) atan(x) = halfPi * x.s - atan(1 / x);\r
+\r
+    // Argument reduction\r
+    // Ensure |x| < 0.42\r
+    // atan(x) = 2 * atan(x / (1 + sqrt(1 + x^2)))\r
+\r
+    k = Math.min(28, wpr / LOG_BASE + 2 | 0);\r
+\r
+    for (i = k; i; --i) x = x.div(x.times(x).plus(1).sqrt().plus(1));\r
+\r
+    external = false;\r
+\r
+    j = Math.ceil(wpr / LOG_BASE);\r
+    n = 1;\r
+    x2 = x.times(x);\r
+    r = new Ctor(x);\r
+    px = x;\r
+\r
+    // atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ...\r
+    for (; i !== -1;) {\r
+      px = px.times(x2);\r
+      t = r.minus(px.div(n += 2));\r
+\r
+      px = px.times(x2);\r
+      r = t.plus(px.div(n += 2));\r
+\r
+      if (r.d[j] !== void 0) for (i = j; r.d[i] === t.d[i] && i--;);\r
+    }\r
+\r
+    if (k) r = r.times(2 << (k - 1));\r
+\r
+    external = true;\r
+\r
+    return finalise(r, Ctor.precision = pr, Ctor.rounding = rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is a finite number, otherwise return false.\r
+   *\r
+   */\r
+  P.isFinite = function () {\r
+    return !!this.d;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is an integer, otherwise return false.\r
+   *\r
+   */\r
+  P.isInteger = P.isInt = function () {\r
+    return !!this.d && mathfloor(this.e / LOG_BASE) > this.d.length - 2;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is NaN, otherwise return false.\r
+   *\r
+   */\r
+  P.isNaN = function () {\r
+    return !this.s;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is negative, otherwise return false.\r
+   *\r
+   */\r
+  P.isNegative = P.isNeg = function () {\r
+    return this.s < 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is positive, otherwise return false.\r
+   *\r
+   */\r
+  P.isPositive = P.isPos = function () {\r
+    return this.s > 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is 0 or -0, otherwise return false.\r
+   *\r
+   */\r
+  P.isZero = function () {\r
+    return !!this.d && this.d[0] === 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is less than `y`, otherwise return false.\r
+   *\r
+   */\r
+  P.lessThan = P.lt = function (y) {\r
+    return this.cmp(y) < 0;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return true if the value of this Decimal is less than or equal to `y`, otherwise return false.\r
+   *\r
+   */\r
+  P.lessThanOrEqualTo = P.lte = function (y) {\r
+    return this.cmp(y) < 1;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return the logarithm of the value of this Decimal to the specified base, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * If no base is specified, return log[10](arg).\r
+   *\r
+   * log[base](arg) = ln(arg) / ln(base)\r
+   *\r
+   * The result will always be correctly rounded if the base of the log is 10, and 'almost always'\r
+   * otherwise:\r
+   *\r
+   * Depending on the rounding mode, the result may be incorrectly rounded if the first fifteen\r
+   * rounding digits are [49]99999999999999 or [50]00000000000000. In that case, the maximum error\r
+   * between the result and the correctly rounded result will be one ulp (unit in the last place).\r
+   *\r
+   * log[-b](a)       = NaN\r
+   * log[0](a)        = NaN\r
+   * log[1](a)        = NaN\r
+   * log[NaN](a)      = NaN\r
+   * log[Infinity](a) = NaN\r
+   * log[b](0)        = -Infinity\r
+   * log[b](-0)       = -Infinity\r
+   * log[b](-a)       = NaN\r
+   * log[b](1)        = 0\r
+   * log[b](Infinity) = Infinity\r
+   * log[b](NaN)      = NaN\r
+   *\r
+   * [base] {number|string|bigint|Decimal} The base of the logarithm.\r
+   *\r
+   */\r
+  P.logarithm = P.log = function (base) {\r
+    var isBase10, d, denominator, k, inf, num, sd, r,\r
+      arg = this,\r
+      Ctor = arg.constructor,\r
+      pr = Ctor.precision,\r
+      rm = Ctor.rounding,\r
+      guard = 5;\r
+\r
+    // Default base is 10.\r
+    if (base == null) {\r
+      base = new Ctor(10);\r
+      isBase10 = true;\r
+    } else {\r
+      base = new Ctor(base);\r
+      d = base.d;\r
+\r
+      // Return NaN if base is negative, or non-finite, or is 0 or 1.\r
+      if (base.s < 0 || !d || !d[0] || base.eq(1)) return new Ctor(NaN);\r
+\r
+      isBase10 = base.eq(10);\r
+    }\r
+\r
+    d = arg.d;\r
+\r
+    // Is arg negative, non-finite, 0 or 1?\r
+    if (arg.s < 0 || !d || !d[0] || arg.eq(1)) {\r
+      return new Ctor(d && !d[0] ? -1 / 0 : arg.s != 1 ? NaN : d ? 0 : 1 / 0);\r
+    }\r
+\r
+    // The result will have a non-terminating decimal expansion if base is 10 and arg is not an\r
+    // integer power of 10.\r
+    if (isBase10) {\r
+      if (d.length > 1) {\r
+        inf = true;\r
+      } else {\r
+        for (k = d[0]; k % 10 === 0;) k /= 10;\r
+        inf = k !== 1;\r
+      }\r
+    }\r
+\r
+    external = false;\r
+    sd = pr + guard;\r
+    num = naturalLogarithm(arg, sd);\r
+    denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);\r
+\r
+    // The result will have 5 rounding digits.\r
+    r = divide(num, denominator, sd, 1);\r
+\r
+    // If at a rounding boundary, i.e. the result's rounding digits are [49]9999 or [50]0000,\r
+    // calculate 10 further digits.\r
+    //\r
+    // If the result is known to have an infinite decimal expansion, repeat this until it is clear\r
+    // that the result is above or below the boundary. Otherwise, if after calculating the 10\r
+    // further digits, the last 14 are nines, round up and assume the result is exact.\r
+    // Also assume the result is exact if the last 14 are zero.\r
+    //\r
+    // Example of a result that will be incorrectly rounded:\r
+    // log[1048576](4503599627370502) = 2.60000000000000009610279511444746...\r
+    // The above result correctly rounded using ROUND_CEIL to 1 decimal place should be 2.7, but it\r
+    // will be given as 2.6 as there are 15 zeros immediately after the requested decimal place, so\r
+    // the exact result would be assumed to be 2.6, which rounded using ROUND_CEIL to 1 decimal\r
+    // place is still 2.6.\r
+    if (checkRoundingDigits(r.d, k = pr, rm)) {\r
+\r
+      do {\r
+        sd += 10;\r
+        num = naturalLogarithm(arg, sd);\r
+        denominator = isBase10 ? getLn10(Ctor, sd + 10) : naturalLogarithm(base, sd);\r
+        r = divide(num, denominator, sd, 1);\r
+\r
+        if (!inf) {\r
+\r
+          // Check for 14 nines from the 2nd rounding digit, as the first may be 4.\r
+          if (+digitsToString(r.d).slice(k + 1, k + 15) + 1 == 1e14) {\r
+            r = finalise(r, pr + 1, 0);\r
+          }\r
+\r
+          break;\r
+        }\r
+      } while (checkRoundingDigits(r.d, k += 10, rm));\r
+    }\r
+\r
+    external = true;\r
+\r
+    return finalise(r, pr, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the maximum of the arguments and the value of this Decimal.\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+  P.max = function () {\r
+    Array.prototype.push.call(arguments, this);\r
+    return maxOrMin(this.constructor, arguments, -1);\r
+  };\r
+   */\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the minimum of the arguments and the value of this Decimal.\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+  P.min = function () {\r
+    Array.prototype.push.call(arguments, this);\r
+    return maxOrMin(this.constructor, arguments, 1);\r
+  };\r
+   */\r
+\r
+\r
+  /*\r
+   *  n - 0 = n\r
+   *  n - N = N\r
+   *  n - I = -I\r
+   *  0 - n = -n\r
+   *  0 - 0 = 0\r
+   *  0 - N = N\r
+   *  0 - I = -I\r
+   *  N - n = N\r
+   *  N - 0 = N\r
+   *  N - N = N\r
+   *  N - I = N\r
+   *  I - n = I\r
+   *  I - 0 = I\r
+   *  I - N = N\r
+   *  I - I = N\r
+   *\r
+   * Return a new Decimal whose value is the value of this Decimal minus `y`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.minus = P.sub = function (y) {\r
+    var d, e, i, j, k, len, pr, rm, xd, xe, xLTy, yd,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    y = new Ctor(y);\r
+\r
+    // If either is not finite...\r
+    if (!x.d || !y.d) {\r
+\r
+      // Return NaN if either is NaN.\r
+      if (!x.s || !y.s) y = new Ctor(NaN);\r
+\r
+      // Return y negated if x is finite and y is ±Infinity.\r
+      else if (x.d) y.s = -y.s;\r
+\r
+      // Return x if y is finite and x is ±Infinity.\r
+      // Return x if both are ±Infinity with different signs.\r
+      // Return NaN if both are ±Infinity with the same sign.\r
+      else y = new Ctor(y.d || x.s !== y.s ? x : NaN);\r
+\r
+      return y;\r
+    }\r
+\r
+    // If signs differ...\r
+    if (x.s != y.s) {\r
+      y.s = -y.s;\r
+      return x.plus(y);\r
+    }\r
+\r
+    xd = x.d;\r
+    yd = y.d;\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+\r
+    // If either is zero...\r
+    if (!xd[0] || !yd[0]) {\r
+\r
+      // Return y negated if x is zero and y is non-zero.\r
+      if (yd[0]) y.s = -y.s;\r
+\r
+      // Return x if y is zero and x is non-zero.\r
+      else if (xd[0]) y = new Ctor(x);\r
+\r
+      // Return zero if both are zero.\r
+      // From IEEE 754 (2008) 6.3: 0 - 0 = -0 - -0 = -0 when rounding to -Infinity.\r
+      else return new Ctor(rm === 3 ? -0 : 0);\r
+\r
+      return external ? finalise(y, pr, rm) : y;\r
+    }\r
+\r
+    // x and y are finite, non-zero numbers with the same sign.\r
+\r
+    // Calculate base 1e7 exponents.\r
+    e = mathfloor(y.e / LOG_BASE);\r
+    xe = mathfloor(x.e / LOG_BASE);\r
+\r
+    xd = xd.slice();\r
+    k = xe - e;\r
+\r
+    // If base 1e7 exponents differ...\r
+    if (k) {\r
+      xLTy = k < 0;\r
+\r
+      if (xLTy) {\r
+        d = xd;\r
+        k = -k;\r
+        len = yd.length;\r
+      } else {\r
+        d = yd;\r
+        e = xe;\r
+        len = xd.length;\r
+      }\r
+\r
+      // Numbers with massively different exponents would result in a very high number of\r
+      // zeros needing to be prepended, but this can be avoided while still ensuring correct\r
+      // rounding by limiting the number of zeros to `Math.ceil(pr / LOG_BASE) + 2`.\r
+      i = Math.max(Math.ceil(pr / LOG_BASE), len) + 2;\r
+\r
+      if (k > i) {\r
+        k = i;\r
+        d.length = 1;\r
+      }\r
+\r
+      // Prepend zeros to equalise exponents.\r
+      d.reverse();\r
+      for (i = k; i--;) d.push(0);\r
+      d.reverse();\r
+\r
+    // Base 1e7 exponents equal.\r
+    } else {\r
+\r
+      // Check digits to determine which is the bigger number.\r
+\r
+      i = xd.length;\r
+      len = yd.length;\r
+      xLTy = i < len;\r
+      if (xLTy) len = i;\r
+\r
+      for (i = 0; i < len; i++) {\r
+        if (xd[i] != yd[i]) {\r
+          xLTy = xd[i] < yd[i];\r
+          break;\r
+        }\r
+      }\r
+\r
+      k = 0;\r
+    }\r
+\r
+    if (xLTy) {\r
+      d = xd;\r
+      xd = yd;\r
+      yd = d;\r
+      y.s = -y.s;\r
+    }\r
+\r
+    len = xd.length;\r
+\r
+    // Append zeros to `xd` if shorter.\r
+    // Don't add zeros to `yd` if shorter as subtraction only needs to start at `yd` length.\r
+    for (i = yd.length - len; i > 0; --i) xd[len++] = 0;\r
+\r
+    // Subtract yd from xd.\r
+    for (i = yd.length; i > k;) {\r
+\r
+      if (xd[--i] < yd[i]) {\r
+        for (j = i; j && xd[--j] === 0;) xd[j] = BASE - 1;\r
+        --xd[j];\r
+        xd[i] += BASE;\r
+      }\r
+\r
+      xd[i] -= yd[i];\r
+    }\r
+\r
+    // Remove trailing zeros.\r
+    for (; xd[--len] === 0;) xd.pop();\r
+\r
+    // Remove leading zeros and adjust exponent accordingly.\r
+    for (; xd[0] === 0; xd.shift()) --e;\r
+\r
+    // Zero?\r
+    if (!xd[0]) return new Ctor(rm === 3 ? -0 : 0);\r
+\r
+    y.d = xd;\r
+    y.e = getBase10Exponent(xd, e);\r
+\r
+    return external ? finalise(y, pr, rm) : y;\r
+  };\r
+\r
+\r
+  /*\r
+   *   n % 0 =  N\r
+   *   n % N =  N\r
+   *   n % I =  n\r
+   *   0 % n =  0\r
+   *  -0 % n = -0\r
+   *   0 % 0 =  N\r
+   *   0 % N =  N\r
+   *   0 % I =  0\r
+   *   N % n =  N\r
+   *   N % 0 =  N\r
+   *   N % N =  N\r
+   *   N % I =  N\r
+   *   I % n =  N\r
+   *   I % 0 =  N\r
+   *   I % N =  N\r
+   *   I % I =  N\r
+   *\r
+   * Return a new Decimal whose value is the value of this Decimal modulo `y`, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * The result depends on the modulo mode.\r
+   *\r
+   */\r
+  P.modulo = P.mod = function (y) {\r
+    var q,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    y = new Ctor(y);\r
+\r
+    // Return NaN if x is ±Infinity or NaN, or y is NaN or ±0.\r
+    if (!x.d || !y.s || y.d && !y.d[0]) return new Ctor(NaN);\r
+\r
+    // Return x if y is ±Infinity or x is ±0.\r
+    if (!y.d || x.d && !x.d[0]) {\r
+      return finalise(new Ctor(x), Ctor.precision, Ctor.rounding);\r
+    }\r
+\r
+    // Prevent rounding of intermediate calculations.\r
+    external = false;\r
+\r
+    if (Ctor.modulo == 9) {\r
+\r
+      // Euclidian division: q = sign(y) * floor(x / abs(y))\r
+      // result = x - q * y    where  0 <= result < abs(y)\r
+      q = divide(x, y.abs(), 0, 3, 1);\r
+      q.s *= y.s;\r
+    } else {\r
+      q = divide(x, y, 0, Ctor.modulo, 1);\r
+    }\r
+\r
+    q = q.times(y);\r
+\r
+    external = true;\r
+\r
+    return x.minus(q);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural exponential of the value of this Decimal,\r
+   * i.e. the base e raised to the power the value of this Decimal, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.naturalExponential = P.exp = function () {\r
+    return naturalExponential(this);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural logarithm of the value of this Decimal,\r
+   * rounded to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.naturalLogarithm = P.ln = function () {\r
+    return naturalLogarithm(this);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal negated, i.e. as if multiplied by\r
+   * -1.\r
+   *\r
+   */\r
+  P.negated = P.neg = function () {\r
+    var x = new this.constructor(this);\r
+    x.s = -x.s;\r
+    return finalise(x);\r
+  };\r
+\r
+\r
+  /*\r
+   *  n + 0 = n\r
+   *  n + N = N\r
+   *  n + I = I\r
+   *  0 + n = n\r
+   *  0 + 0 = 0\r
+   *  0 + N = N\r
+   *  0 + I = I\r
+   *  N + n = N\r
+   *  N + 0 = N\r
+   *  N + N = N\r
+   *  N + I = N\r
+   *  I + n = I\r
+   *  I + 0 = I\r
+   *  I + N = N\r
+   *  I + I = I\r
+   *\r
+   * Return a new Decimal whose value is the value of this Decimal plus `y`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.plus = P.add = function (y) {\r
+    var carry, d, e, i, k, len, pr, rm, xd, yd,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    y = new Ctor(y);\r
+\r
+    // If either is not finite...\r
+    if (!x.d || !y.d) {\r
+\r
+      // Return NaN if either is NaN.\r
+      if (!x.s || !y.s) y = new Ctor(NaN);\r
+\r
+      // Return x if y is finite and x is ±Infinity.\r
+      // Return x if both are ±Infinity with the same sign.\r
+      // Return NaN if both are ±Infinity with different signs.\r
+      // Return y if x is finite and y is ±Infinity.\r
+      else if (!x.d) y = new Ctor(y.d || x.s === y.s ? x : NaN);\r
+\r
+      return y;\r
+    }\r
+\r
+     // If signs differ...\r
+    if (x.s != y.s) {\r
+      y.s = -y.s;\r
+      return x.minus(y);\r
+    }\r
+\r
+    xd = x.d;\r
+    yd = y.d;\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+\r
+    // If either is zero...\r
+    if (!xd[0] || !yd[0]) {\r
+\r
+      // Return x if y is zero.\r
+      // Return y if y is non-zero.\r
+      if (!yd[0]) y = new Ctor(x);\r
+\r
+      return external ? finalise(y, pr, rm) : y;\r
+    }\r
+\r
+    // x and y are finite, non-zero numbers with the same sign.\r
+\r
+    // Calculate base 1e7 exponents.\r
+    k = mathfloor(x.e / LOG_BASE);\r
+    e = mathfloor(y.e / LOG_BASE);\r
+\r
+    xd = xd.slice();\r
+    i = k - e;\r
+\r
+    // If base 1e7 exponents differ...\r
+    if (i) {\r
+\r
+      if (i < 0) {\r
+        d = xd;\r
+        i = -i;\r
+        len = yd.length;\r
+      } else {\r
+        d = yd;\r
+        e = k;\r
+        len = xd.length;\r
+      }\r
+\r
+      // Limit number of zeros prepended to max(ceil(pr / LOG_BASE), len) + 1.\r
+      k = Math.ceil(pr / LOG_BASE);\r
+      len = k > len ? k + 1 : len + 1;\r
+\r
+      if (i > len) {\r
+        i = len;\r
+        d.length = 1;\r
+      }\r
+\r
+      // Prepend zeros to equalise exponents. Note: Faster to use reverse then do unshifts.\r
+      d.reverse();\r
+      for (; i--;) d.push(0);\r
+      d.reverse();\r
+    }\r
+\r
+    len = xd.length;\r
+    i = yd.length;\r
+\r
+    // If yd is longer than xd, swap xd and yd so xd points to the longer array.\r
+    if (len - i < 0) {\r
+      i = len;\r
+      d = yd;\r
+      yd = xd;\r
+      xd = d;\r
+    }\r
+\r
+    // Only start adding at yd.length - 1 as the further digits of xd can be left as they are.\r
+    for (carry = 0; i;) {\r
+      carry = (xd[--i] = xd[i] + yd[i] + carry) / BASE | 0;\r
+      xd[i] %= BASE;\r
+    }\r
+\r
+    if (carry) {\r
+      xd.unshift(carry);\r
+      ++e;\r
+    }\r
+\r
+    // Remove trailing zeros.\r
+    // No need to check for zero, as +x + +y != 0 && -x + -y != 0\r
+    for (len = xd.length; xd[--len] == 0;) xd.pop();\r
+\r
+    y.d = xd;\r
+    y.e = getBase10Exponent(xd, e);\r
+\r
+    return external ? finalise(y, pr, rm) : y;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return the number of significant digits of the value of this Decimal.\r
+   *\r
+   * [z] {boolean|number} Whether to count integer-part trailing zeros: true, false, 1 or 0.\r
+   *\r
+   */\r
+  P.precision = P.sd = function (z) {\r
+    var k,\r
+      x = this;\r
+\r
+    if (z !== void 0 && z !== !!z && z !== 1 && z !== 0) throw Error(invalidArgument + z);\r
+\r
+    if (x.d) {\r
+      k = getPrecision(x.d);\r
+      if (z && x.e + 1 > k) k = x.e + 1;\r
+    } else {\r
+      k = NaN;\r
+    }\r
+\r
+    return k;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal rounded to a whole number using\r
+   * rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.round = function () {\r
+    var x = this,\r
+      Ctor = x.constructor;\r
+\r
+    return finalise(new Ctor(x), x.e + 1, Ctor.rounding);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the sine of the value in radians of this Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-1, 1]\r
+   *\r
+   * sin(x) = x - x^3/3! + x^5/5! - ...\r
+   *\r
+   * sin(0)         = 0\r
+   * sin(-0)        = -0\r
+   * sin(Infinity)  = NaN\r
+   * sin(-Infinity) = NaN\r
+   * sin(NaN)       = NaN\r
+   *\r
+   */\r
+  P.sine = P.sin = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite()) return new Ctor(NaN);\r
+    if (x.isZero()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + Math.max(x.e, x.sd()) + LOG_BASE;\r
+    Ctor.rounding = 1;\r
+\r
+    x = sine(Ctor, toLessThanHalfPi(Ctor, x));\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return finalise(quadrant > 2 ? x.neg() : x, pr, rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the square root of this Decimal, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   *  sqrt(-n) =  N\r
+   *  sqrt(N)  =  N\r
+   *  sqrt(-I) =  N\r
+   *  sqrt(I)  =  I\r
+   *  sqrt(0)  =  0\r
+   *  sqrt(-0) = -0\r
+   *\r
+   */\r
+  P.squareRoot = P.sqrt = function () {\r
+    var m, n, sd, r, rep, t,\r
+      x = this,\r
+      d = x.d,\r
+      e = x.e,\r
+      s = x.s,\r
+      Ctor = x.constructor;\r
+\r
+    // Negative/NaN/Infinity/zero?\r
+    if (s !== 1 || !d || !d[0]) {\r
+      return new Ctor(!s || s < 0 && (!d || d[0]) ? NaN : d ? x : 1 / 0);\r
+    }\r
+\r
+    external = false;\r
+\r
+    // Initial estimate.\r
+    s = Math.sqrt(+x);\r
+\r
+    // Math.sqrt underflow/overflow?\r
+    // Pass x to Math.sqrt as integer, then adjust the exponent of the result.\r
+    if (s == 0 || s == 1 / 0) {\r
+      n = digitsToString(d);\r
+\r
+      if ((n.length + e) % 2 == 0) n += '0';\r
+      s = Math.sqrt(n);\r
+      e = mathfloor((e + 1) / 2) - (e < 0 || e % 2);\r
+\r
+      if (s == 1 / 0) {\r
+        n = '5e' + e;\r
+      } else {\r
+        n = s.toExponential();\r
+        n = n.slice(0, n.indexOf('e') + 1) + e;\r
+      }\r
+\r
+      r = new Ctor(n);\r
+    } else {\r
+      r = new Ctor(s.toString());\r
+    }\r
+\r
+    sd = (e = Ctor.precision) + 3;\r
+\r
+    // Newton-Raphson iteration.\r
+    for (;;) {\r
+      t = r;\r
+      r = t.plus(divide(x, t, sd + 2, 1)).times(0.5);\r
+\r
+      // TODO? Replace with for-loop and checkRoundingDigits.\r
+      if (digitsToString(t.d).slice(0, sd) === (n = digitsToString(r.d)).slice(0, sd)) {\r
+        n = n.slice(sd - 3, sd + 1);\r
+\r
+        // The 4th rounding digit may be in error by -1 so if the 4 rounding digits are 9999 or\r
+        // 4999, i.e. approaching a rounding boundary, continue the iteration.\r
+        if (n == '9999' || !rep && n == '4999') {\r
+\r
+          // On the first iteration only, check to see if rounding up gives the exact result as the\r
+          // nines may infinitely repeat.\r
+          if (!rep) {\r
+            finalise(t, e + 1, 0);\r
+\r
+            if (t.times(t).eq(x)) {\r
+              r = t;\r
+              break;\r
+            }\r
+          }\r
+\r
+          sd += 4;\r
+          rep = 1;\r
+        } else {\r
+\r
+          // If the rounding digits are null, 0{0,4} or 50{0,3}, check for an exact result.\r
+          // If not, then there are further digits and m will be truthy.\r
+          if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r
+\r
+            // Truncate to the first rounding digit.\r
+            finalise(r, e + 1, 1);\r
+            m = !r.times(r).eq(x);\r
+          }\r
+\r
+          break;\r
+        }\r
+      }\r
+    }\r
+\r
+    external = true;\r
+\r
+    return finalise(r, e, Ctor.rounding, m);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the tangent of the value in radians of this Decimal.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-Infinity, Infinity]\r
+   *\r
+   * tan(0)         = 0\r
+   * tan(-0)        = -0\r
+   * tan(Infinity)  = NaN\r
+   * tan(-Infinity) = NaN\r
+   * tan(NaN)       = NaN\r
+   *\r
+   */\r
+  P.tangent = P.tan = function () {\r
+    var pr, rm,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (!x.isFinite()) return new Ctor(NaN);\r
+    if (x.isZero()) return new Ctor(x);\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+    Ctor.precision = pr + 10;\r
+    Ctor.rounding = 1;\r
+\r
+    x = x.sin();\r
+    x.s = 1;\r
+    x = divide(x, new Ctor(1).minus(x.times(x)).sqrt(), pr + 10, 0);\r
+\r
+    Ctor.precision = pr;\r
+    Ctor.rounding = rm;\r
+\r
+    return finalise(quadrant == 2 || quadrant == 4 ? x.neg() : x, pr, rm, true);\r
+  };\r
+\r
+\r
+  /*\r
+   *  n * 0 = 0\r
+   *  n * N = N\r
+   *  n * I = I\r
+   *  0 * n = 0\r
+   *  0 * 0 = 0\r
+   *  0 * N = N\r
+   *  0 * I = N\r
+   *  N * n = N\r
+   *  N * 0 = N\r
+   *  N * N = N\r
+   *  N * I = N\r
+   *  I * n = I\r
+   *  I * 0 = N\r
+   *  I * N = N\r
+   *  I * I = I\r
+   *\r
+   * Return a new Decimal whose value is this Decimal times `y`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   */\r
+  P.times = P.mul = function (y) {\r
+    var carry, e, i, k, r, rL, t, xdL, ydL,\r
+      x = this,\r
+      Ctor = x.constructor,\r
+      xd = x.d,\r
+      yd = (y = new Ctor(y)).d;\r
+\r
+    y.s *= x.s;\r
+\r
+     // If either is NaN, ±Infinity or ±0...\r
+    if (!xd || !xd[0] || !yd || !yd[0]) {\r
+\r
+      return new Ctor(!y.s || xd && !xd[0] && !yd || yd && !yd[0] && !xd\r
+\r
+        // Return NaN if either is NaN.\r
+        // Return NaN if x is ±0 and y is ±Infinity, or y is ±0 and x is ±Infinity.\r
+        ? NaN\r
+\r
+        // Return ±Infinity if either is ±Infinity.\r
+        // Return ±0 if either is ±0.\r
+        : !xd || !yd ? y.s / 0 : y.s * 0);\r
+    }\r
+\r
+    e = mathfloor(x.e / LOG_BASE) + mathfloor(y.e / LOG_BASE);\r
+    xdL = xd.length;\r
+    ydL = yd.length;\r
+\r
+    // Ensure xd points to the longer array.\r
+    if (xdL < ydL) {\r
+      r = xd;\r
+      xd = yd;\r
+      yd = r;\r
+      rL = xdL;\r
+      xdL = ydL;\r
+      ydL = rL;\r
+    }\r
+\r
+    // Initialise the result array with zeros.\r
+    r = [];\r
+    rL = xdL + ydL;\r
+    for (i = rL; i--;) r.push(0);\r
+\r
+    // Multiply!\r
+    for (i = ydL; --i >= 0;) {\r
+      carry = 0;\r
+      for (k = xdL + i; k > i;) {\r
+        t = r[k] + yd[i] * xd[k - i - 1] + carry;\r
+        r[k--] = t % BASE | 0;\r
+        carry = t / BASE | 0;\r
+      }\r
+\r
+      r[k] = (r[k] + carry) % BASE | 0;\r
+    }\r
+\r
+    // Remove trailing zeros.\r
+    for (; !r[--rL];) r.pop();\r
+\r
+    if (carry) ++e;\r
+    else r.shift();\r
+\r
+    y.d = r;\r
+    y.e = getBase10Exponent(r, e);\r
+\r
+    return external ? finalise(y, Ctor.precision, Ctor.rounding) : y;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal in base 2, round to `sd` significant\r
+   * digits using rounding mode `rm`.\r
+   *\r
+   * If the optional `sd` argument is present then return binary exponential notation.\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toBinary = function (sd, rm) {\r
+    return toStringBinary(this, 2, sd, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `dp`\r
+   * decimal places using rounding mode `rm` or `rounding` if `rm` is omitted.\r
+   *\r
+   * If `dp` is omitted, return a new Decimal whose value is the value of this Decimal.\r
+   *\r
+   * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toDecimalPlaces = P.toDP = function (dp, rm) {\r
+    var x = this,\r
+      Ctor = x.constructor;\r
+\r
+    x = new Ctor(x);\r
+    if (dp === void 0) return x;\r
+\r
+    checkInt32(dp, 0, MAX_DIGITS);\r
+\r
+    if (rm === void 0) rm = Ctor.rounding;\r
+    else checkInt32(rm, 0, 8);\r
+\r
+    return finalise(x, dp + x.e + 1, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal in exponential notation rounded to\r
+   * `dp` fixed decimal places using rounding mode `rounding`.\r
+   *\r
+   * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toExponential = function (dp, rm) {\r
+    var str,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (dp === void 0) {\r
+      str = finiteToString(x, true);\r
+    } else {\r
+      checkInt32(dp, 0, MAX_DIGITS);\r
+\r
+      if (rm === void 0) rm = Ctor.rounding;\r
+      else checkInt32(rm, 0, 8);\r
+\r
+      x = finalise(new Ctor(x), dp + 1, rm);\r
+      str = finiteToString(x, true, dp + 1);\r
+    }\r
+\r
+    return x.isNeg() && !x.isZero() ? '-' + str : str;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal in normal (fixed-point) notation to\r
+   * `dp` fixed decimal places and rounded using rounding mode `rm` or `rounding` if `rm` is\r
+   * omitted.\r
+   *\r
+   * As with JavaScript numbers, (-0).toFixed(0) is '0', but e.g. (-0.00001).toFixed(0) is '-0'.\r
+   *\r
+   * [dp] {number} Decimal places. Integer, 0 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   * (-0).toFixed(0) is '0', but (-0.1).toFixed(0) is '-0'.\r
+   * (-0).toFixed(1) is '0.0', but (-0.01).toFixed(1) is '-0.0'.\r
+   * (-0).toFixed(3) is '0.000'.\r
+   * (-0.5).toFixed(0) is '-0'.\r
+   *\r
+   */\r
+  P.toFixed = function (dp, rm) {\r
+    var str, y,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (dp === void 0) {\r
+      str = finiteToString(x);\r
+    } else {\r
+      checkInt32(dp, 0, MAX_DIGITS);\r
+\r
+      if (rm === void 0) rm = Ctor.rounding;\r
+      else checkInt32(rm, 0, 8);\r
+\r
+      y = finalise(new Ctor(x), dp + x.e + 1, rm);\r
+      str = finiteToString(y, false, dp + y.e + 1);\r
+    }\r
+\r
+    // To determine whether to add the minus sign look at the value before it was rounded,\r
+    // i.e. look at `x` rather than `y`.\r
+    return x.isNeg() && !x.isZero() ? '-' + str : str;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return an array representing the value of this Decimal as a simple fraction with an integer\r
+   * numerator and an integer denominator.\r
+   *\r
+   * The denominator will be a positive non-zero value less than or equal to the specified maximum\r
+   * denominator. If a maximum denominator is not specified, the denominator will be the lowest\r
+   * value necessary to represent the number exactly.\r
+   *\r
+   * [maxD] {number|string|bigint|Decimal} Maximum denominator. Integer >= 1 and < Infinity.\r
+   *\r
+   */\r
+  P.toFraction = function (maxD) {\r
+    var d, d0, d1, d2, e, k, n, n0, n1, pr, q, r,\r
+      x = this,\r
+      xd = x.d,\r
+      Ctor = x.constructor;\r
+\r
+    if (!xd) return new Ctor(x);\r
+\r
+    n1 = d0 = new Ctor(1);\r
+    d1 = n0 = new Ctor(0);\r
+\r
+    d = new Ctor(d1);\r
+    e = d.e = getPrecision(xd) - x.e - 1;\r
+    k = e % LOG_BASE;\r
+    d.d[0] = mathpow(10, k < 0 ? LOG_BASE + k : k);\r
+\r
+    if (maxD == null) {\r
+\r
+      // d is 10**e, the minimum max-denominator needed.\r
+      maxD = e > 0 ? d : n1;\r
+    } else {\r
+      n = new Ctor(maxD);\r
+      if (!n.isInt() || n.lt(n1)) throw Error(invalidArgument + n);\r
+      maxD = n.gt(d) ? (e > 0 ? d : n1) : n;\r
+    }\r
+\r
+    external = false;\r
+    n = new Ctor(digitsToString(xd));\r
+    pr = Ctor.precision;\r
+    Ctor.precision = e = xd.length * LOG_BASE * 2;\r
+\r
+    for (;;)  {\r
+      q = divide(n, d, 0, 1, 1);\r
+      d2 = d0.plus(q.times(d1));\r
+      if (d2.cmp(maxD) == 1) break;\r
+      d0 = d1;\r
+      d1 = d2;\r
+      d2 = n1;\r
+      n1 = n0.plus(q.times(d2));\r
+      n0 = d2;\r
+      d2 = d;\r
+      d = n.minus(q.times(d2));\r
+      n = d2;\r
+    }\r
+\r
+    d2 = divide(maxD.minus(d0), d1, 0, 1, 1);\r
+    n0 = n0.plus(d2.times(n1));\r
+    d0 = d0.plus(d2.times(d1));\r
+    n0.s = n1.s = x.s;\r
+\r
+    // Determine which fraction is closer to x, n0/d0 or n1/d1?\r
+    r = divide(n1, d1, e, 1).minus(x).abs().cmp(divide(n0, d0, e, 1).minus(x).abs()) < 1\r
+        ? [n1, d1] : [n0, d0];\r
+\r
+    Ctor.precision = pr;\r
+    external = true;\r
+\r
+    return r;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal in base 16, round to `sd` significant\r
+   * digits using rounding mode `rm`.\r
+   *\r
+   * If the optional `sd` argument is present then return binary exponential notation.\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toHexadecimal = P.toHex = function (sd, rm) {\r
+    return toStringBinary(this, 16, sd, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Returns a new Decimal whose value is the nearest multiple of `y` in the direction of rounding\r
+   * mode `rm`, or `Decimal.rounding` if `rm` is omitted, to the value of this Decimal.\r
+   *\r
+   * The return value will always have the same sign as this Decimal, unless either this Decimal\r
+   * or `y` is NaN, in which case the return value will be also be NaN.\r
+   *\r
+   * The return value is not affected by the value of `precision`.\r
+   *\r
+   * y {number|string|bigint|Decimal} The magnitude to round to a multiple of.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   * 'toNearest() rounding mode not an integer: {rm}'\r
+   * 'toNearest() rounding mode out of range: {rm}'\r
+   *\r
+   */\r
+  P.toNearest = function (y, rm) {\r
+    var x = this,\r
+      Ctor = x.constructor;\r
+\r
+    x = new Ctor(x);\r
+\r
+    if (y == null) {\r
+\r
+      // If x is not finite, return x.\r
+      if (!x.d) return x;\r
+\r
+      y = new Ctor(1);\r
+      rm = Ctor.rounding;\r
+    } else {\r
+      y = new Ctor(y);\r
+      if (rm === void 0) {\r
+        rm = Ctor.rounding;\r
+      } else {\r
+        checkInt32(rm, 0, 8);\r
+      }\r
+\r
+      // If x is not finite, return x if y is not NaN, else NaN.\r
+      if (!x.d) return y.s ? x : y;\r
+\r
+      // If y is not finite, return Infinity with the sign of x if y is Infinity, else NaN.\r
+      if (!y.d) {\r
+        if (y.s) y.s = x.s;\r
+        return y;\r
+      }\r
+    }\r
+\r
+    // If y is not zero, calculate the nearest multiple of y to x.\r
+    if (y.d[0]) {\r
+      external = false;\r
+      x = divide(x, y, 0, rm, 1).times(y);\r
+      external = true;\r
+      finalise(x);\r
+\r
+    // If y is zero, return zero with the sign of x.\r
+    } else {\r
+      y.s = x.s;\r
+      x = y;\r
+    }\r
+\r
+    return x;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return the value of this Decimal converted to a number primitive.\r
+   * Zero keeps its sign.\r
+   *\r
+   */\r
+  P.toNumber = function () {\r
+    return +this;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal in base 8, round to `sd` significant\r
+   * digits using rounding mode `rm`.\r
+   *\r
+   * If the optional `sd` argument is present then return binary exponential notation.\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toOctal = function (sd, rm) {\r
+    return toStringBinary(this, 8, sd, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal raised to the power `y`, rounded\r
+   * to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * ECMAScript compliant.\r
+   *\r
+   *   pow(x, NaN)                           = NaN\r
+   *   pow(x, ±0)                            = 1\r
+\r
+   *   pow(NaN, non-zero)                    = NaN\r
+   *   pow(abs(x) > 1, +Infinity)            = +Infinity\r
+   *   pow(abs(x) > 1, -Infinity)            = +0\r
+   *   pow(abs(x) == 1, ±Infinity)           = NaN\r
+   *   pow(abs(x) < 1, +Infinity)            = +0\r
+   *   pow(abs(x) < 1, -Infinity)            = +Infinity\r
+   *   pow(+Infinity, y > 0)                 = +Infinity\r
+   *   pow(+Infinity, y < 0)                 = +0\r
+   *   pow(-Infinity, odd integer > 0)       = -Infinity\r
+   *   pow(-Infinity, even integer > 0)      = +Infinity\r
+   *   pow(-Infinity, odd integer < 0)       = -0\r
+   *   pow(-Infinity, even integer < 0)      = +0\r
+   *   pow(+0, y > 0)                        = +0\r
+   *   pow(+0, y < 0)                        = +Infinity\r
+   *   pow(-0, odd integer > 0)              = -0\r
+   *   pow(-0, even integer > 0)             = +0\r
+   *   pow(-0, odd integer < 0)              = -Infinity\r
+   *   pow(-0, even integer < 0)             = +Infinity\r
+   *   pow(finite x < 0, finite non-integer) = NaN\r
+   *\r
+   * For non-integer or very large exponents pow(x, y) is calculated using\r
+   *\r
+   *   x^y = exp(y*ln(x))\r
+   *\r
+   * Assuming the first 15 rounding digits are each equally likely to be any digit 0-9, the\r
+   * probability of an incorrectly rounded result\r
+   * P([49]9{14} | [50]0{14}) = 2 * 0.2 * 10^-14 = 4e-15 = 1/2.5e+14\r
+   * i.e. 1 in 250,000,000,000,000\r
+   *\r
+   * If a result is incorrectly rounded the maximum error will be 1 ulp (unit in last place).\r
+   *\r
+   * y {number|string|bigint|Decimal} The power to which to raise this Decimal.\r
+   *\r
+   */\r
+  P.toPower = P.pow = function (y) {\r
+    var e, k, pr, r, rm, s,\r
+      x = this,\r
+      Ctor = x.constructor,\r
+      yn = +(y = new Ctor(y));\r
+\r
+    // Either ±Infinity, NaN or ±0?\r
+    if (!x.d || !y.d || !x.d[0] || !y.d[0]) return new Ctor(mathpow(+x, yn));\r
+\r
+    x = new Ctor(x);\r
+\r
+    if (x.eq(1)) return x;\r
+\r
+    pr = Ctor.precision;\r
+    rm = Ctor.rounding;\r
+\r
+    if (y.eq(1)) return finalise(x, pr, rm);\r
+\r
+    // y exponent\r
+    e = mathfloor(y.e / LOG_BASE);\r
+\r
+    // If y is a small integer use the 'exponentiation by squaring' algorithm.\r
+    if (e >= y.d.length - 1 && (k = yn < 0 ? -yn : yn) <= MAX_SAFE_INTEGER) {\r
+      r = intPow(Ctor, x, k, pr);\r
+      return y.s < 0 ? new Ctor(1).div(r) : finalise(r, pr, rm);\r
+    }\r
+\r
+    s = x.s;\r
+\r
+    // if x is negative\r
+    if (s < 0) {\r
+\r
+      // if y is not an integer\r
+      if (e < y.d.length - 1) return new Ctor(NaN);\r
+\r
+      // Result is positive if x is negative and the last digit of integer y is even.\r
+      if ((y.d[e] & 1) == 0) s = 1;\r
+\r
+      // if x.eq(-1)\r
+      if (x.e == 0 && x.d[0] == 1 && x.d.length == 1) {\r
+        x.s = s;\r
+        return x;\r
+      }\r
+    }\r
+\r
+    // Estimate result exponent.\r
+    // x^y = 10^e,  where e = y * log10(x)\r
+    // log10(x) = log10(x_significand) + x_exponent\r
+    // log10(x_significand) = ln(x_significand) / ln(10)\r
+    k = mathpow(+x, yn);\r
+    e = k == 0 || !isFinite(k)\r
+      ? mathfloor(yn * (Math.log('0.' + digitsToString(x.d)) / Math.LN10 + x.e + 1))\r
+      : new Ctor(k + '').e;\r
+\r
+    // Exponent estimate may be incorrect e.g. x: 0.999999999999999999, y: 2.29, e: 0, r.e: -1.\r
+\r
+    // Overflow/underflow?\r
+    if (e > Ctor.maxE + 1 || e < Ctor.minE - 1) return new Ctor(e > 0 ? s / 0 : 0);\r
+\r
+    external = false;\r
+    Ctor.rounding = x.s = 1;\r
+\r
+    // Estimate the extra guard digits needed to ensure five correct rounding digits from\r
+    // naturalLogarithm(x). Example of failure without these extra digits (precision: 10):\r
+    // new Decimal(2.32456).pow('2087987436534566.46411')\r
+    // should be 1.162377823e+764914905173815, but is 1.162355823e+764914905173815\r
+    k = Math.min(12, (e + '').length);\r
+\r
+    // r = x^y = exp(y*ln(x))\r
+    r = naturalExponential(y.times(naturalLogarithm(x, pr + k)), pr);\r
+\r
+    // r may be Infinity, e.g. (0.9999999999999999).pow(-1e+40)\r
+    if (r.d) {\r
+\r
+      // Truncate to the required precision plus five rounding digits.\r
+      r = finalise(r, pr + 5, 1);\r
+\r
+      // If the rounding digits are [49]9999 or [50]0000 increase the precision by 10 and recalculate\r
+      // the result.\r
+      if (checkRoundingDigits(r.d, pr, rm)) {\r
+        e = pr + 10;\r
+\r
+        // Truncate to the increased precision plus five rounding digits.\r
+        r = finalise(naturalExponential(y.times(naturalLogarithm(x, e + k)), e), e + 5, 1);\r
+\r
+        // Check for 14 nines from the 2nd rounding digit (the first rounding digit may be 4 or 9).\r
+        if (+digitsToString(r.d).slice(pr + 1, pr + 15) + 1 == 1e14) {\r
+          r = finalise(r, pr + 1, 0);\r
+        }\r
+      }\r
+    }\r
+\r
+    r.s = s;\r
+    external = true;\r
+    Ctor.rounding = rm;\r
+\r
+    return finalise(r, pr, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal rounded to `sd` significant digits\r
+   * using rounding mode `rounding`.\r
+   *\r
+   * Return exponential notation if `sd` is less than the number of digits necessary to represent\r
+   * the integer part of the value in normal notation.\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   */\r
+  P.toPrecision = function (sd, rm) {\r
+    var str,\r
+      x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (sd === void 0) {\r
+      str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);\r
+    } else {\r
+      checkInt32(sd, 1, MAX_DIGITS);\r
+\r
+      if (rm === void 0) rm = Ctor.rounding;\r
+      else checkInt32(rm, 0, 8);\r
+\r
+      x = finalise(new Ctor(x), sd, rm);\r
+      str = finiteToString(x, sd <= x.e || x.e <= Ctor.toExpNeg, sd);\r
+    }\r
+\r
+    return x.isNeg() && !x.isZero() ? '-' + str : str;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal rounded to a maximum of `sd`\r
+   * significant digits using rounding mode `rm`, or to `precision` and `rounding` respectively if\r
+   * omitted.\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 1 to MAX_DIGITS inclusive.\r
+   * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r
+   *\r
+   * 'toSD() digits out of range: {sd}'\r
+   * 'toSD() digits not an integer: {sd}'\r
+   * 'toSD() rounding mode not an integer: {rm}'\r
+   * 'toSD() rounding mode out of range: {rm}'\r
+   *\r
+   */\r
+  P.toSignificantDigits = P.toSD = function (sd, rm) {\r
+    var x = this,\r
+      Ctor = x.constructor;\r
+\r
+    if (sd === void 0) {\r
+      sd = Ctor.precision;\r
+      rm = Ctor.rounding;\r
+    } else {\r
+      checkInt32(sd, 1, MAX_DIGITS);\r
+\r
+      if (rm === void 0) rm = Ctor.rounding;\r
+      else checkInt32(rm, 0, 8);\r
+    }\r
+\r
+    return finalise(new Ctor(x), sd, rm);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal.\r
+   *\r
+   * Return exponential notation if this Decimal has a positive exponent equal to or greater than\r
+   * `toExpPos`, or a negative exponent equal to or less than `toExpNeg`.\r
+   *\r
+   */\r
+  P.toString = function () {\r
+    var x = this,\r
+      Ctor = x.constructor,\r
+      str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);\r
+\r
+    return x.isNeg() && !x.isZero() ? '-' + str : str;\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of this Decimal truncated to a whole number.\r
+   *\r
+   */\r
+  P.truncated = P.trunc = function () {\r
+    return finalise(new this.constructor(this), this.e + 1, 1);\r
+  };\r
+\r
+\r
+  /*\r
+   * Return a string representing the value of this Decimal.\r
+   * Unlike `toString`, negative zero will include the minus sign.\r
+   *\r
+   */\r
+  P.valueOf = P.toJSON = function () {\r
+    var x = this,\r
+      Ctor = x.constructor,\r
+      str = finiteToString(x, x.e <= Ctor.toExpNeg || x.e >= Ctor.toExpPos);\r
+\r
+    return x.isNeg() ? '-' + str : str;\r
+  };\r
+\r
+\r
+  // Helper functions for Decimal.prototype (P) and/or Decimal methods, and their callers.\r
+\r
+\r
+  /*\r
+   *  digitsToString           P.cubeRoot, P.logarithm, P.squareRoot, P.toFraction, P.toPower,\r
+   *                           finiteToString, naturalExponential, naturalLogarithm\r
+   *  checkInt32               P.toDecimalPlaces, P.toExponential, P.toFixed, P.toNearest,\r
+   *                           P.toPrecision, P.toSignificantDigits, toStringBinary, random\r
+   *  checkRoundingDigits      P.logarithm, P.toPower, naturalExponential, naturalLogarithm\r
+   *  convertBase              toStringBinary, parseOther\r
+   *  cos                      P.cos\r
+   *  divide                   P.atanh, P.cubeRoot, P.dividedBy, P.dividedToIntegerBy,\r
+   *                           P.logarithm, P.modulo, P.squareRoot, P.tan, P.tanh, P.toFraction,\r
+   *                           P.toNearest, toStringBinary, naturalExponential, naturalLogarithm,\r
+   *                           taylorSeries, atan2, parseOther\r
+   *  finalise                 P.absoluteValue, P.atan, P.atanh, P.ceil, P.cos, P.cosh,\r
+   *                           P.cubeRoot, P.dividedToIntegerBy, P.floor, P.logarithm, P.minus,\r
+   *                           P.modulo, P.negated, P.plus, P.round, P.sin, P.sinh, P.squareRoot,\r
+   *                           P.tan, P.times, P.toDecimalPlaces, P.toExponential, P.toFixed,\r
+   *                           P.toNearest, P.toPower, P.toPrecision, P.toSignificantDigits,\r
+   *                           P.truncated, divide, getLn10, getPi, naturalExponential,\r
+   *                           naturalLogarithm, ceil, floor, round, trunc\r
+   *  finiteToString           P.toExponential, P.toFixed, P.toPrecision, P.toString, P.valueOf,\r
+   *                           toStringBinary\r
+   *  getBase10Exponent        P.minus, P.plus, P.times, parseOther\r
+   *  getLn10                  P.logarithm, naturalLogarithm\r
+   *  getPi                    P.acos, P.asin, P.atan, toLessThanHalfPi, atan2\r
+   *  getPrecision             P.precision, P.toFraction\r
+   *  getZeroString            digitsToString, finiteToString\r
+   *  intPow                   P.toPower, parseOther\r
+   *  isOdd                    toLessThanHalfPi\r
+   *  maxOrMin                 max, min\r
+   *  naturalExponential       P.naturalExponential, P.toPower\r
+   *  naturalLogarithm         P.acosh, P.asinh, P.atanh, P.logarithm, P.naturalLogarithm,\r
+   *                           P.toPower, naturalExponential\r
+   *  nonFiniteToString        finiteToString, toStringBinary\r
+   *  parseDecimal             Decimal\r
+   *  parseOther               Decimal\r
+   *  sin                      P.sin\r
+   *  taylorSeries             P.cosh, P.sinh, cos, sin\r
+   *  toLessThanHalfPi         P.cos, P.sin\r
+   *  toStringBinary           P.toBinary, P.toHexadecimal, P.toOctal\r
+   *  truncate                 intPow\r
+   *\r
+   *  Throws:                  P.logarithm, P.precision, P.toFraction, checkInt32, getLn10, getPi,\r
+   *                           naturalLogarithm, config, parseOther, random, Decimal\r
+   */\r
+\r
+\r
+  function digitsToString(d) {\r
+    var i, k, ws,\r
+      indexOfLastWord = d.length - 1,\r
+      str = '',\r
+      w = d[0];\r
+\r
+    if (indexOfLastWord > 0) {\r
+      str += w;\r
+      for (i = 1; i < indexOfLastWord; i++) {\r
+        ws = d[i] + '';\r
+        k = LOG_BASE - ws.length;\r
+        if (k) str += getZeroString(k);\r
+        str += ws;\r
+      }\r
+\r
+      w = d[i];\r
+      ws = w + '';\r
+      k = LOG_BASE - ws.length;\r
+      if (k) str += getZeroString(k);\r
+    } else if (w === 0) {\r
+      return '0';\r
+    }\r
+\r
+    // Remove trailing zeros of last w.\r
+    for (; w % 10 === 0;) w /= 10;\r
+\r
+    return str + w;\r
+  }\r
+\r
+\r
+  function checkInt32(i, min, max) {\r
+    if (i !== ~~i || i < min || i > max) {\r
+      throw Error(invalidArgument + i);\r
+    }\r
+  }\r
+\r
+\r
+  /*\r
+   * Check 5 rounding digits if `repeating` is null, 4 otherwise.\r
+   * `repeating == null` if caller is `log` or `pow`,\r
+   * `repeating != null` if caller is `naturalLogarithm` or `naturalExponential`.\r
+   */\r
+  function checkRoundingDigits(d, i, rm, repeating) {\r
+    var di, k, r, rd;\r
+\r
+    // Get the length of the first word of the array d.\r
+    for (k = d[0]; k >= 10; k /= 10) --i;\r
+\r
+    // Is the rounding digit in the first word of d?\r
+    if (--i < 0) {\r
+      i += LOG_BASE;\r
+      di = 0;\r
+    } else {\r
+      di = Math.ceil((i + 1) / LOG_BASE);\r
+      i %= LOG_BASE;\r
+    }\r
+\r
+    // i is the index (0 - 6) of the rounding digit.\r
+    // E.g. if within the word 3487563 the first rounding digit is 5,\r
+    // then i = 4, k = 1000, rd = 3487563 % 1000 = 563\r
+    k = mathpow(10, LOG_BASE - i);\r
+    rd = d[di] % k | 0;\r
+\r
+    if (repeating == null) {\r
+      if (i < 3) {\r
+        if (i == 0) rd = rd / 100 | 0;\r
+        else if (i == 1) rd = rd / 10 | 0;\r
+        r = rm < 4 && rd == 99999 || rm > 3 && rd == 49999 || rd == 50000 || rd == 0;\r
+      } else {\r
+        r = (rm < 4 && rd + 1 == k || rm > 3 && rd + 1 == k / 2) &&\r
+          (d[di + 1] / k / 100 | 0) == mathpow(10, i - 2) - 1 ||\r
+            (rd == k / 2 || rd == 0) && (d[di + 1] / k / 100 | 0) == 0;\r
+      }\r
+    } else {\r
+      if (i < 4) {\r
+        if (i == 0) rd = rd / 1000 | 0;\r
+        else if (i == 1) rd = rd / 100 | 0;\r
+        else if (i == 2) rd = rd / 10 | 0;\r
+        r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999;\r
+      } else {\r
+        r = ((repeating || rm < 4) && rd + 1 == k ||\r
+        (!repeating && rm > 3) && rd + 1 == k / 2) &&\r
+          (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1;\r
+      }\r
+    }\r
+\r
+    return r;\r
+  }\r
+\r
+\r
+  // Convert string of `baseIn` to an array of numbers of `baseOut`.\r
+  // Eg. convertBase('255', 10, 16) returns [15, 15].\r
+  // Eg. convertBase('ff', 16, 10) returns [2, 5, 5].\r
+  function convertBase(str, baseIn, baseOut) {\r
+    var j,\r
+      arr = [0],\r
+      arrL,\r
+      i = 0,\r
+      strL = str.length;\r
+\r
+    for (; i < strL;) {\r
+      for (arrL = arr.length; arrL--;) arr[arrL] *= baseIn;\r
+      arr[0] += NUMERALS.indexOf(str.charAt(i++));\r
+      for (j = 0; j < arr.length; j++) {\r
+        if (arr[j] > baseOut - 1) {\r
+          if (arr[j + 1] === void 0) arr[j + 1] = 0;\r
+          arr[j + 1] += arr[j] / baseOut | 0;\r
+          arr[j] %= baseOut;\r
+        }\r
+      }\r
+    }\r
+\r
+    return arr.reverse();\r
+  }\r
+\r
+\r
+  /*\r
+   * cos(x) = 1 - x^2/2! + x^4/4! - ...\r
+   * |x| < pi/2\r
+   *\r
+   */\r
+  function cosine(Ctor, x) {\r
+    var k, len, y;\r
+\r
+    if (x.isZero()) return x;\r
+\r
+    // Argument reduction: cos(4x) = 8*(cos^4(x) - cos^2(x)) + 1\r
+    // i.e. cos(x) = 8*(cos^4(x/4) - cos^2(x/4)) + 1\r
+\r
+    // Estimate the optimum number of times to use the argument reduction.\r
+    len = x.d.length;\r
+    if (len < 32) {\r
+      k = Math.ceil(len / 3);\r
+      y = (1 / tinyPow(4, k)).toString();\r
+    } else {\r
+      k = 16;\r
+      y = '2.3283064365386962890625e-10';\r
+    }\r
+\r
+    Ctor.precision += k;\r
+\r
+    x = taylorSeries(Ctor, 1, x.times(y), new Ctor(1));\r
+\r
+    // Reverse argument reduction\r
+    for (var i = k; i--;) {\r
+      var cos2x = x.times(x);\r
+      x = cos2x.times(cos2x).minus(cos2x).times(8).plus(1);\r
+    }\r
+\r
+    Ctor.precision -= k;\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  /*\r
+   * Perform division in the specified base.\r
+   */\r
+  var divide = (function () {\r
+\r
+    // Assumes non-zero x and k, and hence non-zero result.\r
+    function multiplyInteger(x, k, base) {\r
+      var temp,\r
+        carry = 0,\r
+        i = x.length;\r
+\r
+      for (x = x.slice(); i--;) {\r
+        temp = x[i] * k + carry;\r
+        x[i] = temp % base | 0;\r
+        carry = temp / base | 0;\r
+      }\r
+\r
+      if (carry) x.unshift(carry);\r
+\r
+      return x;\r
+    }\r
+\r
+    function compare(a, b, aL, bL) {\r
+      var i, r;\r
+\r
+      if (aL != bL) {\r
+        r = aL > bL ? 1 : -1;\r
+      } else {\r
+        for (i = r = 0; i < aL; i++) {\r
+          if (a[i] != b[i]) {\r
+            r = a[i] > b[i] ? 1 : -1;\r
+            break;\r
+          }\r
+        }\r
+      }\r
+\r
+      return r;\r
+    }\r
+\r
+    function subtract(a, b, aL, base) {\r
+      var i = 0;\r
+\r
+      // Subtract b from a.\r
+      for (; aL--;) {\r
+        a[aL] -= i;\r
+        i = a[aL] < b[aL] ? 1 : 0;\r
+        a[aL] = i * base + a[aL] - b[aL];\r
+      }\r
+\r
+      // Remove leading zeros.\r
+      for (; !a[0] && a.length > 1;) a.shift();\r
+    }\r
+\r
+    return function (x, y, pr, rm, dp, base) {\r
+      var cmp, e, i, k, logBase, more, prod, prodL, q, qd, rem, remL, rem0, sd, t, xi, xL, yd0,\r
+        yL, yz,\r
+        Ctor = x.constructor,\r
+        sign = x.s == y.s ? 1 : -1,\r
+        xd = x.d,\r
+        yd = y.d;\r
+\r
+      // Either NaN, Infinity or 0?\r
+      if (!xd || !xd[0] || !yd || !yd[0]) {\r
+\r
+        return new Ctor(// Return NaN if either NaN, or both Infinity or 0.\r
+          !x.s || !y.s || (xd ? yd && xd[0] == yd[0] : !yd) ? NaN :\r
+\r
+          // Return ±0 if x is 0 or y is ±Infinity, or return ±Infinity as y is 0.\r
+          xd && xd[0] == 0 || !yd ? sign * 0 : sign / 0);\r
+      }\r
+\r
+      if (base) {\r
+        logBase = 1;\r
+        e = x.e - y.e;\r
+      } else {\r
+        base = BASE;\r
+        logBase = LOG_BASE;\r
+        e = mathfloor(x.e / logBase) - mathfloor(y.e / logBase);\r
+      }\r
+\r
+      yL = yd.length;\r
+      xL = xd.length;\r
+      q = new Ctor(sign);\r
+      qd = q.d = [];\r
+\r
+      // Result exponent may be one less than e.\r
+      // The digit array of a Decimal from toStringBinary may have trailing zeros.\r
+      for (i = 0; yd[i] == (xd[i] || 0); i++);\r
+\r
+      if (yd[i] > (xd[i] || 0)) e--;\r
+\r
+      if (pr == null) {\r
+        sd = pr = Ctor.precision;\r
+        rm = Ctor.rounding;\r
+      } else if (dp) {\r
+        sd = pr + (x.e - y.e) + 1;\r
+      } else {\r
+        sd = pr;\r
+      }\r
+\r
+      if (sd < 0) {\r
+        qd.push(1);\r
+        more = true;\r
+      } else {\r
+\r
+        // Convert precision in number of base 10 digits to base 1e7 digits.\r
+        sd = sd / logBase + 2 | 0;\r
+        i = 0;\r
+\r
+        // divisor < 1e7\r
+        if (yL == 1) {\r
+          k = 0;\r
+          yd = yd[0];\r
+          sd++;\r
+\r
+          // k is the carry.\r
+          for (; (i < xL || k) && sd--; i++) {\r
+            t = k * base + (xd[i] || 0);\r
+            qd[i] = t / yd | 0;\r
+            k = t % yd | 0;\r
+          }\r
+\r
+          more = k || i < xL;\r
+\r
+        // divisor >= 1e7\r
+        } else {\r
+\r
+          // Normalise xd and yd so highest order digit of yd is >= base/2\r
+          k = base / (yd[0] + 1) | 0;\r
+\r
+          if (k > 1) {\r
+            yd = multiplyInteger(yd, k, base);\r
+            xd = multiplyInteger(xd, k, base);\r
+            yL = yd.length;\r
+            xL = xd.length;\r
+          }\r
+\r
+          xi = yL;\r
+          rem = xd.slice(0, yL);\r
+          remL = rem.length;\r
+\r
+          // Add zeros to make remainder as long as divisor.\r
+          for (; remL < yL;) rem[remL++] = 0;\r
+\r
+          yz = yd.slice();\r
+          yz.unshift(0);\r
+          yd0 = yd[0];\r
+\r
+          if (yd[1] >= base / 2) ++yd0;\r
+\r
+          do {\r
+            k = 0;\r
+\r
+            // Compare divisor and remainder.\r
+            cmp = compare(yd, rem, yL, remL);\r
+\r
+            // If divisor < remainder.\r
+            if (cmp < 0) {\r
+\r
+              // Calculate trial digit, k.\r
+              rem0 = rem[0];\r
+              if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);\r
+\r
+              // k will be how many times the divisor goes into the current remainder.\r
+              k = rem0 / yd0 | 0;\r
+\r
+              //  Algorithm:\r
+              //  1. product = divisor * trial digit (k)\r
+              //  2. if product > remainder: product -= divisor, k--\r
+              //  3. remainder -= product\r
+              //  4. if product was < remainder at 2:\r
+              //    5. compare new remainder and divisor\r
+              //    6. If remainder > divisor: remainder -= divisor, k++\r
+\r
+              if (k > 1) {\r
+                if (k >= base) k = base - 1;\r
+\r
+                // product = divisor * trial digit.\r
+                prod = multiplyInteger(yd, k, base);\r
+                prodL = prod.length;\r
+                remL = rem.length;\r
+\r
+                // Compare product and remainder.\r
+                cmp = compare(prod, rem, prodL, remL);\r
+\r
+                // product > remainder.\r
+                if (cmp == 1) {\r
+                  k--;\r
+\r
+                  // Subtract divisor from product.\r
+                  subtract(prod, yL < prodL ? yz : yd, prodL, base);\r
+                }\r
+              } else {\r
+\r
+                // cmp is -1.\r
+                // If k is 0, there is no need to compare yd and rem again below, so change cmp to 1\r
+                // to avoid it. If k is 1 there is a need to compare yd and rem again below.\r
+                if (k == 0) cmp = k = 1;\r
+                prod = yd.slice();\r
+              }\r
+\r
+              prodL = prod.length;\r
+              if (prodL < remL) prod.unshift(0);\r
+\r
+              // Subtract product from remainder.\r
+              subtract(rem, prod, remL, base);\r
+\r
+              // If product was < previous remainder.\r
+              if (cmp == -1) {\r
+                remL = rem.length;\r
+\r
+                // Compare divisor and new remainder.\r
+                cmp = compare(yd, rem, yL, remL);\r
+\r
+                // If divisor < new remainder, subtract divisor from remainder.\r
+                if (cmp < 1) {\r
+                  k++;\r
+\r
+                  // Subtract divisor from remainder.\r
+                  subtract(rem, yL < remL ? yz : yd, remL, base);\r
+                }\r
+              }\r
+\r
+              remL = rem.length;\r
+            } else if (cmp === 0) {\r
+              k++;\r
+              rem = [0];\r
+            }    // if cmp === 1, k will be 0\r
+\r
+            // Add the next digit, k, to the result array.\r
+            qd[i++] = k;\r
+\r
+            // Update the remainder.\r
+            if (cmp && rem[0]) {\r
+              rem[remL++] = xd[xi] || 0;\r
+            } else {\r
+              rem = [xd[xi]];\r
+              remL = 1;\r
+            }\r
+\r
+          } while ((xi++ < xL || rem[0] !== void 0) && sd--);\r
+\r
+          more = rem[0] !== void 0;\r
+        }\r
+\r
+        // Leading zero?\r
+        if (!qd[0]) qd.shift();\r
+      }\r
+\r
+      // logBase is 1 when divide is being used for base conversion.\r
+      if (logBase == 1) {\r
+        q.e = e;\r
+        inexact = more;\r
+      } else {\r
+\r
+        // To calculate q.e, first get the number of digits of qd[0].\r
+        for (i = 1, k = qd[0]; k >= 10; k /= 10) i++;\r
+        q.e = i + e * logBase - 1;\r
+\r
+        finalise(q, dp ? pr + q.e + 1 : pr, rm, more);\r
+      }\r
+\r
+      return q;\r
+    };\r
+  })();\r
+\r
+\r
+  /*\r
+   * Round `x` to `sd` significant digits using rounding mode `rm`.\r
+   * Check for over/under-flow.\r
+   */\r
+   function finalise(x, sd, rm, isTruncated) {\r
+    var digits, i, j, k, rd, roundUp, w, xd, xdi,\r
+      Ctor = x.constructor;\r
+\r
+    // Don't round if sd is null or undefined.\r
+    out: if (sd != null) {\r
+      xd = x.d;\r
+\r
+      // Infinity/NaN.\r
+      if (!xd) return x;\r
+\r
+      // rd: the rounding digit, i.e. the digit after the digit that may be rounded up.\r
+      // w: the word of xd containing rd, a base 1e7 number.\r
+      // xdi: the index of w within xd.\r
+      // digits: the number of digits of w.\r
+      // i: what would be the index of rd within w if all the numbers were 7 digits long (i.e. if\r
+      // they had leading zeros)\r
+      // j: if > 0, the actual index of rd within w (if < 0, rd is a leading zero).\r
+\r
+      // Get the length of the first word of the digits array xd.\r
+      for (digits = 1, k = xd[0]; k >= 10; k /= 10) digits++;\r
+      i = sd - digits;\r
+\r
+      // Is the rounding digit in the first word of xd?\r
+      if (i < 0) {\r
+        i += LOG_BASE;\r
+        j = sd;\r
+        w = xd[xdi = 0];\r
+\r
+        // Get the rounding digit at index j of w.\r
+        rd = w / mathpow(10, digits - j - 1) % 10 | 0;\r
+      } else {\r
+        xdi = Math.ceil((i + 1) / LOG_BASE);\r
+        k = xd.length;\r
+        if (xdi >= k) {\r
+          if (isTruncated) {\r
+\r
+            // Needed by `naturalExponential`, `naturalLogarithm` and `squareRoot`.\r
+            for (; k++ <= xdi;) xd.push(0);\r
+            w = rd = 0;\r
+            digits = 1;\r
+            i %= LOG_BASE;\r
+            j = i - LOG_BASE + 1;\r
+          } else {\r
+            break out;\r
+          }\r
+        } else {\r
+          w = k = xd[xdi];\r
+\r
+          // Get the number of digits of w.\r
+          for (digits = 1; k >= 10; k /= 10) digits++;\r
+\r
+          // Get the index of rd within w.\r
+          i %= LOG_BASE;\r
+\r
+          // Get the index of rd within w, adjusted for leading zeros.\r
+          // The number of leading zeros of w is given by LOG_BASE - digits.\r
+          j = i - LOG_BASE + digits;\r
+\r
+          // Get the rounding digit at index j of w.\r
+          rd = j < 0 ? 0 : w / mathpow(10, digits - j - 1) % 10 | 0;\r
+        }\r
+      }\r
+\r
+      // Are there any non-zero digits after the rounding digit?\r
+      isTruncated = isTruncated || sd < 0 ||\r
+        xd[xdi + 1] !== void 0 || (j < 0 ? w : w % mathpow(10, digits - j - 1));\r
+\r
+      // The expression `w % mathpow(10, digits - j - 1)` returns all the digits of w to the right\r
+      // of the digit at (left-to-right) index j, e.g. if w is 908714 and j is 2, the expression\r
+      // will give 714.\r
+\r
+      roundUp = rm < 4\r
+        ? (rd || isTruncated) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r
+        : rd > 5 || rd == 5 && (rm == 4 || isTruncated || rm == 6 &&\r
+\r
+          // Check whether the digit to the left of the rounding digit is odd.\r
+          ((i > 0 ? j > 0 ? w / mathpow(10, digits - j) : 0 : xd[xdi - 1]) % 10) & 1 ||\r
+            rm == (x.s < 0 ? 8 : 7));\r
+\r
+      if (sd < 1 || !xd[0]) {\r
+        xd.length = 0;\r
+        if (roundUp) {\r
+\r
+          // Convert sd to decimal places.\r
+          sd -= x.e + 1;\r
+\r
+          // 1, 0.1, 0.01, 0.001, 0.0001 etc.\r
+          xd[0] = mathpow(10, (LOG_BASE - sd % LOG_BASE) % LOG_BASE);\r
+          x.e = -sd || 0;\r
+        } else {\r
+\r
+          // Zero.\r
+          xd[0] = x.e = 0;\r
+        }\r
+\r
+        return x;\r
+      }\r
+\r
+      // Remove excess digits.\r
+      if (i == 0) {\r
+        xd.length = xdi;\r
+        k = 1;\r
+        xdi--;\r
+      } else {\r
+        xd.length = xdi + 1;\r
+        k = mathpow(10, LOG_BASE - i);\r
+\r
+        // E.g. 56700 becomes 56000 if 7 is the rounding digit.\r
+        // j > 0 means i > number of leading zeros of w.\r
+        xd[xdi] = j > 0 ? (w / mathpow(10, digits - j) % mathpow(10, j) | 0) * k : 0;\r
+      }\r
+\r
+      if (roundUp) {\r
+        for (;;) {\r
+\r
+          // Is the digit to be rounded up in the first word of xd?\r
+          if (xdi == 0) {\r
+\r
+            // i will be the length of xd[0] before k is added.\r
+            for (i = 1, j = xd[0]; j >= 10; j /= 10) i++;\r
+            j = xd[0] += k;\r
+            for (k = 1; j >= 10; j /= 10) k++;\r
+\r
+            // if i != k the length has increased.\r
+            if (i != k) {\r
+              x.e++;\r
+              if (xd[0] == BASE) xd[0] = 1;\r
+            }\r
+\r
+            break;\r
+          } else {\r
+            xd[xdi] += k;\r
+            if (xd[xdi] != BASE) break;\r
+            xd[xdi--] = 0;\r
+            k = 1;\r
+          }\r
+        }\r
+      }\r
+\r
+      // Remove trailing zeros.\r
+      for (i = xd.length; xd[--i] === 0;) xd.pop();\r
+    }\r
+\r
+    if (external) {\r
+\r
+      // Overflow?\r
+      if (x.e > Ctor.maxE) {\r
+\r
+        // Infinity.\r
+        x.d = null;\r
+        x.e = NaN;\r
+\r
+      // Underflow?\r
+      } else if (x.e < Ctor.minE) {\r
+\r
+        // Zero.\r
+        x.e = 0;\r
+        x.d = [0];\r
+        // Ctor.underflow = true;\r
+      } // else Ctor.underflow = false;\r
+    }\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  function finiteToString(x, isExp, sd) {\r
+    if (!x.isFinite()) return nonFiniteToString(x);\r
+    var k,\r
+      e = x.e,\r
+      str = digitsToString(x.d),\r
+      len = str.length;\r
+\r
+    if (isExp) {\r
+      if (sd && (k = sd - len) > 0) {\r
+        str = str.charAt(0) + '.' + str.slice(1) + getZeroString(k);\r
+      } else if (len > 1) {\r
+        str = str.charAt(0) + '.' + str.slice(1);\r
+      }\r
+\r
+      str = str + (x.e < 0 ? 'e' : 'e+') + x.e;\r
+    } else if (e < 0) {\r
+      str = '0.' + getZeroString(-e - 1) + str;\r
+      if (sd && (k = sd - len) > 0) str += getZeroString(k);\r
+    } else if (e >= len) {\r
+      str += getZeroString(e + 1 - len);\r
+      if (sd && (k = sd - e - 1) > 0) str = str + '.' + getZeroString(k);\r
+    } else {\r
+      if ((k = e + 1) < len) str = str.slice(0, k) + '.' + str.slice(k);\r
+      if (sd && (k = sd - len) > 0) {\r
+        if (e + 1 === len) str += '.';\r
+        str += getZeroString(k);\r
+      }\r
+    }\r
+\r
+    return str;\r
+  }\r
+\r
+\r
+  // Calculate the base 10 exponent from the base 1e7 exponent.\r
+  function getBase10Exponent(digits, e) {\r
+    var w = digits[0];\r
+\r
+    // Add the number of digits of the first word of the digits array.\r
+    for ( e *= LOG_BASE; w >= 10; w /= 10) e++;\r
+    return e;\r
+  }\r
+\r
+\r
+  function getLn10(Ctor, sd, pr) {\r
+    if (sd > LN10_PRECISION) {\r
+\r
+      // Reset global state in case the exception is caught.\r
+      external = true;\r
+      if (pr) Ctor.precision = pr;\r
+      throw Error(precisionLimitExceeded);\r
+    }\r
+    return finalise(new Ctor(LN10), sd, 1, true);\r
+  }\r
+\r
+\r
+  function getPi(Ctor, sd, rm) {\r
+    if (sd > PI_PRECISION) throw Error(precisionLimitExceeded);\r
+    return finalise(new Ctor(PI), sd, rm, true);\r
+  }\r
+\r
+\r
+  function getPrecision(digits) {\r
+    var w = digits.length - 1,\r
+      len = w * LOG_BASE + 1;\r
+\r
+    w = digits[w];\r
+\r
+    // If non-zero...\r
+    if (w) {\r
+\r
+      // Subtract the number of trailing zeros of the last word.\r
+      for (; w % 10 == 0; w /= 10) len--;\r
+\r
+      // Add the number of digits of the first word.\r
+      for (w = digits[0]; w >= 10; w /= 10) len++;\r
+    }\r
+\r
+    return len;\r
+  }\r
+\r
+\r
+  function getZeroString(k) {\r
+    var zs = '';\r
+    for (; k--;) zs += '0';\r
+    return zs;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the value of Decimal `x` to the power `n`, where `n` is an\r
+   * integer of type number.\r
+   *\r
+   * Implements 'exponentiation by squaring'. Called by `pow` and `parseOther`.\r
+   *\r
+   */\r
+  function intPow(Ctor, x, n, pr) {\r
+    var isTruncated,\r
+      r = new Ctor(1),\r
+\r
+      // Max n of 9007199254740991 takes 53 loop iterations.\r
+      // Maximum digits array length; leaves [28, 34] guard digits.\r
+      k = Math.ceil(pr / LOG_BASE + 4);\r
+\r
+    external = false;\r
+\r
+    for (;;) {\r
+      if (n % 2) {\r
+        r = r.times(x);\r
+        if (truncate(r.d, k)) isTruncated = true;\r
+      }\r
+\r
+      n = mathfloor(n / 2);\r
+      if (n === 0) {\r
+\r
+        // To ensure correct rounding when r.d is truncated, increment the last word if it is zero.\r
+        n = r.d.length - 1;\r
+        if (isTruncated && r.d[n] === 0) ++r.d[n];\r
+        break;\r
+      }\r
+\r
+      x = x.times(x);\r
+      truncate(x.d, k);\r
+    }\r
+\r
+    external = true;\r
+\r
+    return r;\r
+  }\r
+\r
+\r
+  function isOdd(n) {\r
+    return n.d[n.d.length - 1] & 1;\r
+  }\r
+\r
+\r
+  /*\r
+   * Handle `max` (`n` is -1) and `min` (`n` is 1).\r
+   */\r
+  function maxOrMin(Ctor, args, n) {\r
+    var k, y,\r
+      x = new Ctor(args[0]),\r
+      i = 0;\r
+\r
+    for (; ++i < args.length;) {\r
+      y = new Ctor(args[i]);\r
+\r
+      // NaN?\r
+      if (!y.s) {\r
+        x = y;\r
+        break;\r
+      }\r
+\r
+      k = x.cmp(y);\r
+\r
+      if (k === n || k === 0 && x.s === n) {\r
+        x = y;\r
+      }\r
+    }\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural exponential of `x` rounded to `sd` significant\r
+   * digits.\r
+   *\r
+   * Taylor/Maclaurin series.\r
+   *\r
+   * exp(x) = x^0/0! + x^1/1! + x^2/2! + x^3/3! + ...\r
+   *\r
+   * Argument reduction:\r
+   *   Repeat x = x / 32, k += 5, until |x| < 0.1\r
+   *   exp(x) = exp(x / 2^k)^(2^k)\r
+   *\r
+   * Previously, the argument was initially reduced by\r
+   * exp(x) = exp(r) * 10^k  where r = x - k * ln10, k = floor(x / ln10)\r
+   * to first put r in the range [0, ln10], before dividing by 32 until |x| < 0.1, but this was\r
+   * found to be slower than just dividing repeatedly by 32 as above.\r
+   *\r
+   * Max integer argument: exp('20723265836946413') = 6.3e+9000000000000000\r
+   * Min integer argument: exp('-20723265836946411') = 1.2e-9000000000000000\r
+   * (Math object integer min/max: Math.exp(709) = 8.2e+307, Math.exp(-745) = 5e-324)\r
+   *\r
+   *  exp(Infinity)  = Infinity\r
+   *  exp(-Infinity) = 0\r
+   *  exp(NaN)       = NaN\r
+   *  exp(±0)        = 1\r
+   *\r
+   *  exp(x) is non-terminating for any finite, non-zero x.\r
+   *\r
+   *  The result will always be correctly rounded.\r
+   *\r
+   */\r
+  function naturalExponential(x, sd) {\r
+    var denominator, guard, j, pow, sum, t, wpr,\r
+      rep = 0,\r
+      i = 0,\r
+      k = 0,\r
+      Ctor = x.constructor,\r
+      rm = Ctor.rounding,\r
+      pr = Ctor.precision;\r
+\r
+    // 0/NaN/Infinity?\r
+    if (!x.d || !x.d[0] || x.e > 17) {\r
+\r
+      return new Ctor(x.d\r
+        ? !x.d[0] ? 1 : x.s < 0 ? 0 : 1 / 0\r
+        : x.s ? x.s < 0 ? 0 : x : 0 / 0);\r
+    }\r
+\r
+    if (sd == null) {\r
+      external = false;\r
+      wpr = pr;\r
+    } else {\r
+      wpr = sd;\r
+    }\r
+\r
+    t = new Ctor(0.03125);\r
+\r
+    // while abs(x) >= 0.1\r
+    while (x.e > -2) {\r
+\r
+      // x = x / 2^5\r
+      x = x.times(t);\r
+      k += 5;\r
+    }\r
+\r
+    // Use 2 * log10(2^k) + 5 (empirically derived) to estimate the increase in precision\r
+    // necessary to ensure the first 4 rounding digits are correct.\r
+    guard = Math.log(mathpow(2, k)) / Math.LN10 * 2 + 5 | 0;\r
+    wpr += guard;\r
+    denominator = pow = sum = new Ctor(1);\r
+    Ctor.precision = wpr;\r
+\r
+    for (;;) {\r
+      pow = finalise(pow.times(x), wpr, 1);\r
+      denominator = denominator.times(++i);\r
+      t = sum.plus(divide(pow, denominator, wpr, 1));\r
+\r
+      if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {\r
+        j = k;\r
+        while (j--) sum = finalise(sum.times(sum), wpr, 1);\r
+\r
+        // Check to see if the first 4 rounding digits are [49]999.\r
+        // If so, repeat the summation with a higher precision, otherwise\r
+        // e.g. with precision: 18, rounding: 1\r
+        // exp(18.404272462595034083567793919843761) = 98372560.1229999999 (should be 98372560.123)\r
+        // `wpr - guard` is the index of first rounding digit.\r
+        if (sd == null) {\r
+\r
+          if (rep < 3 && checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {\r
+            Ctor.precision = wpr += 10;\r
+            denominator = pow = t = new Ctor(1);\r
+            i = 0;\r
+            rep++;\r
+          } else {\r
+            return finalise(sum, Ctor.precision = pr, rm, external = true);\r
+          }\r
+        } else {\r
+          Ctor.precision = pr;\r
+          return sum;\r
+        }\r
+      }\r
+\r
+      sum = t;\r
+    }\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural logarithm of `x` rounded to `sd` significant\r
+   * digits.\r
+   *\r
+   *  ln(-n)        = NaN\r
+   *  ln(0)         = -Infinity\r
+   *  ln(-0)        = -Infinity\r
+   *  ln(1)         = 0\r
+   *  ln(Infinity)  = Infinity\r
+   *  ln(-Infinity) = NaN\r
+   *  ln(NaN)       = NaN\r
+   *\r
+   *  ln(n) (n != 1) is non-terminating.\r
+   *\r
+   */\r
+  function naturalLogarithm(y, sd) {\r
+    var c, c0, denominator, e, numerator, rep, sum, t, wpr, x1, x2,\r
+      n = 1,\r
+      guard = 10,\r
+      x = y,\r
+      xd = x.d,\r
+      Ctor = x.constructor,\r
+      rm = Ctor.rounding,\r
+      pr = Ctor.precision;\r
+\r
+    // Is x negative or Infinity, NaN, 0 or 1?\r
+    if (x.s < 0 || !xd || !xd[0] || !x.e && xd[0] == 1 && xd.length == 1) {\r
+      return new Ctor(xd && !xd[0] ? -1 / 0 : x.s != 1 ? NaN : xd ? 0 : x);\r
+    }\r
+\r
+    if (sd == null) {\r
+      external = false;\r
+      wpr = pr;\r
+    } else {\r
+      wpr = sd;\r
+    }\r
+\r
+    Ctor.precision = wpr += guard;\r
+    c = digitsToString(xd);\r
+    c0 = c.charAt(0);\r
+\r
+    if (Math.abs(e = x.e) < 1.5e15) {\r
+\r
+      // Argument reduction.\r
+      // The series converges faster the closer the argument is to 1, so using\r
+      // ln(a^b) = b * ln(a),   ln(a) = ln(a^b) / b\r
+      // multiply the argument by itself until the leading digits of the significand are 7, 8, 9,\r
+      // 10, 11, 12 or 13, recording the number of multiplications so the sum of the series can\r
+      // later be divided by this number, then separate out the power of 10 using\r
+      // ln(a*10^b) = ln(a) + b*ln(10).\r
+\r
+      // max n is 21 (gives 0.9, 1.0 or 1.1) (9e15 / 21 = 4.2e14).\r
+      //while (c0 < 9 && c0 != 1 || c0 == 1 && c.charAt(1) > 1) {\r
+      // max n is 6 (gives 0.7 - 1.3)\r
+      while (c0 < 7 && c0 != 1 || c0 == 1 && c.charAt(1) > 3) {\r
+        x = x.times(y);\r
+        c = digitsToString(x.d);\r
+        c0 = c.charAt(0);\r
+        n++;\r
+      }\r
+\r
+      e = x.e;\r
+\r
+      if (c0 > 1) {\r
+        x = new Ctor('0.' + c);\r
+        e++;\r
+      } else {\r
+        x = new Ctor(c0 + '.' + c.slice(1));\r
+      }\r
+    } else {\r
+\r
+      // The argument reduction method above may result in overflow if the argument y is a massive\r
+      // number with exponent >= 1500000000000000 (9e15 / 6 = 1.5e15), so instead recall this\r
+      // function using ln(x*10^e) = ln(x) + e*ln(10).\r
+      t = getLn10(Ctor, wpr + 2, pr).times(e + '');\r
+      x = naturalLogarithm(new Ctor(c0 + '.' + c.slice(1)), wpr - guard).plus(t);\r
+      Ctor.precision = pr;\r
+\r
+      return sd == null ? finalise(x, pr, rm, external = true) : x;\r
+    }\r
+\r
+    // x1 is x reduced to a value near 1.\r
+    x1 = x;\r
+\r
+    // Taylor series.\r
+    // ln(y) = ln((1 + x)/(1 - x)) = 2(x + x^3/3 + x^5/5 + x^7/7 + ...)\r
+    // where x = (y - 1)/(y + 1)    (|x| < 1)\r
+    sum = numerator = x = divide(x.minus(1), x.plus(1), wpr, 1);\r
+    x2 = finalise(x.times(x), wpr, 1);\r
+    denominator = 3;\r
+\r
+    for (;;) {\r
+      numerator = finalise(numerator.times(x2), wpr, 1);\r
+      t = sum.plus(divide(numerator, new Ctor(denominator), wpr, 1));\r
+\r
+      if (digitsToString(t.d).slice(0, wpr) === digitsToString(sum.d).slice(0, wpr)) {\r
+        sum = sum.times(2);\r
+\r
+        // Reverse the argument reduction. Check that e is not 0 because, besides preventing an\r
+        // unnecessary calculation, -0 + 0 = +0 and to ensure correct rounding -0 needs to stay -0.\r
+        if (e !== 0) sum = sum.plus(getLn10(Ctor, wpr + 2, pr).times(e + ''));\r
+        sum = divide(sum, new Ctor(n), wpr, 1);\r
+\r
+        // Is rm > 3 and the first 4 rounding digits 4999, or rm < 4 (or the summation has\r
+        // been repeated previously) and the first 4 rounding digits 9999?\r
+        // If so, restart the summation with a higher precision, otherwise\r
+        // e.g. with precision: 12, rounding: 1\r
+        // ln(135520028.6126091714265381533) = 18.7246299999 when it should be 18.72463.\r
+        // `wpr - guard` is the index of first rounding digit.\r
+        if (sd == null) {\r
+          if (checkRoundingDigits(sum.d, wpr - guard, rm, rep)) {\r
+            Ctor.precision = wpr += guard;\r
+            t = numerator = x = divide(x1.minus(1), x1.plus(1), wpr, 1);\r
+            x2 = finalise(x.times(x), wpr, 1);\r
+            denominator = rep = 1;\r
+          } else {\r
+            return finalise(sum, Ctor.precision = pr, rm, external = true);\r
+          }\r
+        } else {\r
+          Ctor.precision = pr;\r
+          return sum;\r
+        }\r
+      }\r
+\r
+      sum = t;\r
+      denominator += 2;\r
+    }\r
+  }\r
+\r
+\r
+  // ±Infinity, NaN.\r
+  function nonFiniteToString(x) {\r
+    // Unsigned.\r
+    return String(x.s * x.s / 0);\r
+  }\r
+\r
+\r
+  /*\r
+   * Parse the value of a new Decimal `x` from string `str`.\r
+   */\r
+  function parseDecimal(x, str) {\r
+    var e, i, len;\r
+\r
+    // TODO BigInt str: no need to check for decimal point, exponential form or leading zeros.\r
+\r
+    // Decimal point?\r
+    if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r
+\r
+    // Exponential form?\r
+    if ((i = str.search(/e/i)) > 0) {\r
+\r
+      // Determine exponent.\r
+      if (e < 0) e = i;\r
+      e += +str.slice(i + 1);\r
+      str = str.substring(0, i);\r
+    } else if (e < 0) {\r
+\r
+      // Integer.\r
+      e = str.length;\r
+    }\r
+\r
+    // Determine leading zeros.\r
+    for (i = 0; str.charCodeAt(i) === 48; i++);\r
+\r
+    // Determine trailing zeros.\r
+    for (len = str.length; str.charCodeAt(len - 1) === 48; --len);\r
+    str = str.slice(i, len);\r
+\r
+    if (str) {\r
+      len -= i;\r
+      x.e = e = e - i - 1;\r
+      x.d = [];\r
+\r
+      // Transform base\r
+\r
+      // e is the base 10 exponent.\r
+      // i is where to slice str to get the first word of the digits array.\r
+      i = (e + 1) % LOG_BASE;\r
+      if (e < 0) i += LOG_BASE;\r
+\r
+      if (i < len) {\r
+        if (i) x.d.push(+str.slice(0, i));\r
+        for (len -= LOG_BASE; i < len;) x.d.push(+str.slice(i, i += LOG_BASE));\r
+        str = str.slice(i);\r
+        i = LOG_BASE - str.length;\r
+      } else {\r
+        i -= len;\r
+      }\r
+\r
+      for (; i--;) str += '0';\r
+      x.d.push(+str);\r
+\r
+      if (external) {\r
+\r
+        // Overflow?\r
+        if (x.e > x.constructor.maxE) {\r
+\r
+          // Infinity.\r
+          x.d = null;\r
+          x.e = NaN;\r
+\r
+        // Underflow?\r
+        } else if (x.e < x.constructor.minE) {\r
+\r
+          // Zero.\r
+          x.e = 0;\r
+          x.d = [0];\r
+          // x.constructor.underflow = true;\r
+        } // else x.constructor.underflow = false;\r
+      }\r
+    } else {\r
+\r
+      // Zero.\r
+      x.e = 0;\r
+      x.d = [0];\r
+    }\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  /*\r
+   * Parse the value of a new Decimal `x` from a string `str`, which is not a decimal value.\r
+   */\r
+  function parseOther(x, str) {\r
+    var base, Ctor, divisor, i, isFloat, len, p, xd, xe;\r
+\r
+    if (str.indexOf('_') > -1) {\r
+      str = str.replace(/(\d)_(?=\d)/g, '$1');\r
+      if (isDecimal.test(str)) return parseDecimal(x, str);\r
+    } else if (str === 'Infinity' || str === 'NaN') {\r
+      if (!+str) x.s = NaN;\r
+      x.e = NaN;\r
+      x.d = null;\r
+      return x;\r
+    }\r
+\r
+    if (isHex.test(str))  {\r
+      base = 16;\r
+      str = str.toLowerCase();\r
+    } else if (isBinary.test(str))  {\r
+      base = 2;\r
+    } else if (isOctal.test(str))  {\r
+      base = 8;\r
+    } else {\r
+      throw Error(invalidArgument + str);\r
+    }\r
+\r
+    // Is there a binary exponent part?\r
+    i = str.search(/p/i);\r
+\r
+    if (i > 0) {\r
+      p = +str.slice(i + 1);\r
+      str = str.substring(2, i);\r
+    } else {\r
+      str = str.slice(2);\r
+    }\r
+\r
+    // Convert `str` as an integer then divide the result by `base` raised to a power such that the\r
+    // fraction part will be restored.\r
+    i = str.indexOf('.');\r
+    isFloat = i >= 0;\r
+    Ctor = x.constructor;\r
+\r
+    if (isFloat) {\r
+      str = str.replace('.', '');\r
+      len = str.length;\r
+      i = len - i;\r
+\r
+      // log[10](16) = 1.2041... , log[10](88) = 1.9444....\r
+      divisor = intPow(Ctor, new Ctor(base), i, i * 2);\r
+    }\r
+\r
+    xd = convertBase(str, base, BASE);\r
+    xe = xd.length - 1;\r
+\r
+    // Remove trailing zeros.\r
+    for (i = xe; xd[i] === 0; --i) xd.pop();\r
+    if (i < 0) return new Ctor(x.s * 0);\r
+    x.e = getBase10Exponent(xd, xe);\r
+    x.d = xd;\r
+    external = false;\r
+\r
+    // At what precision to perform the division to ensure exact conversion?\r
+    // maxDecimalIntegerPartDigitCount = ceil(log[10](b) * otherBaseIntegerPartDigitCount)\r
+    // log[10](2) = 0.30103, log[10](8) = 0.90309, log[10](16) = 1.20412\r
+    // E.g. ceil(1.2 * 3) = 4, so up to 4 decimal digits are needed to represent 3 hex int digits.\r
+    // maxDecimalFractionPartDigitCount = {Hex:4|Oct:3|Bin:1} * otherBaseFractionPartDigitCount\r
+    // Therefore using 4 * the number of digits of str will always be enough.\r
+    if (isFloat) x = divide(x, divisor, len * 4);\r
+\r
+    // Multiply by the binary exponent part if present.\r
+    if (p) x = x.times(Math.abs(p) < 54 ? mathpow(2, p) : Decimal.pow(2, p));\r
+    external = true;\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  /*\r
+   * sin(x) = x - x^3/3! + x^5/5! - ...\r
+   * |x| < pi/2\r
+   *\r
+   */\r
+  function sine(Ctor, x) {\r
+    var k,\r
+      len = x.d.length;\r
+\r
+    if (len < 3) {\r
+      return x.isZero() ? x : taylorSeries(Ctor, 2, x, x);\r
+    }\r
+\r
+    // Argument reduction: sin(5x) = 16*sin^5(x) - 20*sin^3(x) + 5*sin(x)\r
+    // i.e. sin(x) = 16*sin^5(x/5) - 20*sin^3(x/5) + 5*sin(x/5)\r
+    // and  sin(x) = sin(x/5)(5 + sin^2(x/5)(16sin^2(x/5) - 20))\r
+\r
+    // Estimate the optimum number of times to use the argument reduction.\r
+    k = 1.4 * Math.sqrt(len);\r
+    k = k > 16 ? 16 : k | 0;\r
+\r
+    x = x.times(1 / tinyPow(5, k));\r
+    x = taylorSeries(Ctor, 2, x, x);\r
+\r
+    // Reverse argument reduction\r
+    var sin2_x,\r
+      d5 = new Ctor(5),\r
+      d16 = new Ctor(16),\r
+      d20 = new Ctor(20);\r
+    for (; k--;) {\r
+      sin2_x = x.times(x);\r
+      x = x.times(d5.plus(sin2_x.times(d16.times(sin2_x).minus(d20))));\r
+    }\r
+\r
+    return x;\r
+  }\r
+\r
+\r
+  // Calculate Taylor series for `cos`, `cosh`, `sin` and `sinh`.\r
+  function taylorSeries(Ctor, n, x, y, isHyperbolic) {\r
+    var j, t, u, x2,\r
+      i = 1,\r
+      pr = Ctor.precision,\r
+      k = Math.ceil(pr / LOG_BASE);\r
+\r
+    external = false;\r
+    x2 = x.times(x);\r
+    u = new Ctor(y);\r
+\r
+    for (;;) {\r
+      t = divide(u.times(x2), new Ctor(n++ * n++), pr, 1);\r
+      u = isHyperbolic ? y.plus(t) : y.minus(t);\r
+      y = divide(t.times(x2), new Ctor(n++ * n++), pr, 1);\r
+      t = u.plus(y);\r
+\r
+      if (t.d[k] !== void 0) {\r
+        for (j = k; t.d[j] === u.d[j] && j--;);\r
+        if (j == -1) break;\r
+      }\r
+\r
+      j = u;\r
+      u = y;\r
+      y = t;\r
+      t = j;\r
+      i++;\r
+    }\r
+\r
+    external = true;\r
+    t.d.length = k + 1;\r
+\r
+    return t;\r
+  }\r
+\r
+\r
+  // Exponent e must be positive and non-zero.\r
+  function tinyPow(b, e) {\r
+    var n = b;\r
+    while (--e) n *= b;\r
+    return n;\r
+  }\r
+\r
+\r
+  // Return the absolute value of `x` reduced to less than or equal to half pi.\r
+  function toLessThanHalfPi(Ctor, x) {\r
+    var t,\r
+      isNeg = x.s < 0,\r
+      pi = getPi(Ctor, Ctor.precision, 1),\r
+      halfPi = pi.times(0.5);\r
+\r
+    x = x.abs();\r
+\r
+    if (x.lte(halfPi)) {\r
+      quadrant = isNeg ? 4 : 1;\r
+      return x;\r
+    }\r
+\r
+    t = x.divToInt(pi);\r
+\r
+    if (t.isZero()) {\r
+      quadrant = isNeg ? 3 : 2;\r
+    } else {\r
+      x = x.minus(t.times(pi));\r
+\r
+      // 0 <= x < pi\r
+      if (x.lte(halfPi)) {\r
+        quadrant = isOdd(t) ? (isNeg ? 2 : 3) : (isNeg ? 4 : 1);\r
+        return x;\r
+      }\r
+\r
+      quadrant = isOdd(t) ? (isNeg ? 1 : 4) : (isNeg ? 3 : 2);\r
+    }\r
+\r
+    return x.minus(pi).abs();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return the value of Decimal `x` as a string in base `baseOut`.\r
+   *\r
+   * If the optional `sd` argument is present include a binary exponent suffix.\r
+   */\r
+  function toStringBinary(x, baseOut, sd, rm) {\r
+    var base, e, i, k, len, roundUp, str, xd, y,\r
+      Ctor = x.constructor,\r
+      isExp = sd !== void 0;\r
+\r
+    if (isExp) {\r
+      checkInt32(sd, 1, MAX_DIGITS);\r
+      if (rm === void 0) rm = Ctor.rounding;\r
+      else checkInt32(rm, 0, 8);\r
+    } else {\r
+      sd = Ctor.precision;\r
+      rm = Ctor.rounding;\r
+    }\r
+\r
+    if (!x.isFinite()) {\r
+      str = nonFiniteToString(x);\r
+    } else {\r
+      str = finiteToString(x);\r
+      i = str.indexOf('.');\r
+\r
+      // Use exponential notation according to `toExpPos` and `toExpNeg`? No, but if required:\r
+      // maxBinaryExponent = floor((decimalExponent + 1) * log[2](10))\r
+      // minBinaryExponent = floor(decimalExponent * log[2](10))\r
+      // log[2](10) = 3.321928094887362347870319429489390175864\r
+\r
+      if (isExp) {\r
+        base = 2;\r
+        if (baseOut == 16) {\r
+          sd = sd * 4 - 3;\r
+        } else if (baseOut == 8) {\r
+          sd = sd * 3 - 2;\r
+        }\r
+      } else {\r
+        base = baseOut;\r
+      }\r
+\r
+      // Convert the number as an integer then divide the result by its base raised to a power such\r
+      // that the fraction part will be restored.\r
+\r
+      // Non-integer.\r
+      if (i >= 0) {\r
+        str = str.replace('.', '');\r
+        y = new Ctor(1);\r
+        y.e = str.length - i;\r
+        y.d = convertBase(finiteToString(y), 10, base);\r
+        y.e = y.d.length;\r
+      }\r
+\r
+      xd = convertBase(str, 10, base);\r
+      e = len = xd.length;\r
+\r
+      // Remove trailing zeros.\r
+      for (; xd[--len] == 0;) xd.pop();\r
+\r
+      if (!xd[0]) {\r
+        str = isExp ? '0p+0' : '0';\r
+      } else {\r
+        if (i < 0) {\r
+          e--;\r
+        } else {\r
+          x = new Ctor(x);\r
+          x.d = xd;\r
+          x.e = e;\r
+          x = divide(x, y, sd, rm, 0, base);\r
+          xd = x.d;\r
+          e = x.e;\r
+          roundUp = inexact;\r
+        }\r
+\r
+        // The rounding digit, i.e. the digit after the digit that may be rounded up.\r
+        i = xd[sd];\r
+        k = base / 2;\r
+        roundUp = roundUp || xd[sd + 1] !== void 0;\r
+\r
+        roundUp = rm < 4\r
+          ? (i !== void 0 || roundUp) && (rm === 0 || rm === (x.s < 0 ? 3 : 2))\r
+          : i > k || i === k && (rm === 4 || roundUp || rm === 6 && xd[sd - 1] & 1 ||\r
+            rm === (x.s < 0 ? 8 : 7));\r
+\r
+        xd.length = sd;\r
+\r
+        if (roundUp) {\r
+\r
+          // Rounding up may mean the previous digit has to be rounded up and so on.\r
+          for (; ++xd[--sd] > base - 1;) {\r
+            xd[sd] = 0;\r
+            if (!sd) {\r
+              ++e;\r
+              xd.unshift(1);\r
+            }\r
+          }\r
+        }\r
+\r
+        // Determine trailing zeros.\r
+        for (len = xd.length; !xd[len - 1]; --len);\r
+\r
+        // E.g. [4, 11, 15] becomes 4bf.\r
+        for (i = 0, str = ''; i < len; i++) str += NUMERALS.charAt(xd[i]);\r
+\r
+        // Add binary exponent suffix?\r
+        if (isExp) {\r
+          if (len > 1) {\r
+            if (baseOut == 16 || baseOut == 8) {\r
+              i = baseOut == 16 ? 4 : 3;\r
+              for (--len; len % i; len++) str += '0';\r
+              xd = convertBase(str, base, baseOut);\r
+              for (len = xd.length; !xd[len - 1]; --len);\r
+\r
+              // xd[0] will always be be 1\r
+              for (i = 1, str = '1.'; i < len; i++) str += NUMERALS.charAt(xd[i]);\r
+            } else {\r
+              str = str.charAt(0) + '.' + str.slice(1);\r
+            }\r
+          }\r
+\r
+          str =  str + (e < 0 ? 'p' : 'p+') + e;\r
+        } else if (e < 0) {\r
+          for (; ++e;) str = '0' + str;\r
+          str = '0.' + str;\r
+        } else {\r
+          if (++e > len) for (e -= len; e-- ;) str += '0';\r
+          else if (e < len) str = str.slice(0, e) + '.' + str.slice(e);\r
+        }\r
+      }\r
+\r
+      str = (baseOut == 16 ? '0x' : baseOut == 2 ? '0b' : baseOut == 8 ? '0o' : '') + str;\r
+    }\r
+\r
+    return x.s < 0 ? '-' + str : str;\r
+  }\r
+\r
+\r
+  // Does not strip trailing zeros.\r
+  function truncate(arr, len) {\r
+    if (arr.length > len) {\r
+      arr.length = len;\r
+      return true;\r
+    }\r
+  }\r
+\r
+\r
+  // Decimal methods\r
+\r
+\r
+  /*\r
+   *  abs\r
+   *  acos\r
+   *  acosh\r
+   *  add\r
+   *  asin\r
+   *  asinh\r
+   *  atan\r
+   *  atanh\r
+   *  atan2\r
+   *  cbrt\r
+   *  ceil\r
+   *  clamp\r
+   *  clone\r
+   *  config\r
+   *  cos\r
+   *  cosh\r
+   *  div\r
+   *  exp\r
+   *  floor\r
+   *  hypot\r
+   *  ln\r
+   *  log\r
+   *  log2\r
+   *  log10\r
+   *  max\r
+   *  min\r
+   *  mod\r
+   *  mul\r
+   *  pow\r
+   *  random\r
+   *  round\r
+   *  set\r
+   *  sign\r
+   *  sin\r
+   *  sinh\r
+   *  sqrt\r
+   *  sub\r
+   *  sum\r
+   *  tan\r
+   *  tanh\r
+   *  trunc\r
+   */\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the absolute value of `x`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function abs(x) {\r
+    return new this(x).abs();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arccosine in radians of `x`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function acos(x) {\r
+    return new this(x).acos();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic cosine of `x`, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function acosh(x) {\r
+    return new this(x).acosh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the sum of `x` and `y`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * y {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function add(x, y) {\r
+    return new this(x).plus(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arcsine in radians of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function asin(x) {\r
+    return new this(x).asin();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic sine of `x`, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function asinh(x) {\r
+    return new this(x).asinh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arctangent in radians of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function atan(x) {\r
+    return new this(x).atan();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the inverse of the hyperbolic tangent of `x`, rounded to\r
+   * `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function atanh(x) {\r
+    return new this(x).atanh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the arctangent in radians of `y/x` in the range -pi to pi\r
+   * (inclusive), rounded to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * Domain: [-Infinity, Infinity]\r
+   * Range: [-pi, pi]\r
+   *\r
+   * y {number|string|bigint|Decimal} The y-coordinate.\r
+   * x {number|string|bigint|Decimal} The x-coordinate.\r
+   *\r
+   * atan2(±0, -0)               = ±pi\r
+   * atan2(±0, +0)               = ±0\r
+   * atan2(±0, -x)               = ±pi for x > 0\r
+   * atan2(±0, x)                = ±0 for x > 0\r
+   * atan2(-y, ±0)               = -pi/2 for y > 0\r
+   * atan2(y, ±0)                = pi/2 for y > 0\r
+   * atan2(±y, -Infinity)        = ±pi for finite y > 0\r
+   * atan2(±y, +Infinity)        = ±0 for finite y > 0\r
+   * atan2(±Infinity, x)         = ±pi/2 for finite x\r
+   * atan2(±Infinity, -Infinity) = ±3*pi/4\r
+   * atan2(±Infinity, +Infinity) = ±pi/4\r
+   * atan2(NaN, x) = NaN\r
+   * atan2(y, NaN) = NaN\r
+   *\r
+   */\r
+  function atan2(y, x) {\r
+    y = new this(y);\r
+    x = new this(x);\r
+    var r,\r
+      pr = this.precision,\r
+      rm = this.rounding,\r
+      wpr = pr + 4;\r
+\r
+    // Either NaN\r
+    if (!y.s || !x.s) {\r
+      r = new this(NaN);\r
+\r
+    // Both ±Infinity\r
+    } else if (!y.d && !x.d) {\r
+      r = getPi(this, wpr, 1).times(x.s > 0 ? 0.25 : 0.75);\r
+      r.s = y.s;\r
+\r
+    // x is ±Infinity or y is ±0\r
+    } else if (!x.d || y.isZero()) {\r
+      r = x.s < 0 ? getPi(this, pr, rm) : new this(0);\r
+      r.s = y.s;\r
+\r
+    // y is ±Infinity or x is ±0\r
+    } else if (!y.d || x.isZero()) {\r
+      r = getPi(this, wpr, 1).times(0.5);\r
+      r.s = y.s;\r
+\r
+    // Both non-zero and finite\r
+    } else if (x.s < 0) {\r
+      this.precision = wpr;\r
+      this.rounding = 1;\r
+      r = this.atan(divide(y, x, wpr, 1));\r
+      x = getPi(this, wpr, 1);\r
+      this.precision = pr;\r
+      this.rounding = rm;\r
+      r = y.s < 0 ? r.minus(x) : r.plus(x);\r
+    } else {\r
+      r = this.atan(divide(y, x, wpr, 1));\r
+    }\r
+\r
+    return r;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the cube root of `x`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function cbrt(x) {\r
+    return new this(x).cbrt();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` rounded to an integer using `ROUND_CEIL`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function ceil(x) {\r
+    return finalise(x = new this(x), x.e + 1, 2);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` clamped to the range delineated by `min` and `max`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * min {number|string|bigint|Decimal}\r
+   * max {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function clamp(x, min, max) {\r
+    return new this(x).clamp(min, max);\r
+  }\r
+\r
+\r
+  /*\r
+   * Configure global settings for a Decimal constructor.\r
+   *\r
+   * `obj` is an object with one or more of the following properties,\r
+   *\r
+   *   precision  {number}\r
+   *   rounding   {number}\r
+   *   toExpNeg   {number}\r
+   *   toExpPos   {number}\r
+   *   maxE       {number}\r
+   *   minE       {number}\r
+   *   modulo     {number}\r
+   *   crypto     {boolean|number}\r
+   *   defaults   {true}\r
+   *\r
+   * E.g. Decimal.config({ precision: 20, rounding: 4 })\r
+   *\r
+   */\r
+  function config(obj) {\r
+    if (!obj || typeof obj !== 'object') throw Error(decimalError + 'Object expected');\r
+    var i, p, v,\r
+      useDefaults = obj.defaults === true,\r
+      ps = [\r
+        'precision', 1, MAX_DIGITS,\r
+        'rounding', 0, 8,\r
+        'toExpNeg', -EXP_LIMIT, 0,\r
+        'toExpPos', 0, EXP_LIMIT,\r
+        'maxE', 0, EXP_LIMIT,\r
+        'minE', -EXP_LIMIT, 0,\r
+        'modulo', 0, 9\r
+      ];\r
+\r
+    for (i = 0; i < ps.length; i += 3) {\r
+      if (p = ps[i], useDefaults) this[p] = DEFAULTS[p];\r
+      if ((v = obj[p]) !== void 0) {\r
+        if (mathfloor(v) === v && v >= ps[i + 1] && v <= ps[i + 2]) this[p] = v;\r
+        else throw Error(invalidArgument + p + ': ' + v);\r
+      }\r
+    }\r
+\r
+    if (p = 'crypto', useDefaults) this[p] = DEFAULTS[p];\r
+    if ((v = obj[p]) !== void 0) {\r
+      if (v === true || v === false || v === 0 || v === 1) {\r
+        if (v) {\r
+          if (typeof crypto != 'undefined' && crypto &&\r
+            (crypto.getRandomValues || crypto.randomBytes)) {\r
+            this[p] = true;\r
+          } else {\r
+            throw Error(cryptoUnavailable);\r
+          }\r
+        } else {\r
+          this[p] = false;\r
+        }\r
+      } else {\r
+        throw Error(invalidArgument + p + ': ' + v);\r
+      }\r
+    }\r
+\r
+    return this;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the cosine of `x`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function cos(x) {\r
+    return new this(x).cos();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic cosine of `x`, rounded to precision\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function cosh(x) {\r
+    return new this(x).cosh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Create and return a Decimal constructor with the same configuration properties as this Decimal\r
+   * constructor.\r
+   *\r
+   */\r
+  function clone(obj) {\r
+    var i, p, ps;\r
+\r
+    /*\r
+     * The Decimal constructor and exported function.\r
+     * Return a new Decimal instance.\r
+     *\r
+     * v {number|string|bigint|Decimal} A numeric value.\r
+     *\r
+     */\r
+    function Decimal(v) {\r
+      var e, i, t,\r
+        x = this;\r
+\r
+      // Decimal called without new.\r
+      if (!(x instanceof Decimal)) return new Decimal(v);\r
+\r
+      // Retain a reference to this Decimal constructor, and shadow Decimal.prototype.constructor\r
+      // which points to Object.\r
+      x.constructor = Decimal;\r
+\r
+      if (isDecimalInstance(v)) {\r
+        x.s = v.s;\r
+\r
+        if (external) {\r
+          if (!v.d || v.e > Decimal.maxE) {\r
+\r
+            // Infinity.\r
+            x.e = NaN;\r
+            x.d = null;\r
+          } else if (v.e < Decimal.minE) {\r
+\r
+            // Zero.\r
+            x.e = 0;\r
+            x.d = [0];\r
+          } else {\r
+            x.e = v.e;\r
+            x.d = v.d.slice();\r
+          }\r
+        } else {\r
+          x.e = v.e;\r
+          x.d = v.d ? v.d.slice() : v.d;\r
+        }\r
+\r
+        return;\r
+      }\r
+\r
+      t = typeof v;\r
+\r
+      if (t === 'number') {\r
+        if (v === 0) {\r
+          x.s = 1 / v < 0 ? -1 : 1;\r
+          x.e = 0;\r
+          x.d = [0];\r
+          return;\r
+        }\r
+\r
+        if (v < 0) {\r
+          v = -v;\r
+          x.s = -1;\r
+        } else {\r
+          x.s = 1;\r
+        }\r
+\r
+        // Fast path for small integers.\r
+        if (v === ~~v && v < 1e7) {\r
+          for (e = 0, i = v; i >= 10; i /= 10) e++;\r
+\r
+          if (external) {\r
+            if (e > Decimal.maxE) {\r
+              x.e = NaN;\r
+              x.d = null;\r
+            } else if (e < Decimal.minE) {\r
+              x.e = 0;\r
+              x.d = [0];\r
+            } else {\r
+              x.e = e;\r
+              x.d = [v];\r
+            }\r
+          } else {\r
+            x.e = e;\r
+            x.d = [v];\r
+          }\r
+\r
+          return;\r
+        }\r
+\r
+        // Infinity or NaN?\r
+        if (v * 0 !== 0) {\r
+          if (!v) x.s = NaN;\r
+          x.e = NaN;\r
+          x.d = null;\r
+          return;\r
+        }\r
+\r
+        return parseDecimal(x, v.toString());\r
+      }\r
+\r
+      if (t === 'string') {\r
+        if ((i = v.charCodeAt(0)) === 45) {  // minus sign\r
+          v = v.slice(1);\r
+          x.s = -1;\r
+        } else {\r
+          if (i === 43) v = v.slice(1);  // plus sign\r
+          x.s = 1;\r
+        }\r
+\r
+        return isDecimal.test(v) ? parseDecimal(x, v) : parseOther(x, v);\r
+      }\r
+\r
+      if (t === 'bigint') {\r
+        if (v < 0) {\r
+          v = -v;\r
+          x.s = -1;\r
+        } else {\r
+          x.s = 1;\r
+        }\r
+\r
+        return parseDecimal(x, v.toString());\r
+      }\r
+\r
+      throw Error(invalidArgument + v);\r
+    }\r
+\r
+    Decimal.prototype = P;\r
+\r
+    Decimal.ROUND_UP = 0;\r
+    Decimal.ROUND_DOWN = 1;\r
+    Decimal.ROUND_CEIL = 2;\r
+    Decimal.ROUND_FLOOR = 3;\r
+    Decimal.ROUND_HALF_UP = 4;\r
+    Decimal.ROUND_HALF_DOWN = 5;\r
+    Decimal.ROUND_HALF_EVEN = 6;\r
+    Decimal.ROUND_HALF_CEIL = 7;\r
+    Decimal.ROUND_HALF_FLOOR = 8;\r
+    Decimal.EUCLID = 9;\r
+\r
+    Decimal.config = Decimal.set = config;\r
+    Decimal.clone = clone;\r
+    Decimal.isDecimal = isDecimalInstance;\r
+\r
+    Decimal.abs = abs;\r
+    Decimal.acos = acos;\r
+    Decimal.acosh = acosh;        // ES6\r
+    Decimal.add = add;\r
+    Decimal.asin = asin;\r
+    Decimal.asinh = asinh;        // ES6\r
+    Decimal.atan = atan;\r
+    Decimal.atanh = atanh;        // ES6\r
+    Decimal.atan2 = atan2;\r
+    Decimal.cbrt = cbrt;          // ES6\r
+    Decimal.ceil = ceil;\r
+    Decimal.clamp = clamp;\r
+    Decimal.cos = cos;\r
+    Decimal.cosh = cosh;          // ES6\r
+    Decimal.div = div;\r
+    Decimal.exp = exp;\r
+    Decimal.floor = floor;\r
+    Decimal.hypot = hypot;        // ES6\r
+    Decimal.ln = ln;\r
+    Decimal.log = log;\r
+    Decimal.log10 = log10;        // ES6\r
+    Decimal.log2 = log2;          // ES6\r
+    Decimal.max = max;\r
+    Decimal.min = min;\r
+    Decimal.mod = mod;\r
+    Decimal.mul = mul;\r
+    Decimal.pow = pow;\r
+    Decimal.random = random;\r
+    Decimal.round = round;\r
+    Decimal.sign = sign;          // ES6\r
+    Decimal.sin = sin;\r
+    Decimal.sinh = sinh;          // ES6\r
+    Decimal.sqrt = sqrt;\r
+    Decimal.sub = sub;\r
+    Decimal.sum = sum;\r
+    Decimal.tan = tan;\r
+    Decimal.tanh = tanh;          // ES6\r
+    Decimal.trunc = trunc;        // ES6\r
+\r
+    if (obj === void 0) obj = {};\r
+    if (obj) {\r
+      if (obj.defaults !== true) {\r
+        ps = ['precision', 'rounding', 'toExpNeg', 'toExpPos', 'maxE', 'minE', 'modulo', 'crypto'];\r
+        for (i = 0; i < ps.length;) if (!obj.hasOwnProperty(p = ps[i++])) obj[p] = this[p];\r
+      }\r
+    }\r
+\r
+    Decimal.config(obj);\r
+\r
+    return Decimal;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` divided by `y`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * y {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function div(x, y) {\r
+    return new this(x).div(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural exponential of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} The power to which to raise the base of the natural log.\r
+   *\r
+   */\r
+  function exp(x) {\r
+    return new this(x).exp();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` round to an integer using `ROUND_FLOOR`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function floor(x) {\r
+    return finalise(x = new this(x), x.e + 1, 3);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the square root of the sum of the squares of the arguments,\r
+   * rounded to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * hypot(a, b, ...) = sqrt(a^2 + b^2 + ...)\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function hypot() {\r
+    var i, n,\r
+      t = new this(0);\r
+\r
+    external = false;\r
+\r
+    for (i = 0; i < arguments.length;) {\r
+      n = new this(arguments[i++]);\r
+      if (!n.d) {\r
+        if (n.s) {\r
+          external = true;\r
+          return new this(1 / 0);\r
+        }\r
+        t = n;\r
+      } else if (t.d) {\r
+        t = t.plus(n.times(n));\r
+      }\r
+    }\r
+\r
+    external = true;\r
+\r
+    return t.sqrt();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return true if object is a Decimal instance (where Decimal is any Decimal constructor),\r
+   * otherwise return false.\r
+   *\r
+   */\r
+  function isDecimalInstance(obj) {\r
+    return obj instanceof Decimal || obj && obj.toStringTag === tag || false;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the natural logarithm of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function ln(x) {\r
+    return new this(x).ln();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the log of `x` to the base `y`, or to base 10 if no base\r
+   * is specified, rounded to `precision` significant digits using rounding mode `rounding`.\r
+   *\r
+   * log[y](x)\r
+   *\r
+   * x {number|string|bigint|Decimal} The argument of the logarithm.\r
+   * y {number|string|bigint|Decimal} The base of the logarithm.\r
+   *\r
+   */\r
+  function log(x, y) {\r
+    return new this(x).log(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the base 2 logarithm of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function log2(x) {\r
+    return new this(x).log(2);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the base 10 logarithm of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function log10(x) {\r
+    return new this(x).log(10);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the maximum of the arguments.\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function max() {\r
+    return maxOrMin(this, arguments, -1);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the minimum of the arguments.\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function min() {\r
+    return maxOrMin(this, arguments, 1);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` modulo `y`, rounded to `precision` significant digits\r
+   * using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * y {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function mod(x, y) {\r
+    return new this(x).mod(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` multiplied by `y`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * y {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function mul(x, y) {\r
+    return new this(x).mul(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` raised to the power `y`, rounded to precision\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} The base.\r
+   * y {number|string|bigint|Decimal} The exponent.\r
+   *\r
+   */\r
+  function pow(x, y) {\r
+    return new this(x).pow(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Returns a new Decimal with a random value equal to or greater than 0 and less than 1, and with\r
+   * `sd`, or `Decimal.precision` if `sd` is omitted, significant digits (or less if trailing zeros\r
+   * are produced).\r
+   *\r
+   * [sd] {number} Significant digits. Integer, 0 to MAX_DIGITS inclusive.\r
+   *\r
+   */\r
+  function random(sd) {\r
+    var d, e, k, n,\r
+      i = 0,\r
+      r = new this(1),\r
+      rd = [];\r
+\r
+    if (sd === void 0) sd = this.precision;\r
+    else checkInt32(sd, 1, MAX_DIGITS);\r
+\r
+    k = Math.ceil(sd / LOG_BASE);\r
+\r
+    if (!this.crypto) {\r
+      for (; i < k;) rd[i++] = Math.random() * 1e7 | 0;\r
+\r
+    // Browsers supporting crypto.getRandomValues.\r
+    } else if (crypto.getRandomValues) {\r
+      d = crypto.getRandomValues(new Uint32Array(k));\r
+\r
+      for (; i < k;) {\r
+        n = d[i];\r
+\r
+        // 0 <= n < 4294967296\r
+        // Probability n >= 4.29e9, is 4967296 / 4294967296 = 0.00116 (1 in 865).\r
+        if (n >= 4.29e9) {\r
+          d[i] = crypto.getRandomValues(new Uint32Array(1))[0];\r
+        } else {\r
+\r
+          // 0 <= n <= 4289999999\r
+          // 0 <= (n % 1e7) <= 9999999\r
+          rd[i++] = n % 1e7;\r
+        }\r
+      }\r
+\r
+    // Node.js supporting crypto.randomBytes.\r
+    } else if (crypto.randomBytes) {\r
+\r
+      // buffer\r
+      d = crypto.randomBytes(k *= 4);\r
+\r
+      for (; i < k;) {\r
+\r
+        // 0 <= n < 2147483648\r
+        n = d[i] + (d[i + 1] << 8) + (d[i + 2] << 16) + ((d[i + 3] & 0x7f) << 24);\r
+\r
+        // Probability n >= 2.14e9, is 7483648 / 2147483648 = 0.0035 (1 in 286).\r
+        if (n >= 2.14e9) {\r
+          crypto.randomBytes(4).copy(d, i);\r
+        } else {\r
+\r
+          // 0 <= n <= 2139999999\r
+          // 0 <= (n % 1e7) <= 9999999\r
+          rd.push(n % 1e7);\r
+          i += 4;\r
+        }\r
+      }\r
+\r
+      i = k / 4;\r
+    } else {\r
+      throw Error(cryptoUnavailable);\r
+    }\r
+\r
+    k = rd[--i];\r
+    sd %= LOG_BASE;\r
+\r
+    // Convert trailing digits to zeros according to sd.\r
+    if (k && sd) {\r
+      n = mathpow(10, LOG_BASE - sd);\r
+      rd[i] = (k / n | 0) * n;\r
+    }\r
+\r
+    // Remove trailing words which are zero.\r
+    for (; rd[i] === 0; i--) rd.pop();\r
+\r
+    // Zero?\r
+    if (i < 0) {\r
+      e = 0;\r
+      rd = [0];\r
+    } else {\r
+      e = -1;\r
+\r
+      // Remove leading words which are zero and adjust exponent accordingly.\r
+      for (; rd[0] === 0; e -= LOG_BASE) rd.shift();\r
+\r
+      // Count the digits of the first word of rd to determine leading zeros.\r
+      for (k = 1, n = rd[0]; n >= 10; n /= 10) k++;\r
+\r
+      // Adjust the exponent for leading zeros of the first word of rd.\r
+      if (k < LOG_BASE) e -= LOG_BASE - k;\r
+    }\r
+\r
+    r.e = e;\r
+    r.d = rd;\r
+\r
+    return r;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` rounded to an integer using rounding mode `rounding`.\r
+   *\r
+   * To emulate `Math.round`, set rounding to 7 (ROUND_HALF_CEIL).\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function round(x) {\r
+    return finalise(x = new this(x), x.e + 1, this.rounding);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return\r
+   *   1    if x > 0,\r
+   *  -1    if x < 0,\r
+   *   0    if x is 0,\r
+   *  -0    if x is -0,\r
+   *   NaN  otherwise\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function sign(x) {\r
+    x = new this(x);\r
+    return x.d ? (x.d[0] ? x.s : 0 * x.s) : x.s || NaN;\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the sine of `x`, rounded to `precision` significant digits\r
+   * using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function sin(x) {\r
+    return new this(x).sin();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic sine of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function sinh(x) {\r
+    return new this(x).sinh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the square root of `x`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function sqrt(x) {\r
+    return new this(x).sqrt();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` minus `y`, rounded to `precision` significant digits\r
+   * using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   * y {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function sub(x, y) {\r
+    return new this(x).sub(y);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the sum of the arguments, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * Only the result is rounded, not the intermediate calculations.\r
+   *\r
+   * arguments {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function sum() {\r
+    var i = 0,\r
+      args = arguments,\r
+      x = new this(args[i]);\r
+\r
+    external = false;\r
+    for (; x.s && ++i < args.length;) x = x.plus(args[i]);\r
+    external = true;\r
+\r
+    return finalise(x, this.precision, this.rounding);\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the tangent of `x`, rounded to `precision` significant\r
+   * digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function tan(x) {\r
+    return new this(x).tan();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is the hyperbolic tangent of `x`, rounded to `precision`\r
+   * significant digits using rounding mode `rounding`.\r
+   *\r
+   * x {number|string|bigint|Decimal} A value in radians.\r
+   *\r
+   */\r
+  function tanh(x) {\r
+    return new this(x).tanh();\r
+  }\r
+\r
+\r
+  /*\r
+   * Return a new Decimal whose value is `x` truncated to an integer.\r
+   *\r
+   * x {number|string|bigint|Decimal}\r
+   *\r
+   */\r
+  function trunc(x) {\r
+    return finalise(x = new this(x), x.e + 1, 1);\r
+  }\r
+\r
+\r
+  // Create and configure initial Decimal constructor.\r
+  Decimal = clone(DEFAULTS);\r
+  Decimal.prototype.constructor = Decimal;\r
+  Decimal['default'] = Decimal.Decimal = Decimal;\r
+\r
+  // Create the internal constants from their string values.\r
+  LN10 = new Decimal(LN10);\r
+  PI = new Decimal(PI);\r
+\r
+\r
+  // Export.\r
+\r
+\r
+  // AMD.\r
+  if (typeof define == 'function' && define.amd) {\r
+    define(function () {\r
+      return Decimal;\r
+    });\r
+\r
+  // Node and other environments that support module.exports.\r
+  } else if (typeof module != 'undefined' && module.exports) {\r
+    if (typeof Symbol == 'function' && typeof Symbol.iterator == 'symbol') {\r
+      P[Symbol['for']('nodejs.util.inspect.custom')] = P.toString;\r
+      P[Symbol.toStringTag] = 'Decimal';\r
+    }\r
+\r
+    module.exports = Decimal;\r
+\r
+  // Browser.\r
+  } else {\r
+    if (!globalScope) {\r
+      globalScope = typeof self != 'undefined' && self && self.self == self ? self : window;\r
+    }\r
+\r
+    noConflict = globalScope.Decimal;\r
+    Decimal.noConflict = function () {\r
+      globalScope.Decimal = noConflict;\r
+      return Decimal;\r
+    };\r
+\r
+    globalScope.Decimal = Decimal;\r
+  }\r
+})(this);\r
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 (file)
index 0000000..4371baa
--- /dev/null
@@ -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<string, Function>} */
+    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<string, number> & { 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<string, NerdamerSymbolType>} */
+        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<string, NerdamerSymbolType>} 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<string, number> & { length: number }} [map]
+     */
+    function MVTerm(coeff, terms, map) {
+        /** @type {FracType[]} */
+        this.terms = terms || [];
+        /** @type {FracType} */
+        this.coeff = coeff;
+        /** @type {(Record<string, number> & { 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<number, string> | 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<number, string>} [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<number, string>}
+     */
+    MVTerm.prototype.getRevMap = function getRevMap() {
+        if (this.revMap) {
+            return this.revMap;
+        }
+        /** @type {Record<number, string>} */
+        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<string, number> & { length: number }}
+     */
+    core.Utils.toMapObj = function toMapObj(arr) {
+        let c = 0;
+        /** @type {Record<string, number> & { length: number }} */
+        const o = /** @type {Record<string, number> & { 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<string, string>} [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<string, string>} map
+     * @returns {Record<string, NerdamerSymbolType>}
+     */
+    core.Utils.getFunctionsSubs = function getFunctionsSubs(map) {
+        /** @type {Record<string, NerdamerSymbolType>} */
+        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<string, [number, NerdamerSymbolType[]]>} */
+                        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<string, ExpressionParam>} */
+                        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<string, string>} */
+                        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<string, string>} */
+                    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 (file)
index 0000000..f5aa22d
--- /dev/null
@@ -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<string, NerdamerSymbolType>} */
+        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<string, NerdamerSymbolType | boolean>} */
+                    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<string, ExpressionParam>} */ (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<string, NerdamerSymbolType>} */
+                    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<string, ExpressionParam>} */ (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<string, NerdamerSymbolType>} */
+                                                    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<string, NerdamerSymbolType>} 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<string, NerdamerSymbolType>} */
+                const upper = {};
+                /** @type {Record<string, NerdamerSymbolType>} */
+                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<string, ExpressionParam>} */
+                        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 (file)
index 0000000..8984cdd
--- /dev/null
@@ -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<string, number>}
+             */
+            frequencyMap(arr) {
+                /** @type {Record<string, number>} */
+                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<number, string | string[]>} */
+                    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 (file)
index 0000000..1a62b42
--- /dev/null
@@ -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 (file)
index 0000000..7f7e424
--- /dev/null
@@ -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<string, string[]>} */
+                    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<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[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<string, NerdamerSymbolType | string | number>} */
+                    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<string, number>} */
+            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<string, NerdamerSymbolType>} */
+                        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(/(?<![a-z])abs/gu);
+        // Not found or more than 1 occurrence? get out!
+        if (!match || match.length > 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(/(?<![a-z])abs/u, ''));
+        const eqminus = eqns.constructor(eq.replace(/(?<![a-z])abs/u, '(-1)'));
+
+        const resultplus = solve(eqplus, solveFor, null, depth, fn);
+        const resultminus = solve(eqminus, solveFor, null, depth, fn);
+
+        return [resultminus, resultplus];
+    };
+    /*
+     *
+     * @param {string[]|string|Equation} eqns
+     * @param {string} solveFor
+     * @param {Array} solutions
+     * @param {number} depth
+     * @param {string|Equation} fn
+     * @returns {Array}
+     */
+    // let solve = function (eqns, solveFor, solutions, depth, fn) {
+    //     let original = "<multiple>";
+    //     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<string, number>} */
+        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<string, number>} */
+                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<string, NerdamerSymbolType>} */
+            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 (file)
index 0000000..9f62f0b
--- /dev/null
@@ -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 (file)
index 0000000..18f26ee
--- /dev/null
@@ -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<number, boolean>} */
+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 (file)
index 0000000..9fdcdff
--- /dev/null
@@ -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<unknown>} 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<string, NerdamerSymbolType>} */
+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<number, boolean>;
+ *         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<string, NerdamerSymbolType>;
+ *         CONSTANTS: Record<string, NerdamerSymbolType | string | number>;
+ *         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<string, ExpressionParam>) => 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<string, NerdamerSymbolType | string>} */ ({}),
+        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<string, ExpressionParam>} [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(/^(?<zeros>0*).*$/u);
+        return match ? match[1] : '';
+    }
+
+    /** @param {string} num */
+    static removeLeadingZeroes(num) {
+        const match = num.match(/^0*(?<rest>.*)$/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(/\{(?<idx>\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<typeof Array.prototype.slice.call>['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<string, T> | 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<string, string | number | NerdamerSymbolType>}
+ */
+function knownVariable(variable, value) {
+    /** @type {Record<string, string | number | NerdamerSymbolType>} */
+    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<string, T>} 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<string, NerdamerSymbolType>;
+ * }}
+ */
+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<string, NerdamerSymbolType>;
+ * }}
+ */
+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 (
+        `<div class="tree">\n` +
+        `    <ul>\n` +
+        `        <li>\n${treeResult.toHTML(3, indent)}\n` +
+        `        </li>\n` +
+        `    </ul>\n` +
+        `</div>`
+    );
+}
+
+/**
+ * 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<string, [Function, number] | [Function, number[]] | [Function, number, object]>;
+ *     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<number, string> | string[]}
+ */
+function expressions(asObject, asLaTeX, option) {
+    /** @type {Record<number, string> | 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<number, string> | 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] = /\((?<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(/_(?<char>[A-Za-z0-9])/gu, (...g) => `${g[0]} `);
+    // Convert x^2 => x^{2}
+    e = e.replace(/\^(?<char>[A-Za-z0-9])/gu, (...g) => `^{${g[1]}}`);
+    // Convert \frac12 => \frac{1}2
+    e = e.replace(/(?<cmd>\\[A-Za-z]+)(?<digit>\d)/gu, (...g) => `${g[1]}{${g[2]}}`);
+    // Convert \frac{1}2 => \frac{1}{2}
+    e = e.replace(/(?<cmd>\\[A-Za-z]+\{.*?\})(?<digit>\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<string, NerdamerSymbolType>;
+ *     _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<string, NerdamerSymbolType | string | number>;
+ * }}
+ */
+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<string, NerdamerSymbolType>;
+ *     CONSTANTS: Record<string, NerdamerSymbolType | string | number>;
+ *     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<string, [Function, number] | [Function, number[]] | [Function, number, object]>;
+ * }}
+ */
+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<string, Function | [Function, number] | [Function, number[]] | [Function, number, object]>;
+ *     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<SettingsType>} */ (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<string, [Function, number] | [Function, number[]] | [Function, number, object]>;
+ *     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<number, boolean>;
+ * }}
+ */
+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<number, boolean>;
+ * }}
+ */
+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<string, string | number | NerdamerSymbolType>} [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<string, ExpressionParam>} [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<string, Record<string, Function | string | object> | Record<string, string>>} */
+    dependencies: {},
+    /**
+     * @type {Record<
+     *     string,
+     *     (
+     *         symbol: NerdamerSymbolType,
+     *         deps: [Record<string, string>, string]
+     *     ) => [string, [Record<string, string>, 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, string>, string]} [deps]
+     * @returns {[Record<string, string>, 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, string>, string]} [dependencies]
+     * @returns {[Record<string, string>, 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, string>, 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(/(?<prefix>.+)_$/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<string, Function>)[]} */
+    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*(?<fnName>[a-z_][a-z0-9_]*)\((?<fnArgs>[a-z0-9_,\s]*)\)\s*:?=\s*(?<fnBody>.+)\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:
+        /(?<coeff>[+\-/*]*[0-9]+)(?<vars>[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<string, number> }} */
+    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<string, NerdamerSymbolType>} */
+    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<string, NerdamerSymbolType>} [o]
+ * @returns {Record<string, NerdamerSymbolType>}
+ * @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 (
+            /^(?<sign>-?\+?\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<string, unknown>} */ (/** @type {unknown} */ (self));
+        const cloneAny = /** @type {Record<string, unknown>} */ (/** @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(/(?<![A-Za-z])\d+\.?\d*e\+?-?\d+/giu, x => 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(/(?<varname>[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(
+                    /(?<prefix>[a-z0-9_]+)(?<open>\()|(?<close>\))(?<suffix>[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<L; i++) {
+            if(e.charAt(i) === ' ')
+            return i;
+            }
+            
+            return L; //assume the end of the string instead
+            };
+            */
+            /**
+             * Adds a scope to tokens
+             *
+             * @param {string} [scopeType]
+             * @param {number} [column]
+             * @returns {undefined}
+             */
+            const addScope = function (scopeType = undefined, column = undefined) {
+                /** @type {ScopeArrayType} */
+                const newScope = /** @type {ScopeArrayType} */ ([]); // Create a new scope
+                if (scopeType !== undefined) {
+                    newScope.type = scopeType;
+                }
+                newScope.column = column; // Mark the column of the scope
+                scopes.push(newScope); // Add it to the list of scopes
+                target.push(newScope); // Add it to the tokens list since now it's a scope
+                target = newScope; // Point to it
+                depth++; // Go down one in scope
+            };
+            /**
+             * Goes up in scope by one
+             *
+             * @returns {undefined}
+             */
+            const goUp = function () {
+                scopes.pop(); // Remove the scope from the scopes stack
+                target = scopes[--depth]; // Point the above scope
+            };
+            /**
+             * Extracts all the operators from the expression string starting at postion startAt
+             *
+             * @param {number} startAt
+             * @returns {string}
+             */
+            const getOperatorStr = function (startAt) {
+                startAt = startAt === undefined ? col : startAt;
+                // Mark the end of the operator as the start since we're just going
+                // to be walking along the string
+                let end = startAt + 1;
+                // Just keep moving along
+                while (e.charAt(end++) in operators) {
+                    // Intentionally empty - just advancing end pointer
+                }
+                // Remember that we started at one position ahead. The beginning operator is what triggered
+                // this function to be called in the first place. String.CharAt is zero based so we now
+                // have to correct two places. The initial increment + the extra++ at the end of end during
+                // the last iteration.
+                return e.substring(startAt, end - 1);
+            };
+            /**
+             * Breaks operator up in to several different operators as defined in operators
+             *
+             * @param {string} opStr
+             * @returns {Token[]}
+             */
+            const chunkify = function (opStr) {
+                const start = col - opStr.length; // Start of operator
+                const _operators = [];
+                let operator = opStr.charAt(0);
+                // Grab the largest possible chunks but start at 2 since we already know
+                // that the first character is an operator
+                const len = opStr.length;
+                let i;
+                for (i = 1; i < len; i++) {
+                    const ch = opStr.charAt(i);
+                    const o = operator + ch;
+                    // Since the operator now is undefined then the last operator
+                    // was the largest possible combination.
+                    if (o in operators) {
+                        operator = o; // Now the operator is the larger chunk
+                    } else {
+                        _operators.push(new Token(operator, Token.OPERATOR, start + i));
+                        operator = ch;
+                    }
+                }
+                // Add the last operator
+                _operators.push(new Token(operator, Token.OPERATOR, start + i));
+                return _operators;
+            };
+
+            /**
+             * Is used to add a token to the tokens array. Makes sure that no empty token is added
+             *
+             * @param {number} at
+             * @param {string} [token]
+             * @returns {undefined}
+             */
+            const addToken = function (at, token = undefined) {
+                // Grab the token if we're not supplied one
+                if (token === undefined) {
+                    token = e.substring(lpos, at);
+                }
+                // Only add it if it's not an empty string
+                if (token in _.units) {
+                    target.push(new Token(token, Token.UNIT, lpos));
+                } else if (token !== '') {
+                    target.push(new Token(token, Token.VARIABLE_OR_LITERAL, lpos));
+                }
+            };
+            /**
+             * Adds a function to the output
+             *
+             * @param {string} f
+             * @returns {undefined}
+             */
+            const addFunction = function (f) {
+                target.push(new Token(f, Token.FUNCTION, lpos));
+            };
+            /**
+             * Tokens are found between operators so this marks the location of where the last token was found
+             *
+             * @param {number} position
+             * @returns {undefined}
+             */
+            const setLastPosition = function (position) {
+                lpos = position + 1;
+            };
+            /**
+             * When a operator is found and added, especially a combo operator, then the column location has to be
+             * adjusted to the end of the operator
+             *
+             * @returns {undefined}
+             */
+            const adjustColumnPosition = function () {
+                lpos = lpos + operatorStr.length - 2;
+                col = lpos - 1;
+            };
+            for (; col < L; col++) {
+                const ch = e.charAt(col);
+                if (ch in operators) {
+                    addToken(col);
+                    // Is the last token numeric?
+                    const lastTokenIsNumeric = target[0] && isNumber(target[0]);
+                    // Is this character multiplication?
+                    const isMultiplication = lastTokenIsNumeric && ch === MULT;
+                    // If we're in a new scope then go up by one but if the space
+                    // is right befor an operator then it makes no sense to go up in scope
+                    // consider sin -x. The last position = current position at the minus sign
+                    // this means that we're going for sin(x) -x which is wrong
+                    // Ignore comma since comma is still part of the existing scope.
+                    if (hasSpace && lpos < col && !(ch === COMMA || isMultiplication)) {
+                        hasSpace = false;
+                        goUp();
+                    }
+                    // Mark the last position that a
+                    setLastPosition(col + 1);
+                    operatorStr = getOperatorStr(col);
+
+                    adjustColumnPosition();
+                    target.push(...chunkify(operatorStr));
+                } else if (ch in brackets) {
+                    const bracket = brackets[ch];
+
+                    if (bracket.is_open) {
+                        // Mark the bracket
+                        openBrackets.push([bracket, lpos]);
+                        const f = e.substring(lpos, col);
+                        if (f in functions) {
+                            addFunction(f);
+                        } else if (f !== '') {
+                            // Assume multiplication
+                            // TODO: Add the multiplication to stack
+                            target.push(new Token(f, Token.VARIABLE_OR_LITERAL, lpos));
+                        }
+                        // Go down one in scope
+                        addScope(bracket.maps_to, col);
+                    } else if (bracket.is_close) {
+                        // Get the matching bracket
+                        const pair = openBrackets.pop();
+                        // Throw errors accordingly
+                        // missing open bracket
+                        if (!pair) {
+                            throw new ParityError(`Missing open bracket for bracket at: ${col + 1}`);
+                        }
+                        // Incorrect pair
+                        else if (pair[0].id !== bracket.id - 1) {
+                            throw new ParityError('Parity error');
+                        }
+
+                        addToken(col);
+                        goUp();
+                    }
+                    setLastPosition(col);
+                } else if (ch === SPACE) {
+                    const prev = e.substring(lpos, col); // Look back
+                    let nxt = e.charAt(col + 1); // Look forward
+                    if (hasSpace) {
+                        if (prev in operators) {
+                            target.push(new Token(prev, Token.OPERATOR, col));
+                        } else {
+                            addToken(undefined, prev);
+                            // We're at the closing space
+                            goUp(); // Go up in scope if we're at a space
+
+                            // assume multiplication if it's not an operator except for minus
+                            const isOperator = nxt in operators;
+
+                            if ((isOperator && operators[nxt].value === MINUS) || !isOperator) {
+                                target.push(new Token(MULT, Token.OPERATOR, col));
+                            }
+                        }
+                        hasSpace = false; // Remove the space
+                    } else {
+                        // We're at the closing space
+                        // check if it's a function
+                        const f = e.substring(lpos, col);
+
+                        if (f in functions) {
+                            // There's no need to go up in scope if the next character is an operator
+                            hasSpace = true; // Mark that a space was found
+                            addFunction(f);
+                            addScope();
+                        } else if (f in operators) {
+                            target.push(new Token(f, Token.OPERATOR, col));
+                        } else {
+                            addToken(undefined, f);
+                            // Peek ahead to the next character
+                            nxt = e.charAt(col + 1);
+
+                            // If it's a number then add the multiplication operator to the stack but make sure that the next character
+                            // is not an operator
+
+                            if (
+                                prev !== EMPTY_STRING &&
+                                nxt !== EMPTY_STRING &&
+                                !(prev in operators) &&
+                                !(nxt in operators)
+                            ) {
+                                target.push(new Token(MULT, Token.OPERATOR, col));
+                            }
+                        }
+                        // Possible source of bug. Review
+                        /*
+                        //space can mean multiplication so add the symbol if the is encountered
+                        if(/\d+|\d+\.?\d*e[\+\-]*\d+/i.test(f)) {
+                        let next = e.charAt(col+1);
+                        let nextIsOperator = next in operators;
+                        let ns = next_space(col+1);
+                        let next_word = e.substring(col+1, ns);
+                        //the next can either be a prefix operator or no operator
+                        if((nextIsOperator && operators[next].prefix) || !(nextIsOperator || next_word in operators))
+                        target.push(new Token('*', Token.OPERATOR, col));
+                        }
+                        */
+                    }
+                    setLastPosition(col); // Mark this location
+                }
+            }
+            // Check that all brackets were closed
+            if (openBrackets.length) {
+                const b = openBrackets.pop();
+                throw new ParityError(`Missing closed bracket for bracket at ${b[1] + 1}`);
+            }
+            // Add the last token
+            addToken(col);
+
+            return tokens;
+        };
+        /*
+         * Puts token array in Reverse Polish Notation
+         * @param {Token[]} tokens
+         * @returns {Token[]}
+         */
+        this.toRPN = function toRPN(tokens) {
+            const fn = tokens.type;
+            const l = tokens.length;
+            let i;
+            let e; // Current token being processed - also used for error reporting
+            const output = [];
+            const stack = [];
+            const prefixes = [];
+            const collapse = function collapse(target, destination) {
+                while (target.length) {
+                    destination.push(target.pop());
+                }
+            };
+            // Mark all the prefixes and add them to the stack
+            for (i = 0; i < l; i++) {
+                const token = tokens[i];
+                if (token.type !== Token.OPERATOR) {
+                    break;
+                }
+                if (!token.prefix) {
+                    throw new OperatorError('Not a prefix operator');
+                }
+                token.is_prefix = true;
+                stack.push(token);
+            }
+            // Begin with remaining tokens
+            for (; i < l; i++) {
+                e = tokens[i];
+                if (e.type === Token.OPERATOR) {
+                    const operator = e;
+
+                    // Create the option for the operator being overloaded
+                    if (operator.overloaded) {
+                        const next = tokens[i + 1];
+                        // If it's followed by a number or variable then we assume it's not a postfix operator
+                        if (next && next.type === Token.VARIABLE_OR_LITERAL) {
+                            operator.postfix = false;
+                            // Override the original function with the overload function
+                            operator.action = operator.overloadAction;
+                            operator.leftAssoc = operator.overloadLeftAssoc;
+                        }
+                    }
+
+                    // If the stack is not empty
+                    while (stack.length) {
+                        const last = stack[stack.length - 1];
+                        // If (there is an operator at the top of the operator stack with greater precedence)
+                        // or (the operator at the top of the operator stack has equal precedence and is left associative)) ~ wikipedia
+                        // the !prefixes.length makes sure that the operator on stack isn't prematurely taken fromt he stack.
+                        if (
+                            !(
+                                last.precedence > 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)}<li>\n${this.left.toHTML(depth + 2, indent)}${tab(depth + 1)}</li> \n`
+                    : '';
+                const right = this.right
+                    ? `${tab(depth + 1)}<li>\n${this.right.toHTML(depth + 2, indent)}${tab(depth + 1)}</li>\n`
+                    : '';
+                html = `${tab(depth)}<div class="${this.type.toLowerCase()}"><span>${this.value}</span></div>${tab(depth)}\n`;
+                if (left || right) {
+                    html += `${tab(depth)}<ul>\n${left}${right}${tab(depth)}</ul>\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\((?<inner>.+)\\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<string, { order: number; action: Function }>} */
+        this.getPreprocessors = function getPreprocessors() {
+            /** @type {Record<string, { order: number; action: Function }>} */
+            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<string, string>} */
+            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<string, Function>} */ ({}),
+        Calculus: /** @type {Record<string, Function>} */ ({}),
+        Algebra: /** @type {Record<string, Function>} */ ({}),
+        Extra: /** @type {Record<string, Function>} */ ({}),
+    };
+}
+
+// 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 (file)
index 0000000..a0df0c8
--- /dev/null
@@ -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 (file)
index 0000000..433fb2b
--- /dev/null
@@ -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 (file)
index 0000000..8f2e3df
--- /dev/null
@@ -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');
+       });
+});
index 51f55971e9dcb471aed28dc2f98c520c4853b431..60047e995be77a5f8742f52d2f5540570ff7f08d 100644 (file)
@@ -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
        //
index fbb7dc4c3a92f4e6523d1a8450eec7aa770e9e90..4c1e9724a4df1585ce5f1397d3236d21f422127f 100644 (file)
@@ -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()
        ],