Semantics for STACK authors
Questions, evidence and compatibility conventions
This page is for STACK authors and maintainers reviewing mathematical conventions and compatibility questions. Start with the prioritized questions below, then follow the linked cases, sources and observations.
Teachers adapting exercises can start with the authoring guide and report interpretation. Developers reproducing comparisons can use the report’s development reference.
This page compares a bounded SymPy CAS adapter with pinned STACK behavior. Case numbers refer to the unchanged 2,303-row imported fixture. An expected acceptance in that fixture is evidence of the desired test outcome, not necessarily a universally valid complex identity. Mathematical truth, fixture expectation, native observation, and port behavior are kept separate below.
Implemented behavior and observed native agreement
Agreement here concerns the stated cases and bounded contracts. It does not claim general Maxima normalization, complete CAS parity or identical internal representations.
Bounded matrices
matrix([a,b],[c,d]) is a distinct rectangular matrix value with finite supported scalar entries. The port permits 1–16 rows, 1–16 columns and at most 64 entries, subject to the existing expression limits. Empty/zero-dimensional, ragged and incomplete matrices, nested non-scalar entries and matrix arithmetic are outside this contract. The editor’s historical empty-matrix convention is not inherited.
Following pinned STACK’s ATMatrix, AlgEquiv checks row count, then column count, then ordered entries through the existing scalar equivalence machinery. Wrong dimensions are an applicable incorrect result. SameType recognizes matrix type independently of dimensions; ordinary nested lists remain a different type. EqualComAss additionally checks the submitted cell forms, so matrix([x+x]) and matrix([2*x]) are algebraically equal but have different written forms. Literal decimal cells retain the scalar/list binary64 provenance rules; frozen decimal matrix aliases fail closed rather than acquiring invented provenance.
Native-compatible matrix grading currently supports AlgEquiv, SameType and EqualComAss. Other native matrix answer tests remain an explicit port limitation; native STACK supports more, including LowestTerms. Separately, the Python-only PythonChecker with supplied basis and matrix-property functions supports basis and exact constraint tasks. These are outside this native inventory and documented in Python answer tests. There is no automatic cell partial credit: PRT scores remain author-defined. The supplied basis checker may color individual vectors as diagnostic feedback, without assigning marks to them. Fixed dimensions are validated before grading; learner-selected dimensions must stay inside the configured bounds.
The unchanged 2,303-row inventory gains 13 supported official matrix rows (452 to 465 supported rows overall). All 13 match desired outcomes in direct CPython execution. Native comparison and browser Pyodide parity are enforced by the existing pinned CI harness. The matrix decision record documents the bounds, provenance restrictions and remaining excluded matrix cases. The set and trigonometric questions below remain unchanged.
The positive dimension bounds, optional learner resizing and Python-only answer tests are explicit port contracts, rather than unresolved questions requiring a new native STACK semantic decision. Authors should choose the correct test and scoring policy for their task; see the matrix authoring guidance.
Written form and numerical fractions
EqualComAss checks written structure in addition to value. STACK’s rule-based test documentation describes division as a numerator multiplied by an opaque reciprocal. The port now preserves that form:
| Cases | Submitted / reference | Expected result and reason |
|---|---|---|
| 645 | x/(-y) / -(x/y) |
Reject: an inner reciprocal minus differs from an outer minus. |
| 647 | 1/2*1/x / 1/(2*x) |
Reject: two written fractions differ from one reciprocal of a product. |
| 658 | (1/2)*(a+b) / (a+b)/2 |
Reject: a rational coefficient differs from held division of the sum. |
| 665 | 1/3*i / i/3 |
Reject: the written numerator/reciprocal distinction remains. |
| 2278 | x-1/-4 / ignored reference |
Reject: numerical-fraction extraction sees a negative numerator over a negative denominator after outer subtraction. |
Case 2278 does not ban negative denominators. Native LowestTerms accepts 1/-3 and x+1/-4 but rejects -1/-3 and x-1/-4, as follows from list_expression_numbers and lowesttermsp. It ignores the reference and does not cancel polynomial factors. Written form and algebraic normalization therefore remain distinct contracts.
Literal floating-point arithmetic
STACK’s numerical-rounding documentation describes binary floating-point artifacts and recommends numerical tests for approximate answers. For these literal-constant AlgEquiv rows, the port reproduces native outcomes through finite binary64 arithmetic and exact equality of the resulting machine values:
| Case | Submitted / reference | Native-compatible result |
|---|---|---|
| 32 | 452 / 4.52*10^2 |
Reject: the product is approximately 451.99999999999994. |
| 37 | 0.99999999 / 99999998/99999999 |
Accept: both yield the same binary64 value although the exact rationals differ. |
| 43 | 100.4-80.0 / 20.4 |
Reject: subtraction yields approximately 20.400000000000006. |
For case 37, the STACK author’s issue 1581 comment explains that Maxima’s rational conversion of a float need not recover its decimal fraction. It distinguishes exact notation, finite precision, and the difficulty of inferring student intent. The port’s binary64 path is bounded: it applies to AlgEquiv literal arithmetic with a decimal/scientific literal and integer exponents. Symbols, functions and frozen author values retain the symbolic path. The shared parser evaluates decimals as exact rationals, and numerical tolerance tests retain their own tolerances. The port neither implements Maxima’s general rat algorithm nor adds epsilon-based algebraic acceptance.
Reciprocal square roots
The installation notes explicitly require 1/sqrt(x) and sqrt(1/x) to be equivalent while noting the domain debate. The port implements this convention for a top-level reciprocal square root of a single symbol (case 80). It does not propagate the rule into logarithms, larger products or ratios. At a negative real value the two principal-complex values have opposite signs, so this is a documented symbolic compatibility convention, not a general principal-complex identity. Explicit numeric 1/sqrt(-1) and sqrt(1/(-1)) remain distinct; ratios, arbitrary root products and nested powers are not forced together.
Principal logarithmic powers
For nonzero x,y, principal powers satisfy
\[x^{\log y}=\exp(\log y\,\log x)=\exp(\log x\,\log y)=y^{\log x}.\]
The port certifies this case 166 identity by bounded exponential rewriting, also used in STACK’s equivalence routine. This is our mathematical proof, not a located published STACK rationale for the particular row. It does not expand logarithms of products or require positive real assumptions. The earlier characterization as a different branch convention was too conservative.
Finite lists and sets
The supported collection tests have different contracts:
| Test and objects | Pinned native comparison route | Current port contract |
|---|---|---|
AlgEquiv, lists |
Recursively compare corresponding entries with AlgEquiv. |
Ordered recursive comparison for supported leaves. |
AlgEquiv, sets |
Prepare members with ATSets_prepare, then compare membership. |
Structural SymPy members, with bounded exact rectangular conversion for certain numeric complex constants. |
Sets |
Related set preparation, with duplicate, extra and missing feedback. | Structural member comparison and outer duplicate feedback; no general scalar-proof matching. |
EqualComAss |
Written form after applicability/equivalence checks. | Held arithmetic, list order and set multiplicity in the supported subset. |
SameType |
ATAlgEquivfun applicability at this pin. |
The observed applicability behavior, not a strict nested-shape validator. |
The pinned set routines do not run full scalar algebraic_equivalence on every candidate member pair. STACK’s equivalence documentation describes canonical representations and the expression-growth cost of stronger generic transformations. That explains a general engineering tradeoff; it does not prove that any particular rejected equality was intentional. The port uses SymPy normalization, not every intermediate Maxima form. For example, {(x+1)^2} and {x^2+2*x+1} remain structurally distinct in its set tests. Its bounded rational multiple of exp(i*pi*q) may acquire an exact rectangular form under AlgEquiv but not Sets. Matching results alone do not establish matching preparation pipelines.
Lists retain order and repetition; nested sets retain their boundaries, so {1,{2}} is not flattened to {1,2}. Lists inside sets retain list order. The port preserves submitted repetitions until the relevant test runs. Sets duplicate feedback applies to the submitted outer set:
| Submitted / reference | Sets result |
|---|---|
{1,1} / {1} |
Correct, no outer duplicate warning. |
{{1},{1}} / {1} |
Correct, with an outer duplicate warning. |
This does not promise a warning for every repeated nested leaf. The explicit EqualComAss written-form contract distinguishes {4,4} from {4}. A set’s entries commute, but written multiplicity and nested boundaries are retained: {1,2,2} / {2,1,2} passes, whereas {4,4} / {4} fails the form check despite equal mathematical set values. Nested list order matters. The port checks applicability and algebraic equality before held form. Frozen teacher/qtest aliases that would lose the required form fail at build time; that is a port preservation boundary, not a native prohibition.
At this pin, ATSameTypefun uses ATAlgEquivfun applicability. The port reproduces these observations:
| Pair | SameType and reason |
|---|---|
[1,[2]] / [9] |
True: unequal list length remains applicable. |
[1,[2]] / [9,{3}] |
False: equal-length lists recurse to differing member types. |
{[1,2]} / {1,2} |
True: set/set comparison is applicable regardless of member types. |
These are predicate results, not an intentional mathematical definition of nested type or strict shape equality. Collection input checkanswertype follows the same rule; content still requires an equality test.
Questions requiring clarification
Eight domain and branch rows
All eight have desired/native acceptance and port rejection. The counterexamples test principal-complex semantics; they do not establish that STACK intends that domain. Please clarify the assumptions rather than infer a general rewrite from the observed results.
| Case | Accepted identity | Principal-complex qualification or counterexample | Question for STACK |
|---|---|---|---|
| 50 | i/sqrt(x) = sqrt(-1/x) |
True for nonzero real x; at x=-i, the sides are exp(3*pi*i/4) and exp(-pi*i/4). |
Is x implicitly real, or is another root convention intended? |
| 76 | abs(x) = sqrt(x^2) |
True for real x; at x=i, the sides are 1 and i. The port accepts under an explicit real assumption. |
Should the fixture declare real x? |
| 92 | 6*((x-2)^3)^k = 6*(x-2)^(3*k) |
At x=1,k=1/2, the sides are 6*i and -6*i. |
Is k integral, x>2, or odd-inner-power denesting intended? |
| 99 | sqrt(k/m)*sqrt(m/k) = 1 |
True for positive real k/m; at k=-1,m=1, the product is -1. |
Should positivity be explicit, or is a broader root combination intended? |
| 101 | 2*pi*sqrt(m/k) = 2*pi/sqrt(k/m) |
At m=-1,k=1, the sides are 2*pi*i and -2*pi*i. |
Is positivity intended, or does the documented single-symbol convention extend to ratios? |
| 127 | (-1)^n*cos(x)^n = (-cos(x))^n |
True for integer n; at n=1/2,x=pi, the sides are -1 and 1. |
Is n implicitly integral? |
| 155 | log(a^2*b) = 2*log(a)+log(b) |
True for positive real a,b; at a=-1,b=1, the sides are 0 and 2*pi*i. |
Should positivity be required, or are branch constants ignored? |
| 167 | log((x+1)/(1-x)) = -log((1-x)/(x+1)) |
True for real -1<x<1; at x=2, the sides differ by 2*pi*i. |
Is the interval intended, or equality modulo 2*pi*i? |
Defining variables documents assume/declare contexts and the real-style square-root identity relevant to 50 and 76, without specifying those bare rows completely. Simplification warns against general nested-power denesting without assumptions, making 92 a question rather than a license for blanket rewriting. The equivalence implementation sets logexpand:super and applies transformations including radcan; that is a mechanism for logarithmic differences, not a per-case justification for ignoring branches. For 50, 99, 101, 127, 155 and 167 we found fixture expectations and implementation, but no case-specific published rationale. The counterexamples and questions are the port team’s analysis.
log(x^2) against 2*log(abs(x))
This belongs with case 155 (log(a^2*b) = 2*log(a)+log(b), expected 1; fixture line 291), with a twist: the rewrite drops the abs. The identity \(\ln(x^2)=2\ln|x|\) holds for every real \(x\neq 0\), but it is rejected, while 2*log(x), equal only for \(x>0\), is accepted.
log-square.xml: teacher’s answer ta: log(x^2), one AlgEquiv node, questionsimplify=1.
| Answer | Mathematically | STACK 4.13.1 and 4.14.0 |
|---|---|---|
2*log(abs(x)) |
Equal for every real \(x\neq 0\) | Score 0 |
log(abs(x)^2) |
Equal for every real \(x\neq 0\) | Score 1 |
2*log(x) |
Equal only for \(x>0\) | Score 1 |
log(x) |
Not equal | Score 0 |
With the teacher’s answer 2*log(abs(x)) instead, the answers log(x^2), log(abs(x)^2) and 2*log(x) all get 0.
The cause, as far as we can see, is Maxima’s default logexpand:true, which STACK keeps. Ordinary simplification rewrites log(x^2) to 2*log(x) before any answer test runs. With questionsimplify=1 the teacher’s answer is therefore stored as 2*log(x), and that is also the model answer the students see. It is not the block-local logexpand:super in algebraic_equivalence (assessment.mac line 1096). A change would need a STACK-side rewrite of log(a^(2k)) before simplification, or a change in Maxima. We have not tried either.
Question. Is rejecting 2*log(abs(x)) against a teacher’s log(x^2) intended, as part of the design behind case 155? If not, which route would you prefer: a STACK-side rewrite before simplification, or a change in Maxima?
Nested radical: clarify the domain
Case 471 uses expected acceptance (-3) but native and port both reject. For real x, put u=sqrt(x^2+1)+x. Then u>0 and u^2=2*x*sqrt(x^2+1)+2*x^2+1, so the outer square root is u and the submitted expression is zero. At complex x=-i the expression is instead 2*i. Please confirm whether the fixture assumes real x. A bounded, sign-aware proof of that real identity is separate port work; native rejection does not settle the mathematical claim. STACK’s equivalence documentation acknowledges incomplete radical proofs and the expression-growth/timeout risk of aggressive generic transformations.
Exact constant sets and row 212
| Case | Mathematically equal? | Fixture expects | Pinned native | Port |
|---|---|---|---|---|
| 201 | Yes | Accept (-3) |
Reject | Accept |
| 202 | Yes | Accept (-3) |
Reject | Accept |
| 203 | Yes | Accept (-3) |
Reject | Accept |
| 212 | Yes | Reject (0) |
Reject | Accept |
- 201:
{-sqrt(2)/sqrt(3)}/{-2/sqrt(6)}. SymPy canonicalizes the members together. STACK’s equivalence documentation describes this pair as a canonicalization limit and recommends author-sideradcanwhere appropriate; blanket use can cause expression growth. - 202:
{[-sqrt(2)/sqrt(3),0],[2/sqrt(6),0]}/{[2/sqrt(6),0],[-2/sqrt(6),0]}. This is the same radical equality within list members of a set, not a disputed complex branch identity. - 203:
{5/4*exp(i*pi/6)}/{5*sqrt(3)/8+5*i/8}. Bounded exact rectangular conversion proves the equality in the port. - 212:
{(2-2^(5/2))/2,(2^(5/2)+2)/2}/{1-2^(3/2),2^(3/2)+1}. For these positive constants,2^(5/2)/2=2^(3/2)and SymPy canonicalizes corresponding members.
Cases 201–203 are reported as known_native_mathematical_failure on the separate compatibility axis: native misses the desired exact equality. Row 212 is a counted known port normalization difference, because its fixture and native test require rejection. Is that expected rejection meant to assert prepared-form behavior? What normalization should authors expect for mathematically equal set members? The port’s choice between SymPy-canonical members and faithful Maxima prepared-form distinctions is a project compatibility policy. Emulating row 212 would require held member provenance before SymPy canonicalization; no expression-specific grading exception exists.
The teacher’s 1.015 at a tie
A question rather than a bug report; the impact is low. It matters only when the teacher’s answer is a decimal ending in 5 exactly at the rounding position.
decimal-tie-binary.xml: a: 1.015, NumDecPlaces(ans1, a, 2) and NumSigFigs(ans2, a, 3).
Answers (ans1, ans2) |
Expected | STACK 4.13.1 and 4.14.0 (NumDecPlaces, NumSigFigs) |
|---|---|---|
1.02, 1.02 |
1, 1 | 0, 0 |
1.01, 1.01 |
0, 0 | 1, 1 |
1.015 is stored as the binary float 1.01499999999999990230…, and both tests round that value. STACK’s own decimalplaces(1.015,2) and significantfigures(1.015,3) also give 1.01, so in this question the inputs’ teacher’s answers, decimalplaces(a,2) and significantfigures(a,3), show 1.01 too. For 1.02, NumSigFigs says “Either you have not rounded correctly, or you have rounded an intermediate answer which propagates an error.” With the teacher’s answer written as 203/200, NumDecPlaces accepts 1.02.
The rounding notes explain that floats are binary in general, but not this tie case. STACK’s fixture line 2636, ['NumSigFigs', '3', '0.0499', '0.04985', 1, '', 'Round teacher answer'], suggests that the teacher’s decimal is meant to be rounded as written. 0.04985 is also below the tie in binary (0.04984999999999999848…). It rounds up only because 0.04985/10^-4 comes out as exactly 498.5 in sigfigsfun (a bigfloat, then float); for 1.015 the same arithmetic gives 101.49999999999999.
Where in the source: ATNumDecPlaces compares round(sans*10^n) with round(tans*10^n) in floating point (numericaltest.mac lines 388–389). decimalplacesfun rounds 10^n*float(ex) (assessment.mac line 858). sigfigsfun tests ex1-floor(ex1) = 0.5 on a float (assessment.mac line 909), and ATNumSigFigs uses it (numericaltest.mac line 280). A possible approach, untested: round the decimal that the teacher wrote, for example through the exact rational of the float’s shortest decimal form.
Question. Should a teacher’s 1.015 give 1.02 in these tests, as it would under either tie rule? If so, we would be glad to help with a patch.
abs of a product, in AlgEquiv and Int
This is not a new report. STACK’s answer-test fixtures, which “document the expected behaviour of answer tests” (lines 20–22), already record it:
| Fixture line (case here) | Fixture | Expected |
|---|---|---|
| 720 (468) | AlgEquiv, abs(x^2-4) / abs(x-2)*abs(x+2) |
−3: “Expected maths failure, where test should return 1” (line 61). Line 719 (467) is the same case as a quotient. |
| 1992 (1388) | Int, log(abs((x^2-9)))+c / log(abs(x-3))+log(abs(x+3)) |
0, ATInt_EqFormalDiff |
| 1991 (1387) | Int, log(abs((x-3)*(x+3)))+c / log(abs(x-3))+log(abs(x+3)) |
1, ATInt_true_equiv: the same teacher’s answer, with the product unexpanded inside abs |
The rows are published in Results/AlgEquiv.md (class expectedfail) and Results/Int.md. The Int documentation says “The test cannot cope with some situations. Please contact the developers when you find some of these.” (Calculus.md line 40). In this report, case 468 is a desired acceptance that native STACK rejects, like cases 201–203; the port accepts it.
We met it in an ordinary exercise. abs-product.xml has the teacher’s answer abs(x^2-1) and one AlgEquiv node. int-partial-fractions.xml has Int with option x and the teacher’s answer integrate(2*x/(x^2-1),x)+c, which Maxima returns as log(abs(x^2-1))+c.
| Question | Answer | Mathematically | STACK 4.13.1 and 4.14.0 |
|---|---|---|---|
| abs-product | abs(x^2-1) |
Correct | Score 1 |
| abs-product | abs((x-1)*(x+1)) |
Correct | Score 0 |
| abs-product | abs(x-1)*abs(x+1) |
Correct | Score 0 |
| abs-product | sqrt((x^2-1)^2) |
Correct | Score 1 |
| abs-product | x^2-1 |
Wrong | Score 0 |
| int-partial-fractions | log(abs(x^2-1))+c |
Correct | Score 1, ATInt_true_equiv |
| int-partial-fractions | log(abs(x-1))+log(abs(x+1))+c |
Correct | Score 0, ATInt_EqFormalDiff |
| int-partial-fractions | log(abs(x^2+1))+c |
Wrong | Score 0 |
ATInt_EqFormalDiff asks the student to see the teacher. With the option [x,FORMAL], Int gives the partial-fraction answer mark 1 but shows the same feedback.
A possible approach, untested: in algebraic_equivalence (assessment.mac lines 1084–1182), before comparing, rewrite abs(a)*abs(b) to abs(a*b) and abs(a)^n to abs(a^n) on both sides, and combine log(abs(a))+log(abs(b)) in the same way.
Question. Is a fix wanted? We could offer one.
Tie rules of NumDecPlaces and NumSigFigs
A documentation question, not a bug. NumDecPlaces (and decimalplaces) round half to even, through Maxima’s round; NumSigFigs (and significantfigures) round half up.
decimal-tie-rule.xml: a: 0.125, NumDecPlaces(ans1, a, 2) and NumSigFigs(ans2, a, 2).
Answers (ans1, ans2) |
STACK 4.13.1 and 4.14.0 (NumDecPlaces, NumSigFigs) |
|---|---|
0.13, 0.13 |
0, 1 |
0.12, 0.12 |
1, 0 |
This is documented: Numerical_rounding.md lines 45–47 (Maxima’s round rounds half to even; significantfigures rounds up) and Numbers.md lines 169–173 (decimalplaces rounds; with n = 0 it uses round). The NumSigFigs paragraph (Numerical.md line 72) links to “notes about numerical rounding” (Numbers.md). The NumDecPlaces paragraph (line 76) says only that the test “rounds the numbers to the specified number of decimal places”.
Question. Would a sentence there naming the tie rule, or a link to the rounding notes, like the one in the NumSigFigs paragraph, be welcome? We could send a pull request.
Possible future development
Trigonometric identity inside a set
Mathematically sin(x)^2+cos(x)^2=1; the two singleton sets denote the same set with no domain/branch counterexample. Direct pinned probes show a normalization/proof limit:
| Comparison | Native STACK | Port |
|---|---|---|
Scalar AlgEquiv: sin(x)^2+cos(x)^2 / 1 |
Accept | Accept |
List AlgEquiv: [sin(x)^2+cos(x)^2] / [1] |
Accept | Accept |
Set AlgEquiv: {sin(x)^2+cos(x)^2} / {1} |
Reject | Reject |
Sets: {sin(x)^2+cos(x)^2} / {1} |
Reject | Reject |
Both set rejections have CAS applicability true, not a syntax error. Project controls port-list-equivalence, port-set-trig-structural and port-sets-trig separately record list/set outcomes and Python/Pyodide parity. For set AlgEquiv, the pinned call chain is ATAlgEquiv → ATAlgEquivfun → ATSet → ATSets_prepare → mutual subsetp membership checks. Sets uses the same preparation with its duplicate, extra and missing feedback path. The native preparation is:
ATSets_prepare(S) :=
ev(map(lambda([ex], ineqprepare(trigreduce(ex))), S), simp)$For p:sin(x)^2+cos(x)^2, the direct native trace is:
| Stage | Native result |
|---|---|
trigreduce(p) |
(cos(2*x)+1)/2 + (1-cos(2*x))/2 |
ineqprepare(trigreduce(p)) |
The same two-fraction sum. |
Ordinary ev(...,simp) in set preparation |
The same two-fraction sum. |
ATSets_prepare({p}) |
{(cos(2*x)+1)/2 + (1-cos(2*x))/2} |
ATSets_prepare({1}) |
{1} |
trigreduce has rewritten the squares using double angles, but ordinary simplification here has not combined the fractions. The membership test does not invoke stronger scalar equivalence to bridge the remaining gap. Native probes identify the missing rational-arithmetic step:
ratsimp(trigreduce(p)); /* 1 */
expand(trigreduce(p)); /* 1 */
trigsimp(p); /* 1 */
ratsimp(ATSets_prepare({p})); /* {1} */Scalar AlgEquiv accepts the prepared member against 1. Applying ratsimp to the prepared set before either set answer test also changes rejection to acceptance. These are native CAS probes, not claims that all these author functions are exposed by the port. The port instead prepares the submitted set as {sin(x)^2+cos(x)^2} and the reference as {1}. It neither makes the native double-angle rewrite nor invokes its scalar identity prover for set members. Its scalar and list paths do accept the identity.
This shared rejection arises through different intermediate forms. It is not an explicit convention that trig identities cease to hold in sets. STACK’s Sets documentation advises author-side preprocessing when stronger trig simplification is needed. The broader equivalence documentation explains the general cost of aggressive transforms; neither establishes that this specific simple rejection was deliberately selected. Author advice would be useful on whether preprocessing is the desired authoring path or bounded preparation should prove this pair. Either stronger port approach below would be an explicit compatibility change, not a description of current native matching.
Proposed stronger set-member matching
This is a design proposal, not current native or port behavior. For bounded flat sets of already supported scalar expressions, the matching algorithm is straightforward and could be a small-to-medium feature. Grading, feedback, nested collections and predictable computation limits require a more explicit contract. Once algebraic duplicates are handled, the comparison could:
- Recognize structurally identical members first and cache those results.
- Send remaining candidate pairs through scalar
AlgEquivunder the same domain assumptions. Record certified equality, proved inequality where available, and inconclusive results separately; failure to prove equality does not establish inequality. - Construct a bipartite graph from equality certificates and find a complete matching with a polynomial-time algorithm. Reassign tentative pairings when needed rather than greedily committing to the first candidate; factorial permutation search is unnecessary.
Two flat 32-member sets have at most 1,024 cross-set candidate pairs. That does not bound total work: quotienting algebraic duplicates adds within-set comparisons, nested sets recurse, and a single symbolic proof may be costly. The proof work is the main runtime concern, not graph matching. Cache proofs and limit proof time/work, expression growth and total recursive comparisons with a shared budget; the pair count alone guarantees no response time.
| Issue | Contract to decide before implementation |
|---|---|
| Algebraic duplicates | {sin(x)^2+cos(x)^2,1} is the same mathematical set as {1}. A raw-entry bijection would wrongly reject it. Form duplicate classes from certified equality before cardinality/matching, while retaining submitted entries for Sets duplicate feedback. Incomplete proofs can leave classes unresolved. |
| Nested collections | Compare list entries in order and sets unordered recursively, retaining list/set types and nesting boundaries. Apply existing depth/size bounds and one shared recursive budget. |
| Inconclusive results | Distinguish budget exhaustion or an unproved candidate from proved inequality. Accept only with necessary certificates; do not describe uncertainty as a mathematical counterexample or definitive extra/missing member. |
| Written form | Keep EqualComAss held arithmetic and repeated entries. Stronger algebraic equality may satisfy its prerequisite but must not replace form comparison or erase provenance. |
For Sets, deterministic matching and retained submission provenance would support useful feedback where several pairings work. Extra/missing diagnostics should describe the checker outcome without claiming that incomplete proof established inequality. A first scope could be flat exact scalar sets, then recursive collections and richer feedback under the stated contract. Adding ratsimp to set preparation would fix the traced trig example more narrowly; it would not implement generic scalar-proof matching or settle duplicates, recursion and resource limits.
Exact-real constant certification
Case 474 compares cos(2*pi/17) with its constructible radical representation. The fixture desires acceptance (-3), while native and port reject. It is an exact-real identity, not a branch-convention dispute. The port would need a certified algebraic-number proof with correct root selection; numerical agreement is insufficient. STACK advice on a useful proof method or priority would help future work. The equivalence documentation also notes that more aggressive generic transforms can grow expressions and time out.
Deliberate port scope and engineering choices
One Python checker interface
PythonChecker is the single XML extension for Python assessment. The selected file provides named functions with the same three-argument contract (learner value, teacher value, JSON options object) and optional feedback and vector assessments. The supplied basis and matrix_property functions select their mathematical case through that text; other functions may do the same without adding answer-test types or grading branches to the adapter.
Input validation uses the common parser and XML settings; mathematical domain checks run in the selected function during grading. The build’s checker preflight verifies valid model grading, and authored qtests verify the intended scoring. PRT scores and answer notes remain in XML.
Decimal precision in collections
These native observations differ from the port’s rule to reject collection inputs whose precision provenance it cannot preserve:
| Context | Pinned native observation | Port contract |
|---|---|---|
Set {1/10} / {0.1}, or {1/2} / {0.5} |
Both set tests reject. | Decimal literals anywhere beneath a set are unsupported; use exact fractions. |
Direct list [1/10] / [0.1] |
Scalar/list AlgEquiv accepts. |
Direct decimal list leaves use bounded scalar binary64 behavior. |
[100.4-80.0] / [20.4] |
AlgEquiv rejects. |
Preserve literal arithmetic; freezing the first to [102/5] changes the result. |
[sin(0.1)] / [sin(1/10)]; [sqrt(0.5)] / [sqrt(1/2)] |
AlgEquiv rejects. |
Decimal-bearing function/symbolic list leaves are unsupported; use exact fractions. |
[20.4] / ev([100.4-80.0],simp) |
AlgEquiv rejects. |
Decimal-bearing transformed collections are unsupported. |
The set rejections are consistent with distinct prepared native float/rational members; these probes do not prove the precise intermediate representation. They do not make {1/2} and {0.5} different mathematical sets: 0.5 is exactly binary-representable. Because the port parses decimals as exact rationals, it cannot use structural set keys for every native float case and claim parity. The deliberate representation boundaries are:
- Decimal list leaves permit literal constant arithmetic, unary signs and bounded integer powers; decimal-bearing functions, symbolic leaves and author transformations need exact-fraction alternatives.
- Frozen collection/helper definitions reject transitive decimal aliases in qtests and CASText too. Learner collection answers referencing decimal-derived author aliases are rejected.
- Dynamic collection
sansmust directly name the learner input. Collection feedback aliases and learner-dependent helper/collection construction are unsupported because serialization can lose literal arithmetic.
These limits do not assert that native STACK forbids those author expressions.
Feedback and tested layer
Ordinary port feedback is abbreviated English. Negative lists do not reproduce every native nested index/reason note; EqualComAss also omits some internal notes. Full localized feedback and exact ordinary notes are not claimed compatible. Authored PRT branch notes and their ordered qtest expectations are preserved. These bounded CAS checks and Python/Pyodide agreement do not establish Moodle/PHP-controller, input-security or attempt-state parity.
Evidence and report interpretation
The comparison pins STACK revision 10d323311b7f39fb7b092dd10ce75f7d8632f525. The collection trace uses Maxima 5.46.0, port revision 1e72245874bfea49836e58ba48dcee30af5ee0ba, and SymPy 1.13.3.
| Evidence category | Interpretation |
|---|---|
| Mathematical claim | Equality under stated domain and branch assumptions. |
| Fixture expectation | The imported desired outcome, not a universal proof. |
| Native observation | Pinned STACK/Maxima result, not automatically mathematical truth. |
| Port behavior | What the bounded adapter accepts or explicitly declines. |
An explicit source convention, such as case 80, differs from observed implementation behavior such as SameType, and both differ from a proof limit such as the trig set. A port boundary describes its representation limits, not native prohibitions. The eight domain/branch cases and two exact-proof cases 471 and 474 remain case-pinned in checker policy. Collection normalization observations are additional. The report distinguishes Domain/branch difference from Unproved identity with explanations, evidence, links and author questions. A changed label does not confer a compatibility pass; any changed result invalidates its previous exception and needs review.
Minimal STACK questions
The four questions log(x^2), the teacher’s tie, abs of a product and tie rules were checked with STACK’s own code, not the port, through minimal question files:
Each file is Moodle XML for the STACK question type, with question tests that state the expected marks; they fail where this page reports a difference. In decimal-tie-rule.xml the tests expect 0.13 from both, only to show where the two rules differ; under the documented rules NumDecPlaces expects 0.12. The files are CC BY-SA 4.0 (Andrey Chesnokov, 2026).
Versions, with identical results in both:
- STACK 4.13.1 (2026080600):
masterat 10d3233, 12 commits after tag v4.13.1 (d785ac3) that change only documentation,CHANGES.mdand a licence file; the code is identical to the tag. Source links on this page are to this commit. - STACK 4.14.0 (2026101500):
masterat 3338304, one CI-only commit after tag v4.14.0 (3946874). - Maxima 5.46.0.
We ran STACK’s PHP code through the STACK API’s question loader and grading path (input validation, then grade_response() and the response trees), and the question tests through the API’s test controller. We did not run them in a Moodle site.