SSDS Documentation

A guide to discovering recurring symbolic structures and constructing reusable mathematical abstractions.

Research softwareSSDS discovers recurring symbolic structures across expression banks and generates candidate mathematical abstractions.

Installation

Install the required dependencies and run SSDS from the project environment.

pip install ssds-core==0.1.0

Quickstart

Create an expression bank and pass it to run_ssds().

from Convert import make_bank
from run_ssds import run_ssds

bank = make_bank([
    "Newton's Second Law: F = m*a",
    "Kinetic Energy: KE = 1/2*m*v**2",
])

results = run_ssds(
    bank,
    alpha=0.5,
    beta=0.5,
    verbose=True,
)

Your First Discovery

SSDS validates the expression bank, extracts symbolic and behavioral features, and compares expressions. It then clusters related forms, generalizes recurring structures, and filters candidate results.

How SSDS Works

SSDS processes an expression bank through these stages:

Expression Bank
    ↓
Structure Extraction
    ↓
Behavior Analysis
    ↓
Similarity
    ↓
Clustering
    ↓
Anti-Unification
    ↓
Candidate Filtering
    ↓
Candidate Analysis
    ↓
Optional AI Interpretation

Symbolic Structure

SSDS extracts structural features from each expression, including tree topology, operators, nesting, and variable relationships.

Structural similarity can remain high even when numerical behavior differs. Behavioral analysis handles that distinction.

Behavioral Signatures

SSDS samples numerical responses across input ranges to characterize boundedness, monotonicity, periodicity, variance, derivative behavior, integral behavior, and stability. Sampling ranges and parameter values affect the resulting behavior signatures.

Similarity

SSDS combines structural and behavioral similarity using alpha and beta.

results = run_ssds(
    bank,
    alpha=0.5,
    beta=0.5,
)

Increase alpha to emphasize structure. Increase beta to emphasize behavior. Do not assume the values must sum to 1 unless the implementation enforces that constraint.

Generalization

Anti-unification replaces differing subexpressions while preserving shared symbolic structure. The result represents a candidate abstraction; it does not prove independent mathematical significance.

Novelty Detection

SSDS applies novelty_threshold during candidate filtering.

results = run_ssds(
    bank,
    novelty_threshold=0.35,
)

Candidates can be classified as novel, repeated, or discarded depending on the filtering logic.

Expression Banks

Create expression banks with make_bank(). The function parses equations into SymPy expressions.

from Convert import make_bank

bank = make_bank([
    "Newton's Second Law: F = m*a",
    "Kinetic Energy: KE = 1/2*m*v**2",
    "Momentum: p = m*v",
])

You can also pass a multiline string:

bank = make_bank("""
F = m*a
E = m*v**2/2
p = m*v
""")

Running SSDS

results = run_ssds(
    bank,
    verbose=True,
)

Override configuration values explicitly when needed:

results = run_ssds(
    bank,
    alpha=0.5,
    beta=0.5,
    sim_threshold=0.85,
    novelty_threshold=0.35,
    verbose=True,
)

AI Interpretation

Alpha feature: AI interpretation is currently in alpha testing and is unreliable. It can produce incorrect, incomplete, or misleading interpretations. Treat its output as an experimental hypothesis, not a validated mathematical or scientific result.

Set ai_enabled=True to enable the optional AI interpretation stage. Use index to select the zero-based expression passed to the AI system.

bank = make_bank([
    "Reynolds Number: Re = U*L/nu",       # index 0
    "Froude Number: Fr = U/sqrt(g*L)",    # index 1
    "Mach Number: Ma = U/c",              # index 2
])

results = run_ssds(
    bank,
    ai_enabled=True,
    index=1,
)

This selects bank[1] for AI interpretation. index does not determine the starting point of symbolic discovery. Set index=None when no specific expression should be selected.

Configuration

Pass configuration values directly to run_ssds(). When a value is omitted, SSDS uses the configured default.

See the run_ssds() reference for the full parameter list and defaults.

Discovery Results

See Result Objects for what run_ssds() returns.

Interpreting SSDS Results

