@@ -295,24 +295,92 @@ function _expand_power(args, ::Type{T}) where {T}
295295 return result
296296end
297297
298+ """
299+ _sort3(a, b, c) -> (a, b, c) sorted ascending
300+
301+ Sort three integers without heap allocation (in-place bubble sort).
302+ """
303+ function _sort3 (a:: Real , b:: Real , c:: Real )
304+ if a > b
305+ a, b = b, a
306+ end
307+ if b > c
308+ b, c = c, b
309+ end
310+ if a > b
311+ a, b = b, a
312+ end
313+ return a, b, c
314+ end
315+
316+ """
317+ _monomial_key(m::_Monomial)::NTuple{4,Int64}
318+
319+ Compute a canonical hash key for a monomial: (degree, sorted_val1, sorted_val2, sorted_val3).
320+ Uses integer tuple instead of a sorted Vector for faster hashing.
321+ """
322+ function _monomial_key (m:: _Monomial )
323+ n = length (m. variables)
324+ if n == 0
325+ return (Int64 (0 ), Int64 (0 ), Int64 (0 ), Int64 (0 ))
326+ elseif n == 1
327+ a = m. variables[1 ]. value
328+ return (Int64 (1 ), Int64 (a), Int64 (0 ), Int64 (0 ))
329+ elseif n == 2
330+ a, b = m. variables[1 ]. value, m. variables[2 ]. value
331+ lo, hi = a <= b ? (a, b) : (b, a)
332+ return (Int64 (2 ), Int64 (lo), Int64 (hi), Int64 (0 ))
333+ else # n >= 3; degree > 3 is rejected at classification stage
334+ a, b, c = _sort3 (
335+ m. variables[1 ]. value,
336+ m. variables[2 ]. value,
337+ m. variables[3 ]. value,
338+ )
339+ return (Int64 (n), Int64 (a), Int64 (b), Int64 (c))
340+ end
341+ end
342+
343+ """
344+ _monomial_vars(key::NTuple{4,Int64})::Vector{MOI.VariableIndex}
345+
346+ Given a monomial key, reconstruct the list of variables.
347+ """
348+ function _monomial_vars (key:: NTuple{4,Int64} )
349+ degree = key[1 ]
350+ if degree == 0
351+ return MOI. VariableIndex[]
352+ elseif degree == 1
353+ return [MOI. VariableIndex (key[2 ])]
354+ elseif degree == 2
355+ return [MOI. VariableIndex (key[2 ]), MOI. VariableIndex (key[3 ])]
356+ else # degree == 3
357+ return [
358+ MOI. VariableIndex (key[2 ]),
359+ MOI. VariableIndex (key[3 ]),
360+ MOI. VariableIndex (key[4 ]),
361+ ]
362+ end
363+ end
364+
298365"""
299366 _combine_like_monomials(monomials::Vector{_Monomial{T}}) where {T}
300367
301368Combine like monomials (same variables, regardless of order).
369+ Assumes all monomials have degree ≤ 3.
302370"""
303371function _combine_like_monomials (monomials:: Vector{_Monomial{T}} ) where {T}
304- # Use a dict keyed by sorted variable tuple
305- combined = Dict {Vector{MOI.VariableIndex },T} ()
372+ # Key: NTuple{4,Int64} (degree + up to 3 sorted variable indices).
373+ combined = Dict {NTuple{4,Int64 },T} ()
306374
307375 for m in monomials
308- # Sort variables for canonical key
309- key = sort (m. variables, by = v -> v. value)
376+ key = _monomial_key (m)
310377 combined[key] = get (combined, key, zero (T)) + m. coefficient
311378 end
312379
313380 result = _Monomial{T}[]
314- for (vars , coef) in combined
381+ for (key , coef) in combined
315382 if ! iszero (coef)
383+ vars = _monomial_vars (key)
316384 push! (result, _Monomial {T} (coef, vars))
317385 end
318386 end
@@ -341,7 +409,7 @@ function _classify_monomial(m::_Monomial)
341409 else
342410 return :pp
343411 end
344- elseif degree == 3
412+ else # degree == 3 (degree > 3 rejected early in _parse_cubic_expression)
345413 if num_params == 0
346414 return :vvv # Invalid - no parameter
347415 elseif num_params == 1
@@ -351,8 +419,6 @@ function _classify_monomial(m::_Monomial)
351419 else
352420 return :ppp
353421 end
354- else
355- return :invalid # Degree > 3
356422 end
357423end
358424
@@ -377,12 +443,17 @@ function _parse_cubic_expression(
377443 return nothing
378444 end
379445
446+ # Reject any monomial with degree > 3 before combining
447+ for m in monomials
448+ if _monomial_degree (m) > 3
449+ return nothing
450+ end
451+ end
452+
380453 # Combine like terms
381454 monomials = _combine_like_monomials (monomials)
382455
383456 # Classify and collect terms
384- cubic_terms = _ScalarCubicTerm{T}[]
385-
386457 cubic_ppp = _ScalarCubicTerm{T}[]
387458 cubic_ppv = _ScalarCubicTerm{T}[]
388459 cubic_pvv = _ScalarCubicTerm{T}[]
@@ -399,8 +470,8 @@ function _parse_cubic_expression(
399470 for m in monomials
400471 classification = _classify_monomial (m)
401472
402- if classification == :invalid || classification == : vvv
403- return nothing # Invalid degree or no parameter in cubic
473+ if classification == :vvv
474+ return nothing # No parameter in cubic term
404475 elseif classification == :constant
405476 constant += m. coefficient
406477 elseif classification == :v
0 commit comments