Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
JsonSerializationOptions,
PatternMatchOptions,
SimplifyOptions,
TransformOptions,
IComputeEngine as ComputeEngine,
Scope,
Tensor,
Expand All @@ -38,6 +39,7 @@ import { toAsciiMath } from './ascii-math';
// Dynamic import for serializeJson to avoid circular dependency
import { cmp, eq, same } from './compare';
import { CancellationError } from '../../common/interruptible';
import { transform } from './transform';
import { isSymbol, isString, isNumber, isFunction } from './type-guards';
import { extractIntervalBounds } from './inequality-bounds';

Expand Down Expand Up @@ -822,6 +824,10 @@ export abstract class _BoxedExpression implements Expression {
return null;
}

transform(options: TransformOptions): Expression | null {
return transform(this, options);
}

has(_v: string | string[]): boolean {
return false;
}
Expand Down
179 changes: 179 additions & 0 deletions src/compute-engine/boxed-expression/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import type {
EvaluateOptions,
Expression,
ExpressionInput,
FormOption,
ReplaceOptions,
Rule,
RuleConditionFunction,
RuleFunction,
RuleReplaceFunction,
TransformOptions,
} from '../global-types';
import type { LatexString } from '../types';

export function transform(
expr: Expression,
options: TransformOptions
): Expression | null {
const { engine: ce } = expr;
const { type, match } = options;
let { targets } = options;

// In absence of any matching spec., set the target as this *expr*
if (match === undefined && targets === undefined) targets = expr;
if (match !== undefined && targets !== undefined)
throw new Error('Cannot specify both `match` and `targets`');

// if (options.type === 'replace') options.replace

/*
* All transformations take place through the match->replace mechanism of 'replace()', using a single rule
* Bundle the components to construct the rule.
*/
let replace: LatexString | Expression | RuleReplaceFunction | RuleFunction;

// First, generate the 'replace' component; dependent upon transformation
// ------------------------------

switch (type) {
case 'replace':
if (options.replace === undefined)
throw new Error(
`Expected 'replace' option for transformation 'replace'`
);
// @todo: ensure wrapped in a 'RuleFunction' for consistency?
replace = options.replace;
break;

case 'structural':
replace = ((expr) =>
expr.isStructural ? undefined : expr.structural) satisfies RuleFunction;
break;

case 'canonical':
// 'canonical' must have a degree: i.e. either 'true' or 'CanonicalForm | CanonicalForm[]'
if (!options.canonical)
throw new Error(
`Expected 'canonical' option for transformation 'canonical'`
);
replace = ((expr) =>
expr.isCanonical
? undefined
: ce.expr(expr, {
form:
options.canonical === true
? 'canonical'
: options.canonical /* CanonicalForm */,
})) satisfies RuleFunction;
break;

case 'evaluate':
case 'N':
const evalOptions: Partial<EvaluateOptions> = {
...(options.evalOptions ?? {}),
numericApproximation: type === 'N' ? true : false,
};
replace = ((expr) => {
const result = expr.canonical.evaluate(evalOptions);
if (result.isSame(expr)) return undefined;
return result;
}) satisfies RuleFunction;
break;
case 'simplify':
replace = ((expr) => {
const result = expr.simplify(options.simplifyOptions);
if (result.isSame(expr)) return undefined;
return result;
}) satisfies RuleFunction;
break;
default:
throw new TypeError(`Unknown transform type: '${type}'`);
}

/*
* Build the rule (and 'match' component based on strategy).
*
*/
let rule: Rule;
// The only case where recursivity is _not_ to apply.
const directOnly =
targets &&
(targets === expr ||
(Array.isArray(targets) && targets.length === 1 && targets[0] === expr));
const replaceOptions: Partial<ReplaceOptions> = {
recursive: directOnly ? false : true,
direction: options.direction,
// @note: do not supply 'form' here, since this will undesirably apply to the entire input.
// Instead, apply this in the replacement `RuleFunction`
};

// For select transformations, a 'form' definition may be supplied.
// (Notably, all others - with exception of 'structural' - by definition produce canonical
// (output))
if (type === 'replace') replaceOptions.form = options.form;

// Standard pattern-matching route
// -----------------------------------
if (match !== undefined) {
let pattern: LatexString | ExpressionInput;
let condition: LatexString | RuleConditionFunction | undefined;

// Pattern bundled with match-options/condition
if (typeof match === 'object' && 'pattern' in match) {
if (match.useVariations)
replaceOptions.useVariations = match.useVariations;
if (match.matchPermutations)
replaceOptions.matchPermutations = match.matchPermutations;
// @fix?: 'matchMissingTerms' is not currently utilized in a 'replace()' context (i.e., is not
// forwarded for internal matching).
// if (match.matchMissingTerms)
// replaceOptions.matchMissingTerms = match.matchMissingTerms;

pattern = match.pattern;
condition = match.condition;
} else {
// Str
pattern = match;
}

rule = {
match: pattern,
replace,
condition,
};
} else {
// Targeted transformation ('targets')
// -----------------------------------
// (In contrast to 'match', permit allow exact/referential expression-based, and predicate-based
// matching)
const replacementForm: FormOption = replaceOptions.form ?? 'canonical'; // For all
// transformations where 'form' not specifiable, the output is always [to be] made canonical

// Proceed by way of a 'RuleFunction' to emulate exact matching (with standard match patterns
// neither permiting matching via expr.-identity nor predicate.)
rule = (expr) => {
if (!directOnly) {
// Instead of matching via 'rule.match', 'targets' replicates this through either
// referential-identity and/or predicate-based matching.
if (typeof targets === 'function') {
if (!targets(expr)) return undefined;
} else {
const targetExprs = Array.isArray(targets) ? targets : [targets];
if (!targetExprs.some((target) => target === expr)) return undefined;
}
}

// With exception of a 'replace' transformation - which may specify its replacement
// 'replace' will take the form of a RuleFunction (according to the transformation)
return replace instanceof Function
? (replace as RuleFunction)(expr)
: ce.expr(replace, {
form: replacementForm,
});
};
}

// Transformations ultimately apply via single-Rule application with `replace()`
return expr.replace(rule, replaceOptions);
}
11 changes: 11 additions & 0 deletions src/compute-engine/boxed-expression/type-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ export function isFunction(
);
}