An SSDS run produces several layers of output. The first block describes the candidate structure. The following blocks show how each member expression was normalized, evaluated, and vectorized. The final blocks report mathematical characterization, and, when AI interpretation is enabled, optional LLM processing.

Canonical Example

── novel structure #4 ──────────────────────────────────
      [4] v0*v1*v2*φ1*φ2
         behavior       : exponential
         bounded        : False  boundedness=0.00
         periodic       : False  monotone=False
         variance       : 1.148e+15
         derivative     : mean=5.089e+07
         integral       : mean=1.731e+08
         stability      : -5.729e+04
         sensitivity    : 1.051e-06
         members        : ['v0*v1*v2*v3*v4', '0.5*v0*v1*v2*v3**2']
         novelty        : 48.2  |  closest: sq
    Composition: Op5Eq1(Op5Eq1(w, b, -4, 5, 1), z, 4, Op5Eq1(-5, 5, 2, 2.71, 3.14), Op5Eq1(m, t, m, q, -10))
    Variables:   ['b', 'm', 'q', 't', 'w', 'z']
    Numpy expr:  ((((w)*(b)*(-4)*(5)*(1)))*(z)*(4)*(((-5)*(5)*(2)*(2.71)*(3.14)))*(((m)*(t)*(m)*(q)*(-10))))
    Result:      -490141440.00000006
    Vectorized:  [-3.40376000e-02 -1.30359431e+02 -5.18200415e+03 -5.71571871e+04
     -3.40376000e+05]

    Composition: Op5Eq2(w, Op5Eq2(5, q, n, -2, p), Op5Eq2(y, 3.14, z, -10, w), 10, p)
    Variables:   ['n', 'p', 'q', 'w', 'y', 'z']
    Numpy expr:  ((w)*(((5)*(q)*(n)*(-2)*(p)))*(((y)*(3.14)*(z)*(-10)*(w)))*(10)*(p))
    Result:      18086400.0
    Vectorized:  [3.14000000e-05 3.90837778e-01 2.62924565e+01 4.08642251e+02
     3.14000000e+03]

    Composition: Op5Eq3(-4, -4, Op5Eq3(c, u, q, -3, u), Op5Eq3(3.14, 10, m, z, -1), Op5Eq3(-3, -3, w, 0.5, w))
    Variables:   ['c', 'm', 'q', 'u', 'w', 'z']
    Numpy expr:  ((-4)*(-4)*(((c)*(u)*(q)*(-3)*(u)))*(((3.14)*(10)*(m)*(z)*(-1)))*(((-3)*(-3)*(w)*(0.5)*(w))))
    Result:      97666560.00000001
    Vectorized:  [6.78240000e-05 8.44209601e-01 5.67917060e+01 8.82667263e+02
     6.78240000e+03]

    Composition: Op5Eq4(Op5Eq4(-1, u, b, z, -10), -3, -4, z, -2)
    Variables:   ['b', 'u', 'z']
    Numpy expr:  ((((-1)*(u)*(b)*(z)*(-10)))*(-3)*(-4)*z*(-2))
    Result:      -4320.0
    Vectorized:  [-2.40000000e-02 -2.67759375e+00 -2.19615000e+01 -8.65800938e+01
     -2.40000000e+02]

    Composition: Op5Eq5(Op5Eq5(5, x, a, 5, 3.14), Op5Eq5(1, x, 0.5, 4, 4), Op5Eq5(n, 4, b, c, 3.14), Op5Eq5(-2, x, p, 10, u), -1)
    Variables:   ['a', 'b', 'c', 'n', 'p', 'u', 'x']
    Numpy expr:  ((((5)*(x)*(a)*(5)*(3.14)))*(((1)*(x)*(0.5)*(4)*(4)))*(((n)*(4)*(b)*(c)*(3.14)))*(((-2)*(x)*(p)*(10)*(u)))*(-1))
    Result:      38958829056.0
    Vectorized:  [1.57753600e-04 6.38159924e+00 7.26513158e+02 1.59108947e+04
     1.57753600e+05]

    Composition: Op5Eq6(Op5Eq6(3, 3.14, b, 3, c), -10, Op5Eq6(5, -5, n, -4, 2), Op5Eq6(u, m, 5, u, -5), Op5Eq6(y, z, w, t, 0.5))
    Variables:   ['b', 'c', 'm', 'n', 't', 'u', 'w', 'y', 'z']
    Numpy expr:  ((((3)*(3.14)*(b)*(3)*(c)))*(-10)*(((5)*(-5)*(n)*(-4)*(2)))*(((u)*(m)*(5)*(u)*(-5)))*(((y)*(z)*(w)*(t)*(0.5))))
    Result:      1538248319999.9998
    Vectorized:  [7.06500000e-05 9.28850408e+00 1.78953032e+03 5.52241692e+04
     7.06500000e+05]

    Composition: Op5Eq7(Op5Eq7(3, 3, t, -4, 1), -10, Op5Eq7(-3, q, 2.71, u, 5), 1, 2)
    Variables:   ['q', 't', 'u']
    Numpy expr:  ((((3)*(3)*(t)*(-4)*(1)))*(-10)*(((-3)*(q)*(2.71)*(u)*(5)))*(1)*(2))
    Result:      -175607.99999999997
    Vectorized:  [   -29.268      -1004.7155625  -4869.4635    -13623.7966875
     -29268.       ]

    Composition: Op5Eq8(z, -10, Op5Eq8(-3, 10, z, -10, 1), t, Op5Eq8(c, q, 2.71, n, 0.5))
    Variables:   ['c', 'n', 'q', 't', 'z']
    Numpy expr:  ((z)*(-10)*(((-3)*(10)*(z)*(-10)*(1)))*(t)*(((c)*(q)*(2.71)*(n)*(0.5))))
    Result:      -2438999.9999999995
    Vectorized:  [-4.06500000e-03 -4.79027797e+00 -1.12521804e+02 -8.80786734e+02
     -4.06500000e+03]

    Composition: Op5Eq9(Op5Eq9(-5, t, y, 5, m), -1, -10, Op5Eq9(4, c, z, u, -1), -2)
    Variables:   ['c', 'm', 't', 'u', 'y', 'z']
    Numpy expr:  ((((-5)*(t)*(y)*(5)*(m)))*(-1)*(-10)*(((4)*(c)*(z)*(u)*(-1)))*(-2))
    Result:      -1440000.0
    Vectorized:  [-2.00000000e-03 -2.35684033e+00 -5.53612813e+01 -4.33351407e+02
     -2.00000000e+03]

    Composition: Op5Eq10(z, -1, Op5Eq10(w, -5, w, -4, 2), 3.14, Op5Eq10(-2, a, t, b, z))
    Variables:   ['a', 'b', 't', 'w', 'z']
    Numpy expr:  ((z)*(-1)*(((w)*(-5)*(w)*(-4)*(2)))*(3.14)*(((-2)*(a)*(t)*(b)*(z))))
    Result:      602880.0
    Vectorized:  [2.51200000e-05 9.62062224e-02 3.82435731e+00 4.21824259e+01
     2.51200000e+02]

    Concept: discovered 20 invariants across 5 families. 20 invariants expressed as operator laws. Closed under 20 operations. 4 parameter recursion patterns.
    Operator definition: v0*v1*v2*φ1*φ2
    Parameters: ['v0', 'v1', 'v2', 'φ1', 'φ2']
    Families:
      - derivative: Differentiation produces scaled/self-similar operator variants.
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v0), v1*v2*φ1*φ2)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v1), v0*v2*φ1*φ2)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v2), v0*v1*φ1*φ2)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), φ1), v0*v1*v2*φ2)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), φ2), v0*v1*v2*φ1)
      - second: Family for second invariants.
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v0, 2)), 0)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v1, 2)), 0)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v2, 2)), 0)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (φ1, 2)), 0)
        * Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (φ2, 2)), 0)
      - integral: Family for integral invariants.
        * Eq(Integral(Op(v0, v1, v2, φ1, φ2), v0), v0**2*v1*v2*φ1*φ2/2)
        * Eq(Integral(Op(v0, v1, v2, φ1, φ2), v1), v0*v1**2*v2*φ1*φ2/2)
        * Eq(Integral(Op(v0, v1, v2, φ1, φ2), v2), v0*v1*v2**2*φ1*φ2/2)
        * Eq(Integral(Op(v0, v1, v2, φ1, φ2), φ1), v0*v1*v2*φ1**2*φ2/2)
        * Eq(Integral(Op(v0, v1, v2, φ1, φ2), φ2), v0*v1*v2*φ1*φ2**2/2)
      - composition: Family for composition invariants.
        * Eq(Op(v0, v1, v2, φ1, φ2)**2, Op(v0**2, v1**2, v2**2, φ1**2, φ2**2))
      - parameter: Family for parameter invariants.
        * Eq(Op(v0, v1, v2, φ1 - 1, φ2), v0*v1*v2*φ2*(φ1 - 1))
        * Eq(Op(v0, v1, v2, φ1 + 1, φ2), v0*v1*v2*φ2*(φ1 + 1))
        * Eq(Op(v0, v1, v2, φ1, φ2 - 1), v0*v1*v2*φ1*(φ2 - 1))
        * Eq(Op(v0, v1, v2, φ1, φ2 + 1), v0*v1*v2*φ1*(φ2 + 1))

    Invariants (operator form prioritised):
      [OP] composition_closure  wrt=None   [closed]
        Eq(Op(v0, v1, v2, φ1, φ2)**2, Op(v0**2, v1**2, v2**2, φ1**2, φ2**2))
      [OP] derivative           wrt=v0     [closed] (shift: -2)
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v0), v1*v2*φ1*φ2)
      [OP] derivative           wrt=v1     [closed] (shift: -2)
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v1), v0*v2*φ1*φ2)
      [OP] derivative           wrt=v2     [closed] (shift: -2)
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), v2), v0*v1*φ1*φ2)
      [OP] derivative           wrt=φ1     [closed] (shift: -2)
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), φ1), v0*v1*v2*φ2)
      [OP] derivative           wrt=φ2     [closed] (shift: -2)
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), φ2), v0*v1*v2*φ1)
      [OP] integral_closure     wrt=v0     [closed]
        Eq(Integral(Op(v0, v1, v2, φ1, φ2), v0), v0**2*v1*v2*φ1*φ2/2)
      [OP] integral_closure     wrt=v1     [closed]
        Eq(Integral(Op(v0, v1, v2, φ1, φ2), v1), v0*v1**2*v2*φ1*φ2/2)
      [OP] integral_closure     wrt=v2     [closed]
        Eq(Integral(Op(v0, v1, v2, φ1, φ2), v2), v0*v1*v2**2*φ1*φ2/2)
      [OP] integral_closure     wrt=φ1     [closed]
        Eq(Integral(Op(v0, v1, v2, φ1, φ2), φ1), v0*v1*v2*φ1**2*φ2/2)
      [OP] integral_closure     wrt=φ2     [closed]
        Eq(Integral(Op(v0, v1, v2, φ1, φ2), φ2), v0*v1*v2*φ1*φ2**2/2)
      [OP] parameter_recursion  wrt=φ1     [closed] (shift: -1)
        Eq(Op(v0, v1, v2, φ1 - 1, φ2), v0*v1*v2*φ2*(φ1 - 1))
      [OP] parameter_recursion  wrt=φ1     [closed] (shift: 1)
        Eq(Op(v0, v1, v2, φ1 + 1, φ2), v0*v1*v2*φ2*(φ1 + 1))
      [OP] parameter_recursion  wrt=φ2     [closed] (shift: -1)
        Eq(Op(v0, v1, v2, φ1, φ2 - 1), v0*v1*v2*φ1*(φ2 - 1))
      [OP] parameter_recursion  wrt=φ2     [closed] (shift: 1)
        Eq(Op(v0, v1, v2, φ1, φ2 + 1), v0*v1*v2*φ1*(φ2 + 1))
      [OP] second_derivative    wrt=v0     [closed]
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v0, 2)), 0)
      [OP] second_derivative    wrt=v1     [closed]
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v1, 2)), 0)
      [OP] second_derivative    wrt=v2     [closed]
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (v2, 2)), 0)
      [OP] second_derivative    wrt=φ1     [closed]
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (φ1, 2)), 0)
      [OP] second_derivative    wrt=φ2     [closed]
        Eq(Derivative(Op(v0, v1, v2, φ1, φ2), (φ2, 2)), 0)
    Closure properties: ['composition']
