Migrating from Symbolica 2.2 to 3.0
This guide covers the main changes needed when upgrading Symbolica 2.2 to 3.0, including Numerica 3.0 and the API refinements shipped in the 3.0.x bug-fix releases. It describes the current 3.0 API, matching the website's 3.0.7 browser build. See the release post for new features and improvements. If upgrading from an earlier version, first follow the 2.0 and 2.2 migration guides.
Upgrade Checklist
-
Upgrade the Python package:
pip install --upgrade 'symbolica>=3,<4'For Rust, use Rust 1.89 or newer and update the dependency:
cargo add symbolica@3Update direct Numerica and Graphica dependencies to version 3 as well.
-
Replace symbolic equality conditions using
==or!=with.eq()or.ne(). HandleNonefrom undecidable conditions and property queries. -
Replace
solve_linear_system()withsolve()and read assignments from solution branches. -
Update arbitrary-precision result handling and numeric callbacks for
FloatandComplexFloat. -
Review antisymmetric function patterns: matching no longer permutes their arguments.
-
Replace
level_rangewithmin_levelandmax_level; update custom match callback return values. -
Re-export and recompile saved native evaluator libraries. Remove dimension arguments from compiled evaluator
load()calls, and update consumers ofget_instructions(). -
Handle unknown series coefficients explicitly.
-
For Rust, update numeric backend features, algebraic-domain imports, sampling code, and integration grid construction.
-
Replace legacy license keys when prompted and update direct Rust licensing imports and worker authorization.
-
Review custom printers and notebook display code for bounded MathML output.
-
When using Community tensors, preserve their tensor interface and update imports to the current Community namespaces.
Python Comparisons and Conditions
Equality tests and symbolic equations
== and != return Python Booleans. Expressions compare structurally, with
exact-value equality for scalar numbers. Transformers and held expressions
compare by object identity; comparison does not execute them.
Use .eq() and .ne() when you need a deferred Condition, including solver
equations, pattern restrictions, and Transformer branches:
from symbolica import Expression, S, T
x, y, f, w_ = S("x", "y", "f", "w_")
assert (x == x) is True
assert (x == y) is False
# 2.2: cond=(w_ == x)
assert f(x).replace(f(w_), y, cond=w_.eq(x)) == y
# 2.2: T().if_then(T() == x, ...)
transform = T().if_then(T().eq(x), T().replace(x, y))
assert transform(x) == y
held = x.hold(T().replace(x, y))
assert held.eq(y).eval() is True
assert (held == y) is False
# Preserve the equation for the solver.
assert Expression.solve(x.eq(1), [x])[0][x] == 1Ordered operators <, <=, >, and >= also return conditions. Combine
parenthesized relations with &, |, and ~. Python and, or, not, and
chained comparisons request Boolean conversion instead of building a symbolic
formula.
from symbolica import S
x, y = S("x", "y")
condition = (x >= 0) & (x <= 1) & y.ne(0)
assert (x <= 1).eval() is None
assert x.eq(1).eval() is NoneCondition.eval() returns True, False, or None; converting an undecidable
condition to bool raises TypeError. A Transformer if_then with an unknown
condition raises ValueError. Pass equations directly to solve() without
calling eval() or bool() first. Creating an inequality condition does not
imply that solve() supports solving inequalities.
Expression truth, hashing, and sorting
if x == 3: remains a structural test. In contrast, bool(x) raises
TypeError for a general symbolic expression. Numeric expression truth follows
the numeric value: bool(N(0)) is false.
Equal scalar numbers have compatible Python hashes. For example,
N(1) == N(1.0) == 1, and {N(1): "one"}[1] works. Equality uses the exact
stored value: a binary float created with N(0.1) is not the exact rational
N(1) / 10. Symbolic equality does not attempt an algebraic proof.
Ordered comparisons now mean mathematical real ordering. For internal
structural sorting, use compare_structure() explicitly:
from functools import cmp_to_key
from symbolica import S
x, y = S("x", "y")
ordered = sorted([y, x], key=cmp_to_key(lambda a, b: a.compare_structure(b)))This structural order can change between versions. Powers now have length two, matching the base and exponent yielded by iteration.
Property queries can be unknown
is_real(), is_integer(), is_scalar(), is_positive(), and
is_nonnegative() return bool | None. Use is False when a branch requires
a proven negative result; not expr.is_real() also accepts an unknown result.
from symbolica import S
x = S("unknown_property")
r = S("real_property", is_real=True)
assert x.is_real() is None
assert r.is_real() is True
assert (r + 1j).is_real() is False
assert (r**2).is_positive() is None # It could be zero.
assert (r**2).is_nonnegative() is TrueSetting is_real=False leaves the symbol without a realness assumption; it
does not assert that the symbol is non-real. is_finite() and is_constant()
remain Boolean queries.
Testing a zero identity
Structural equality still does not prove an algebraic identity. Use
check_zero()
when simplification and sampling are needed, and handle all three results:
from symbolica import E
identity = E('(x+1)^2-x^2-2*x-1')
assert identity.check_zero(numerical_samples=0) is TrueThe keyword options control finite-field samples, numerical samples, maximum
precision, exact fallback, and whether floating coefficients are treated as
exact stored values. Numerical nonzero tests are heuristic; set
numerical_samples=0 to disable them. A result of None means undecidable,
and a small numerical value is not treated as zero by a tolerance.
Slicing children and mixed arithmetic
Expression indexing accepts negative indices, and slicing returns a Python list of immediate children in iteration order:
from symbolica import S
f, x, y, z = S('f', 'x', 'y', 'z')
assert f(x, y, z)[-1] == z
assert f(x, y, z)[1:] == [y, z]This does not recursively traverse an expression. Arithmetic with a custom
right operand can dispatch to its reflected methods, such as __radd__ and
__rmul__. Extension types should implement those methods, return
NotImplemented for unsupported inputs, and preserve their own return types.
Exact Equation Solving
See Solving equations for a runnable introduction to branches, free variables, and coverage.
Replace Expression.solve_linear_system() with Expression.solve(). An
expression is interpreted as equal to zero; use .eq(rhs) for an explicit
right-hand side. The result is a SolutionSet, whose branches map variables
directly to expressions:
from symbolica import Expression, S, Reals
x, y = S("x", "y")
# 2.2: x_value, y_value = Expression.solve_linear_system(equations, [x, y])
result = Expression.solve([x + y - 3, x - y - 1], [x, y])
branch = result[0]
x_value, y_value = branch[x], branch[y]
assert dict(branch) == {x: 2, y: 1}
roots = Expression.solve(x**2 - 1, [x], domain=Reals)
assert {branch[x] for branch in roots} == {-1, 1}
family = Expression.solve(x + y - 1, [x, y])[0]
assert family[x] == 1 - y
assert family.free_variables() == [y]
assert family.get(y) is NoneThe default domain is Complexes; select Reals, Rationals, or Integers
when needed. Branch order is not guaranteed. len(result) counts branches,
which may represent entire families. Free variables are omitted from the
assignment mapping, and indexing a free variable raises KeyError.
For parameter-dependent systems, inspect result.coverage,
result.coverage_guard, and branch.conditions(). A "generic" answer applies
under the coverage guard; substitute excluded parameter values into the
original equations and solve those cases separately. Converting a branch to a
dictionary extracts assignments only, so retain its conditions alongside them.
bool(result) and result.is_empty() can raise IncompleteCoverage if
emptiness cannot be established. Unsupported problems raise
UnsupportedProblem rather than returning a complete empty solution set.
Algebraic Coefficient Fields
Ordinary .to_polynomial() conversion still treats radicals as independent
variables. In 3.0, use extensions=[] to discover algebraic coefficients,
or supply generators to include a larger field:
from symbolica import E, P
p = E('x+sqrt(2)').to_polynomial(extensions=[])
q = E('x^2-2').to_polynomial(extensions=[E('sqrt(2)')])
formal = P('x^2-2', minimal_poly=P('t^2-2'))Radicals and selected root(...) expressions specify embedded algebraic
numbers. A formal minimal polynomial defines a generator without selecting
a particular root. Existing polynomials can also be converted with
to_number_field; adjoin explicitly composes formal fields. See the
polynomial tutorial for all routes.
For direct expression factorization, the keyword is singular:
E('x^2-2').factor(extension=[E('sqrt(2)')]).
Selecting algebraic roots
Root indices now put real roots first in ascending order, followed by complex
roots ordered by real part and then imaginary part. For example, index zero of
(x**3-x-1).root(0) selects its real root. If code previously stored root
indices, check that each still selects the intended embedding rather than
assuming the old enumeration is unchanged.
Polynomial root isolation
Polynomial.isolate_roots() now returns (IsolatedRoot, multiplicity) pairs
for distinct roots in the complex plane. Use isolate_real_roots() for only
real roots. Code that expects rational interval endpoints should use
isolate_real_root_intervals(), which returns (lower, upper, multiplicity).
An IsolatedRoot provides a certified disk's
center and radius, refine(tolerance), and to_expression() for an exact
symbolic root. The center is a pair of exact real and imaginary parts, not the
root itself.
Numerica Scalars in Python
Constructing and converting numbers
Symbolica exports Numerica's immutable Float and ComplexFloat types.
Arbitrary-precision APIs return these instead of Decimal values or pairs of
Decimal values. Existing Decimal and component-pair inputs remain accepted.
from decimal import Decimal
from symbolica import Float, ComplexFloat, S
x = Float("1.234567890123456789", decimal_digits=80)
y = Float.from_ratio(1, 3, precision=256)
z = ComplexFloat(x, y)
exact = x.to_decimal() # Exact stored binary value as Decimal.
rounded = x.to_decimal(20) # 20 significant digits, ties to even.
real, imag = z.as_tuple() # Float components; also z.real and z.imag.
decimal_parts = z.to_decimal_tuple()
v = S("v")
result = (v*v).evaluate({v: z}, decimal_digit_precision=80)
assert isinstance(result, ComplexFloat)
assert isinstance(result.real.to_decimal(), Decimal)Use precision for bits or decimal_digits for decimal working precision;
specifying both raises ValueError. Copies preserve precision unless an
explicit precision is requested. Native floats use 53 bits; decimal strings
infer precision from significant digits, with a minimum of 53 bits. Construct
high-precision inputs from strings, integers, or exact ratios to avoid first
rounding them through a native float.
Arithmetic tracks accuracy and can change result precision. with_precision()
returns a rounded copy; increasing precision cannot recover lost digits.
Decimal conversion is exact by default and independent of Python's global
decimal context. Both scalar classes are unhashable.
| API | Return type in 3.0 |
|---|---|
Evaluator.evaluate_with_prec(...) |
list[Float] |
Evaluator.evaluate_complex_with_prec(...) |
list[ComplexFloat] |
Expression.evaluate(..., decimal_digit_precision=...) |
ComplexFloat |
Expression.nsolve(...) |
Float, even with a native float initial guess |
Expression.nsolve_system(...) |
list[Float] |
Polynomial.approximate_roots(..., decimal_digit_precision=...) |
list[tuple[ComplexFloat, int]] |
The 32-digit evaluator path also returns these scalars. Machine-precision
expression evaluation and polynomial roots still return native complex numbers;
machine-precision batch evaluators return NumPy arrays when NumPy is installed
and nested Python lists otherwise. Install symbolica[numpy] when downstream
code requires array methods, dtypes, or broadcasting; otherwise consume the
results as nested sequences.
Replace re, im = result with re, im = result.as_tuple() for Float components
or result.to_decimal_tuple() for Decimal components. Use to_decimal() where
a downstream library expects Decimal, and float() or complex() when
deliberately converting to machine precision.
Numeric callbacks
The decimal and decimal_complex callback keys remain, but callback arguments
are now sequences of Float and ComplexFloat, respectively:
from symbolica import S
f = S("numeric_callback", eval={
"decimal": lambda args: args[0].sin(),
"decimal_complex": lambda args: args[0].sin(),
})Tagged callbacks follow the same convention. A constant callback can return
the new scalars without losing precision. Decimal and component-pair callback
results remain accepted. Convert arguments explicitly with to_decimal() or
to_decimal_tuple() when using Decimal-specific operations.
Both types provide elementary functions, including inverse trigonometric and
hyperbolic functions, plus constants such as
Float.pi(decimal_digits=80) and ComplexFloat.i(decimal_digits=80).
Instance methods zero(), one(), and nan() preserve precision.
sample_unit(random.Random(seed)) samples at the working precision;
complex samples have zero imaginary part.
Pattern Matching
Matching levels
Replace level_range=(minimum, maximum) with min_level=minimum and
max_level=maximum in expressions, Transformers, and replacements:
from symbolica import S
x, f = S("x", "f")
expr = x * f(x, f(x))
assert expr.replace(x, 1, min_level=1, max_level=1) == x * f(1, f(x))Bounds are inclusive. min_level defaults to zero and max_level=None removes
the upper bound. Levels count function nesting by default;
level_is_tree_depth=True counts expression tree depth.
Callback decisions
PatternRestriction.req_matches(callback), wildcard.req(callback), and
wildcard.req_cmp(other, callback) share this contract:
| Old match-stack decision | 3.0 return value | Meaning |
|---|---|---|
-1 |
False |
Reject |
0 |
None |
Undecidable |
1 |
True |
Accept |
Return None while required wildcards are missing from the match dictionary.
A returned Condition is evaluated explicitly. Only a true condition accepts
a completed match; negating an unknown condition still yields unknown.
Restrictions on multiple wildcards wait until the values are available.
Antisymmetric function matching
This is a breaking change for rules that rely on argument permutations. In 2.2, the matcher considered permutations of an antisymmetric function's arguments. In 3.0, it matches arguments position by position, just as it does for an ordinary function. This affects both Python and Rust matching APIs.
Calls without wildcards still normalize using antisymmetry: arguments are sorted, odd permutations introduce a minus sign, and repeated arguments yield zero. Calls containing wildcards, including wildcards nested inside arguments, retain their argument order. A pattern's wildcard positions therefore refer to the target function's stored, normalized argument order.
from symbolica import S
f = S("antisymmetric_example", is_antisymmetric=True)
w_, a_, b_ = S("w_", "a_", "b_")
expr = f(1, 2)
# Antisymmetric normalization still applies to concrete calls.
assert f(2, 1) == -expr
assert f(1, 1) == 0
# Matching does not move the second argument into the first position.
assert not expr.matches(f(w_, 1))
assert expr.replace(f(w_, 1), 9) == expr
# Put the fixed argument and wildcard in the target's stored order.
assert expr.replace(f(1, w_), 9) == 9
assert list(expr.match(f(a_, b_))) == [{a_: 1, b_: 2}]Review rules that put a fixed argument in a particular slot, conditions that expect a wildcard to receive any argument, and code that counts or iterates over matches. If a rule must cover several argument orders, provide those patterns explicitly. When a rule expresses an antisymmetric identity, carry the appropriate permutation sign into its replacement; swapping arguments alone is not sufficient. Avoid assuming that the internal canonical order of symbolic arguments is stable across versions.
Evaluators
Loading compiled libraries
Compiled libraries now carry input and output dimensions. Remove input_len
and output_len from Python load() calls:
from symbolica import CompiledRealEvaluator
# 2.2: CompiledRealEvaluator.load("test.so", "my_fun", input_len, output_len)
compiled = CompiledRealEvaluator.load("test.so", "my_fun")This applies to real, complex, SIMD, and CUDA evaluators. CUDA still accepts
number_of_evaluations and block_size as runtime settings. Re-export and
recompile older libraries once to include the metadata. Loading a library
without it raises an error with rebuild instructions.
Precision evaluators raise ValueError for invalid input counts or unsupported
precision, and remain usable after a failed call.
Exported instructions and function definitions
get_instructions() now returns EvaluatorInstructions:
from symbolica import S, FunctionDefinition
x, y, f = S("x", "y", "f")
evaluator = f(x).evaluator([x], functions=[
FunctionDefinition(f, [y], y**2, inlining="never"),
])
# 2.2: instructions, temporary_count, constants = evaluator.get_instructions()
exported = evaluator.get_instructions()
instructions = exported.instructions
temporary_count = exported.temporary_count
constants = exported.constants
assert exported.input_count == exported.output_count == 1
assert len(exported.sub_evaluators) == 1Custom instruction consumers must handle sub_evaluators, whose entries carry
the function, tags, and nested evaluator body. Function-call instructions can
refer to those bodies. Choose inlining="always" to request inlining,
"never" to retain calls, or "auto" for the default policy.
Pass FunctionDefinition objects to functions=....
Function symbols and tags in the instruction stream are Expression objects,
not strings. In particular, EvaluatorFunction.tags is a list of expressions.
Do not assume that an instruction object can be serialized directly as JSON.
Registered constants and fixed-argument calls, such as pi and exp(1), have
entries in exported.constant_functions. Each
EvaluatorConstantFunction
provides read-only index, function, tags, and arguments properties.
Code generators must compute these entries at their target precision and fill
the corresponding constant slots; their exact placeholders are not the values
of the constants.
Use Evaluator.from_instructions() to reconstruct a validated evaluator:
from symbolica import E, S, Evaluator
x = S('x')
original = E('x^2+1').evaluator([x], jit_compile=False, n_cores=1)
restored = Evaluator.from_instructions(original.get_instructions())
assert restored.evaluate([[3.0]])[0][0] == 10Retained bodies and registered constant definitions are preserved. External
callback implementations must still be registered on their symbols. For
transfer to another thread or process, Evaluator.save() and Evaluator.load()
include and restore the necessary symbolic state; callbacks are not serialized.
Regenerate older saved artifacts rather than relying on private binary formats
remaining identical between builds.
Notebook Output and Custom Printers
formatted() now uses bounded, interactive MathML by default. To retain the
highlighted text presentation, use expr.formatted(mathml=False).
max_terms controls the legacy text view; use page_size to bound the number
of entries requested in a MathML page.
from symbolica import S
x, y, z = S('x', 'y', 'z')
expr = ((x+y+z)**20).expand()
viewer = expr.mathml_viewer(page_size=25)
page = viewer.page() # Bounded HTML and metadata; no widget import required.
assert page['end'] <= 25
viewer.close()Omitting page_size allows automatic previews to include a compact complete
product when it fits. An explicit size remains a maximum, and rendering budgets
can show fewer entries. Custom integrations can use the shared
MathMLViewer with a
MathMLPageSource; javascript() supplies
the standalone browser renderer. The website and Gallery provide this
integration without loading AnyWidget. Other notebook environments need the
display integration appropriate to their frontend.
Custom print callbacks may now receive PrintMode.MathML. Return a bounded
MathML fragment for that mode, or None to use the native renderer; plaintext,
LaTeX, and Typst strings are not interchangeable with MathML. Symbol printers
can also use a dictionary mapping mode names or PrintMode members to strings.
Community Tensor and Graph APIs
Use symbolica.community.tensor for tensor algebra and
symbolica.community.hepkit for particle-physics tools. The older spenso
import forwards to tensor, and the older Community vakint import forwards
to hepkit.vakint. Former Idenso simplifications are methods on
TensorExpression.
Tensor-aware transformations preserve the ordered external-axis interface.
Use outer() to keep axes independent and contract_ports() or compose()
to choose explicit contractions or matrix channels. Use to_expression()
when deliberately dropping the tensor interface. Inherited scalar
transformations can return ordinary expressions, so check the return type when
continuing with tensor operations.
For interactive tensor output, use tensor_expr.paged(settings=...) rather
than the inherited scalar viewer factory. It retains tensor notation and index
labels. The direct MathML pager does not accept custom Typst
notation_source; use to_typst_project() or to_svg() for that output.
Graph objects can be displayed directly in the Gallery, where their interactive
SVG controls are isolated from the notebook's reactive output tree.
Series Coefficients
See Series expansions for runnable examples of the coefficient boundary and fractional powers.
Accessing an unknown coefficient now differs from accessing a known zero.
Python indexing and get_coefficient() raise IndexError at or beyond the
series' absolute order:
from symbolica import N, S
x = S("x")
series = x.exp().series(x, 0, 2)
assert series[2] == N(1) / 2
assert series[-1] == 0
assert series.get_absolute_order() == (3, 1)
try:
series[3]
except IndexError:
pass # The coefficient of x**3 has not been computed.
else:
raise AssertionError("Expected an unknown coefficient")Negative indices denote negative powers, not offsets from the end. Rational
exponents are supported, and powers refer to the variable minus the expansion
point. Requested depth is inclusive; get_absolute_order() is the exclusive
boundary of known coefficients. For fractional expansions, use that reported
boundary instead of assuming depth + 1.
Use to_expression() to deliberately discard the remainder before extracting
coefficients from an ordinary expression.
Rust and Numerica API Changes
Numeric backends and WebAssembly
The old gmp and no_gmp features are replaced by independent backend choices:
| 2.2 feature selection | 3.0 feature selection |
|---|---|
gmp |
integer-gmp, float-mpfr |
no_gmp |
integer-malachite, float-astro |
Select exactly one integer backend and one floating-point backend. GMP/MPFR remain the defaults. Disable defaults when choosing alternatives:
[dependencies]
symbolica = { version = "3", default-features = false, features = [
"integer-malachite", "float-astro", "native_code_generation"
] }The same backend names apply to direct Numerica dependencies. Cargo unifies
features across dependencies, so ensure all dependencies select compatible
backends. Native evaluator compilation and JIT APIs require
native_code_generation, which is enabled by default in Symbolica.
For WebAssembly, use default-features = false, features = ["wasm"].
This selects Malachite/Astro backends and the smaller polynomial-code mode;
native compilation and JIT are unavailable in that configuration.
Rust builds that expose the C bindings must enable the separate c_api
feature; those bindings are no longer compiled unconditionally.
Solving and algebraic domains
Replace the legacy linear-system entry point with the solve builder:
use symbolica::prelude::*;
let x = parse!("x");
let y = parse!("y");
let equations = [parse!("x+y-3"), parse!("x-y-1")];
let solutions = AtomView::solve(&equations)
.over(Reals)
.wrt(&[x, y])?;
for (variable, value) in &solutions[0] {
println!("{variable} = {value}");
}Use .wrt_with_exponent::<E, _>() when selecting polynomial exponent storage
explicitly. The same solution-coverage and free-variable considerations as in
Python apply.
The symbolica::domains::algebraic_number module moved to
symbolica::domains::algebraic. Update explicit imports of
AlgebraicExtension and AlgebraicNumber, or use the prelude. The new module
also exposes Root, AlgebraicContext, AlgebraicEmbedding, and
AlgebraicQuotient for root-aware and formal algebraic computations.
Property checks, matching, series, and compiled evaluation
- Mathematical expression property methods now return
ConditionResult. Use.is_true()or.is_false()to require a proof, or convert toOption<bool>to handle the unknown case explicitly. Replacement,ReplaceBuilder, andMatchSettingsuse.min_level(1).max_level(1)instead of.level_range(...)..max_level(None)clears the upper bound.Series::coefficient(exponent)returnsOption<F::Element>:Nonemeans the coefficient is unknown, whereasSome(0)means it is known to vanish. This includes missing terms below the leading term. PropagateNonewith?in custom callbacks when appropriate, or useto_atom()to explicitly discard the remainder.- Compiled evaluators expose
get_input_len()andget_output_len()per evaluation. Safeevaluate()calls check slice lengths before entering native code, andevaluate_batch()returns an error for incompatible shapes.
Ring ordering and sampling
Random sampling moved out of Ring into SampleableRing, with an associated
SamplingPolicy. Generic code that samples ring elements should require that
trait and pass a policy by reference. To embed a uniformly sampled integer in
any ring, use sample_small_integer() or sample_integer() instead:
use symbolica::prelude::*;
let mut rng = symbolica::rand::rng();
// 2.2: Q.sample(&mut rng, (-4, 7))
let small = Q.sample_small_integer(&mut rng, -4..=7);
let policy = Integer::from(5)..=Integer::from(9);
let integer = Z.sample(&mut rng, &policy);These ranges are inclusive. RNG traits and generators are re-exported through
symbolica::rand and numerica::rand; Symbolica's prelude includes Rng,
RngCore, and SeedableRng.
Use OrderedRing::cmp() for mathematical total ordering or
RealEmbedding::try_cmp() / try_sign() for a comparison that may fail.
InternalOrdering::internal_cmp() provides an internal structural order and
should not be used to infer mathematical signs.
Custom rings can expose optimized bulk operations through Ring::kernels();
the default provides no specialized kernels. Owned division helpers include
try_div_owned() and quot_rem_owned(). Use exact_div_owned() only when
divisibility is already established.
Shared extensions and bounded computations
Optional Python implementations can register through Rust's
ExtensionPoint and ExtensionImplementation in symbolica::api::python.
Keep participating components in one native library with one Symbolica kernel
and shared PyO3 class identities. Use the actual Rust types to pass objects
between components instead of importing another component's Python module to
find its implementation. The owning class still defines each public method
and its stub; registration supplies the implementation.
The Rust API also adds tracked numerical evaluation, polynomial and rational reconstruction, and shared cooperative memory and elapsed-time budgets. Tracked values retain uncertainty separately from working precision; increasing precision does not make an uncertain input exact. Fallible reconstruction oracles and nested resource scopes propagate failures rather than silently continuing with a partial result.
Integer representation
Integer::Double now contains DoubleInteger instead of a bare i128.
Code that directly constructs or matches that variant must convert the payload:
use symbolica::domains::integer::Integer;
let value = Integer::Double((1_i128 << 80).into());
if let Integer::Double(inner) = value {
let raw: i128 = inner.get();
assert_eq!(raw, 1_i128 << 80);
}Prefer Integer::from(value) when constructing an integer so that Numerica
chooses its representation.
Integration grids and matrices
ContinuousGrid::new() and DiscreteGrid::new() now return Result<_, String>.
Propagate or handle construction errors:
use symbolica::numerical_integration::{ContinuousGrid, DiscreteGrid, Grid};
let continuous = ContinuousGrid::<f64>::new(2, 128, 100, None, false)?;
let discrete = DiscreteGrid::new(
vec![Some(Grid::Continuous(continuous))], 0.01, false,
)?;Continuous dimensions and bin counts must be positive. A supplied bin-number evolution must be nonempty and contain only positive counts. Invalid discrete grid settings are also rejected.
Sparse matrix constructors validate CSR structure, including ordered, unique
column indices within each row. Use SparseMatrix::try_from_csr() to receive
an error; from_csr() and from_csr_slices() panic on invalid input. Dense
matrix shape validation and singular-matrix inversion checks are stricter.
Matrix::row_iter() now returns an iterator of rows instead of a concrete
std::slice::Chunks type; update explicit type annotations.
Licensing
Symbolica 3.0 includes the Symbolica Source-Available License 1.0; Numerica remains MIT licensed. See the license information and the license supplied with the package.
Legacy keys are processed with an outdated-key warning instead of being
rejected solely for their old format. Replace them to use the current signed
format and offline validation. The new format is
S-<user-id>-YYYY.MM.DD-<signature>; the visible date expires at midnight UTC
on that date. Obtain a replacement through the
license page. Activation still uses Python's
set_license_key(), Rust's LicenseManager::set_license_key(), or
SYMBOLICA_LICENSE.
Rust licensing APIs now live in symbolica::license:
use symbolica::license::LicenseManager;The root-level symbolica::LicenseManager path was removed;
symbolica::prelude::* still includes it. ExecutionCapabilities,
LibraryUnlock, and LibraryUnlockGuard also live in symbolica::license.
The activate_oem_license! and register_library_unlock! macros are exported
at the crate root. Library authors can use signed library keys for scoped
multicore unlocking; Python packages register them with
register_library_unlock() from the package named by the key.
Dependent Rust libraries can propagate an authorized operation to their
workers with InheritedLibraryUnlock. Capture on the calling thread and keep
an activation guard alive inside each worker:
use symbolica::license::InheritedLibraryUnlock;
let unlock = InheritedLibraryUnlock::capture();
std::thread::scope(|threads| {
threads.spawn(|| {
let _guard = unlock.activate();
// Run this library's Symbolica operations here.
});
});Keep the captured handle within the licensed library operation. Rust workers without Python thread state no longer try to acquire the GIL just to inspect Python authorization frames; dependent libraries still need to propagate their caller's authorization at the worker boundary.
Common Before/After Summary
| Area | 2.2 | 3.0 |
|---|---|---|
| Deferred equality | w_ == x, T() == x |
w_.eq(x), T().eq(x) |
| Deferred inequality | w_ != x |
w_.ne(x) |
| Unknown properties | Boolean checks | Handle None in Python / ConditionResult in Rust |
| Linear solving | solve_linear_system(...) |
solve(...), then read a branch's assignments |
| High-precision results | Decimal or Decimal pairs | Float or ComplexFloat; convert explicitly |
| Match levels | level_range=(a, b) |
min_level=a, max_level=b |
| Antisymmetric matching | Argument permutations considered | Positional matching; wildcard patterns retain argument order |
| Match-stack decisions | -1, 0, 1 |
False, None, True |
| Compiled evaluator loading | Caller supplies dimensions | Dimensions loaded from rebuilt library |
| Instruction export | Three-item tuple | EvaluatorInstructions with nested bodies |
| Instruction constants and tags | Placeholder slots / string assumptions | Resolve constant_functions; names and tags are expressions |
| Machine-precision batches | NumPy assumed | Arrays with NumPy; nested lists without it |
| Notebook formatting | Highlighted text settings | Bounded MathML by default; mathml=False for text |
| Root indices | Previous enumeration | Real roots first, then complex roots |
| Unknown series coefficient | Indistinguishable from zero | IndexError in Python / None in Rust |
| Rust backend features | gmp, no_gmp |
Independent integer and float features |
| Rust grid construction | Grid value | Result |
| Rust algebraic module | domains::algebraic_number |
domains::algebraic |
| Rust licensing import | symbolica::LicenseManager |
symbolica::license::LicenseManager |