export function assertIsFunction(
expr: Expression | null | undefined,
operator?: string
): asserts expr is Expression & FunctionInterface {
if (!isFunction(expr, operator)) {
throw new Error(
`Expected function${operator ? ` with operator ${operator}` : ''}`
);
}
}

export function isString(
expr: Expression | null | undefined
): expr is Expression & StringInterface {
Expand Down
28 changes: 27 additions & 1 deletion src/compute-engine/boxed-expression/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
import { _BoxedOperatorDefinition } from './boxed-operator-definition';
import { _BoxedValueDefinition } from './boxed-value-definition';
import { _BoxedExpression } from './abstract-boxed-expression';
import { isNumber, isFunction, isSymbol, numericValue } from './type-guards';
import {
isNumber,
isFunction,
isSymbol,
numericValue,
assertIsFunction,
} from './type-guards';

/**
* Check if an expression contains symbolic transcendental functions of constants
Expand Down Expand Up @@ -452,3 +458,23 @@
value: new _BoxedValueDefinition(ce, name, { type: 'function' }),
};
}
/**
* Get nth (1-based) operand of *expr*; or `null` if this does not exist (or expr is not a
* function).
* Further, if assert is true (**default**: false), will throw if expr. is not a function.
*

Check failure on line 465 in src/compute-engine/boxed-expression/utils.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `·`
* <!--
* @todo?: move to 'function-utils'?
* -->
*
*/
export function getOp(
expr: Expression,
index: number,
assert: boolean = false
): Expression | null {
if (assert) assertIsFunction(expr);
else if (!isFunction(expr)) return null;

return expr.ops[index - 1] || null;
}
1 change: 0 additions & 1 deletion src/compute-engine/global-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export type {
OEISOptions,
OperatorDefinition,
BaseDefinition,
SimplifyOptions,
SymbolDefinition,
SymbolDefinitions,
LibraryDefinition,
Expand Down
46 changes: 0 additions & 46 deletions src/compute-engine/types-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import type {
} from './types-expression';
import type {
EvaluateOptions as KernelEvaluateOptions,
Rule as KernelRule,
BoxedRule as KernelBoxedRule,
BoxedRuleSet as KernelBoxedRuleSet,
Scope as KernelScope,
} from './types-kernel-evaluation';

Expand All @@ -27,9 +24,6 @@ import type {
export interface ComputeEngine {}

type EvaluateOptions = KernelEvaluateOptions;
type Rule = KernelRule<Expression, ExpressionInput, ComputeEngine>;
type BoxedRule = KernelBoxedRule<Expression, ComputeEngine>;
type BoxedRuleSet = KernelBoxedRuleSet<Expression, ComputeEngine>;
type Scope = KernelScope<BoxedDefinition>;

/**
Expand Down Expand Up @@ -517,46 +511,6 @@ export interface BaseDefinition {
readonly isConstant?: boolean;
}

/** Options for `Expression.simplify()`
*
* @category Boxed Expression
*/
export type SimplifyOptions = {
/**
* The set of rules to apply. If `null`, use no rules. If not provided,
* use the default simplification rules.
*/
rules?: null | Rule | ReadonlyArray<BoxedRule | Rule> | BoxedRuleSet;

/**
* Use this cost function to determine if a simplification is worth it.
*
* If not provided, `ce.costFunction`, the cost function of the engine is
* used.
*/
costFunction?: (expr: Expression) => number;

/**
* The simplification strategy to use.
*
* - `'default'`: Use standard simplification rules (default)
* - `'fu'`: Use the Fu algorithm for trigonometric simplification.
* This is more aggressive for trig expressions and may produce
* different results than the default strategy.
*
* **Note:** When using the `'fu'` strategy, the `costFunction` and `rules`
* options are ignored. The Fu algorithm uses its own specialized cost
* function that prioritizes minimizing the number of trigonometric
* functions. Standard simplification is applied before and after the
* Fu transformations using the engine's default rules.
* - `'trig'`: Rewrite exponentials of an imaginary argument to
* trigonometric form via Euler's formula (`e^{iθ} → cos θ + i·sin θ`),
* then simplify. This is the opt-in inverse of the default behavior, which
* keeps `e^{iθ}` in exponential form for a symbolic angle `θ`.
*/
strategy?: 'default' | 'fu' | 'trig';
};

/**
* A table mapping symbols to their definition.
*
Expand Down
17 changes: 17 additions & 0 deletions src/compute-engine/types-evaluation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import type {
BoxedRule as KernelBoxedRule,
BoxedRuleSet as KernelBoxedRuleSet,
EvaluateOptions as KernelEvaluateOptions,
SimplifyOptions as KernelSimplifyOptions,
TransformOptions as KernelTransformOptions,
EvalContext as KernelEvalContext,
ExpressionMapInterface as KernelExpressionMapInterface,
Rule as KernelRule,
RuleConditionFunction as KernelRuleConditionFunction,
MatchConditionFunction as KernelMatchConditionFunction,
RuleFunction as KernelRuleFunction,
RuleReplaceFunction as KernelRuleReplaceFunction,
RuleStep as KernelRuleStep,
Expand All @@ -30,6 +33,18 @@ export type { AssumeResult };
*/
export type EvaluateOptions = KernelEvaluateOptions;

export type SimplifyOptions = KernelSimplifyOptions<
Expression,
ExpressionInput,
ComputeEngine
>;

export type TransformOptions = KernelTransformOptions<
Expression,
ExpressionInput,
ComputeEngine
>;

/**
* Map-like interface keyed by boxed expressions.
*
Expand Down Expand Up @@ -70,6 +85,8 @@ export type RuleConditionFunction = KernelRuleConditionFunction<
ComputeEngine
>;

export type MatchConditionFunction = KernelMatchConditionFunction<Expression>;

/**
* Dynamic rule callback.
*
Expand Down
Loading
Loading