How to read these labelsField names describe what the analyzer tested, not formal mathematical properties — "novel," "closed," "stability," and "behavior" are internal classifications from numerical sampling, not proofs. Validate any candidate against the source equations before treating it as a result.
FieldMeaningCaveat
novel structure #NCandidate index or rank and its current novelty-filter category.“Novel” is an internal category, not proof of mathematical novelty.
behaviorBehavior classification reported for the analyzed sample.Interpret it using the configured numerical analyzer and sampling inputs.
bounded / periodic / monotoneBoolean classifications for the corresponding sampled properties; boundedness is the accompanying score.False means the implemented test did not classify the sample as satisfying the property. It is not a universal mathematical negation.
varianceSpread of the sampled numerical values.Scale depends on input ranges, parameters, normalization, and numeric dtype.
derivative / integral / stability / sensitivityReported derivative, integral, stability, and sensitivity metrics from the configured analyzer.These names do not establish units-aware or formal meanings such as Lyapunov stability. Missing or non-finite calculations require implementation-specific handling.
membersExpressions associated with the candidate structure and selected cluster.Compare them with the source bank and compositions to determine whether they are original, canonicalized, transformed, or expanded expressions for that run.
novelty / closestInternal novelty ranking/filtering quantity and the closest comparison label or identifier. In this run, sq is an internal label for the nearest previously seen candidate, and its expansion is not shown here.The score is not a probability and does not establish scientific or mathematical novelty.
Composition / Op5EqNGenerated structural representation of a member expression; nested wrappers preserve nested structure.These are internal five-argument structural wrappers, not user-facing mathematical operators or guaranteed executable code.
VariablesDistinct symbolic variables detected in the composition; constants are excluded.Ordering follows variable collection in the implementation, and generalized φ parameters may be separate from concrete member variables.
Numpy exprGenerated arithmetic representation used for numerical evaluation.It is diagnostic generated output. Unsupported expressions or conversion failures require evaluator-specific handling.
ResultScalar from one concrete substitution/evaluation of a composition.It is not necessarily the final candidate result. Floating-point values should be compared with tolerances.
VectorizedMultiple evaluations over the run’s input vector, used as numerical samples for behavioral characterization.Exact vector, dtype, and NaN/Inf handling are implementation details. For example, -3.40376000e-02 is approximately -0.0340376.
Concept blockCounts and named families produced by the mathematical characterization stage, including invariants, operator laws, operations, and parameter recursion patterns.These counts are analyzer output and should not be read as stronger formal claims than the recorded checks.
Operator definition / ParametersParameterized candidate expression and the generated symbols used for derivative, integral, composition, and shift checks.This is an internal operator model, not automatically a formally defined mathematical operator.
FamiliesCategories such as derivative, second, integral, composition, and parameter for tested symbolic relationships.The family names describe analyzer groupings, not independent mathematical theories.
Eq(...)Machine-readable equality between symbolic left and right expressions, such as a derivative relationship.It is symbolic analyzer output, not prose or a scientific interpretation.
[closed] / shiftStatus and parameter-shift metadata attached to an invariant record.[closed] reflects the implemented recognition/closure condition; it does not imply closure under every abstract operation.
Closure propertiesNamed operations recognized as closure properties, such as composition.Fewer names than individual closed records is expected because multiple records can belong to one property.

Output Layers

candidate structure
    ↓
member expressions
    ↓
symbolic composition
    ↓
numeric evaluation
    ↓
behavioral characterization
    ↓
novelty/filtering metadata
    ↓
optional LLM processing
    ↓
operator characterization
    ↓
invariant/relationship records

The candidate, members, composition, operator, and invariant equations are symbolic or structural layers. Results and vectorized values are numerical layers. Novelty and category labels are filtering metadata. LLM grouping and concept text are optional AI-generated output. Derivative, integral, second-derivative, parameter-shift, and closure records are mathematical-analysis output produced by the configured analyzer. Structured return values are the appropriate machine-readable source; much of the printed detail is diagnostic.

Use the Python object returned by run_ssds() as the primary result. The exact schema is implementation-defined by the current project version; this documentation does not invent a field layout that is not exposed by the source. Use verbose output for human inspection, and inspect the returned object or its documented result classes for programmatic processing.

run_ssds()

Runs the full SSDS discovery pipeline on an expression bank.

run_ssds(
    bank,
    api=None,
    ai_enabled=False,
    alpha=None,
    beta=None,
    sim_threshold=None,
    novelty_threshold=None,
    verbose=True,
    index=None,
)
ArgumentPurpose
bankExpression bank to analyze.
apiOptional API interface for supported AI functionality.
ai_enabledEnable AI interpretation.
alphaStructural similarity weight.
betaBehavioral similarity weight.
sim_thresholdSimilarity threshold.
novelty_thresholdNovelty threshold.
verboseEnable diagnostic output.
indexSelect the expression for AI interpretation.

make_bank()

Parse equations, optionally canonicalize variables, and save a JSON expression bank.

from Convert import make_bank

bank = make_bank([
    "Newton's Second Law: F = m*a",
    "Kinetic Energy: KE = 1/2*m*v**2",
  ], output_path="physics_bank.json")

Configuration Defaults

Pass configuration values directly to run_ssds(). When omitted, values come from the defaults in config.py.

Result Objects

The pipeline returns a list of behavior-analysis results grouped by category. With verbose output enabled, the reports and discovery stages are printed while the pipeline runs.

Saving Expression Banks

Use output_path to write the converted bank as JSON. When it is omitted, make_bank() writes to bank.json and selects a numbered path when that file already exists.

bank = make_bank(
    equations,
    output_path="outputs/my_bank.json",
    canonicalize=True,
)

Saving Terminal Output

Use Python's contextlib.redirect_stdout to capture SSDS standard output:

from contextlib import redirect_stdout

from run_ssds import run_ssds

with open("ssds_output.txt", "w") as output_file:
    with redirect_stdout(output_file):
        results = run_ssds(
            bank,
            verbose=True,
        )

This captures writes to stdout. It does not capture stderr.

Basic Example

from Convert import make_bank
from run_ssds import run_ssds

bank = make_bank("""
F = m*a
E = m*v**2/2
""")
results = run_ssds(bank)

Hidden Distance

(Example coming soon)

Use this example area for an expression bank where a shared relationship is not obvious from surface notation.

Turbulence

(Example coming soon)

Use turbulence expression banks to study recurring structures across turbulence equations and derived quantities.

Feynman Benchmark

(Example coming soon)

Benchmark expression banks compare recurring-structure discovery across known scientific equations.

Methodology

SSDS separates symbolic and behavioral analysis before combining them during similarity evaluation. Generalization operates on related expressions identified by the preceding discovery stages. Candidate filtering removes structures that fail the configured criteria.

Metrics

See the run_ssds() reference for what each parameter controls.

Experiments

Use equation banks to evaluate recurring symbolic structures, algebraically different representations, structural transformations, behavioral similarity, candidate abstractions, generalized operators, novelty detection, and invariant behavior. Keep the expression bank and configuration fixed when comparing runs.

Experimental Results

SSDS produces candidate mathematical abstractions and behavior-analysis results. Validate candidates against the source equations before treating them as mathematical results.

Limitations

Some closure, invariant, and parameter-shift analyses can time out or remain incomplete. Behavioral results depend on sampling ranges and parameter values. AI interpretation is an alpha feature and remains unreliable. AI-generated interpretations can be incorrect, incomplete, or misleading. Some candidates may require additional symbolic or numerical validation before classification.