Skip to content

Commit 3089e7c

Browse files
authored
Remove some unnecessary parens when printing nonlinear expressions (#4201)
1 parent 38f45a7 commit 3089e7c

3 files changed

Lines changed: 65 additions & 42 deletions

File tree

docs/src/manual/nonlinear.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ exp(x[1]) - 1 ≤ 0
4141
4242
julia> @constraint(model, con[i = 1:2], 2^x[i] >= i)
4343
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarNonlinearFunction, MathOptInterface.GreaterThan{Float64}}, ScalarShape}}:
44-
con[1] : (2 ^ x[1]) - 1 ≥ 0
45-
con[2] : (2 ^ x[2]) - 2 ≥ 0
44+
con[1] : 2 ^ x[1] - 1 ≥ 0
45+
con[2] : 2 ^ x[2] - 2 ≥ 0
4646
```
4747

4848
Delete a nonlinear constraint using [`delete`](@ref):
@@ -128,7 +128,7 @@ A [`NonlinearExpr`](@ref) can be used in [`@objective`](@ref),
128128

129129
```jldoctest nl_expression
130130
julia> @objective(model, Min, expr^2 + 1)
131-
((exp(x[1]) + sqrt(x[2])) ^ 2) + 1
131+
(exp(x[1]) + sqrt(x[2])) ^ 2 + 1
132132
133133
julia> @constraint(model, [i = 1:2], my_expr[i] <= i)
134134
2-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarNonlinearFunction, MathOptInterface.LessThan{Float64}}, ScalarShape}}:
@@ -172,7 +172,7 @@ julia> @expression(model, expr, sum(exp.(x)))
172172
exp(x[1]) + exp(x[2])
173173
174174
julia> @objective(model, Min, sum(exp(x[i]) / expr for i in 1:2))
175-
(exp(x[1]) / (exp(x[1]) + exp(x[2]))) + (exp(x[2]) / (exp(x[1]) + exp(x[2])))
175+
exp(x[1]) / (exp(x[1]) + exp(x[2])) + exp(x[2]) / (exp(x[1]) + exp(x[2]))
176176
```
177177
In this model, JuMP will compute the value (and derivatives) of the denominator
178178
twice, without realizing that it is the same expression.
@@ -192,7 +192,7 @@ julia> @constraint(model, expr == sum(exp.(x)))
192192
expr - (exp(x[1]) + exp(x[2])) = 0
193193
194194
julia> @objective(model, Min, sum(exp(x[i]) / expr for i in 1:2))
195-
(exp(x[1]) / expr) + (exp(x[2]) / expr)
195+
exp(x[1]) / expr + exp(x[2]) / expr
196196
```
197197

198198
The reason JuMP does not perform common subexpression elimination automatically
@@ -254,7 +254,7 @@ julia> aff = x + 1;
254254
julia> quad = x^2 + x;
255255
256256
julia> expr = cos(x) * sin(quad) + aff
257-
(cos(x) * sin(x² + x)) + (x + 1)
257+
cos(x) * sin(x² + x) + (x + 1)
258258
```
259259

260260
### Supported operators

src/nlp_expr.jl

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,16 @@ variable_ref_type(::Type{GenericNonlinearExpr{V}}) where {V} = V
167167
const _PREFIX_OPERATORS =
168168
(:+, :-, :*, :/, :^, :||, :&&, :>, :<, :(<=), :(>=), :(==))
169169

170-
_needs_parentheses(::Union{Number,AbstractVariableRef}) = false
171-
_needs_parentheses(::Any) = true
172-
function _needs_parentheses(x::GenericNonlinearExpr)
170+
_needs_parentheses(::Any, ::Union{Number,AbstractVariableRef}) = false
171+
172+
_needs_parentheses(::Any, ::Any) = true
173+
174+
function _needs_parentheses(head, x::GenericNonlinearExpr)
175+
if head in (:+, :-) && x.head in (:*, :/, :^)
176+
# A simplification. By all variations of BEDMAS rules, ^, /, and *
177+
# apply with higher precedence than + and -.
178+
return false
179+
end
173180
return x.head in _PREFIX_OPERATORS && length(x.args) > 1
174181
end
175182

@@ -231,7 +238,7 @@ function function_string(mime::MIME, x::GenericNonlinearExpr)
231238
push!(stack, " $op $p_open")
232239
end
233240
continue
234-
elseif _needs_parentheses(arg.args[i])
241+
elseif _needs_parentheses(arg.head, arg.args[i])
235242
push!(stack, p_right)
236243
push!(stack, arg.args[i])
237244
push!(stack, p_left)

test/test_nlp_expr.jl

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ function test_extension_expression(
8181
@test string(@expression(model, sin(x)^2.0)) == "sin(x) ^ 2"
8282
@test string(@expression(model, 2 * sin(x)^2.0)) == "2 * (sin(x) ^ 2)"
8383
@test string(@expression(model, 1 + sin(x))) == "1 + sin(x)"
84-
@test string(@expression(model, 1 + 2 * sin(x))) == "1 + (2 * sin(x))"
84+
@test string(@expression(model, 1 + 2 * sin(x))) == "1 + 2 * sin(x)"
8585
@test string(@expression(model, 2.0 * sin(x)^2 + cos(x) / x)) ==
86-
"(2 * (sin(x) ^ 2)) + (cos(x) / x)"
86+
"2 * (sin(x) ^ 2) + cos(x) / x"
8787
@test string(@expression(model, 2.0 * sin(x)^2 - cos(x) / x)) ==
88-
"(2 * (sin(x) ^ 2)) - (cos(x) / x)"
88+
"2 * (sin(x) ^ 2) - cos(x) / x"
8989
return
9090
end
9191

@@ -162,7 +162,7 @@ function test_extension_latex(ModelType = Model, VariableRefType = VariableRef)
162162

163163
@expression(model, g, ifelse(x > 0, sin(x), x + cos(x)^2))
164164
@test function_string(MIME("text/latex"), g) ==
165-
raw"\textsf{ifelse}\left({{x} > {0}}, {\textsf{sin}\left({x}\right)}, {{x} + {\left({\textsf{cos}\left({x}\right)} ^ {2}\right)}}\right)"
165+
raw"\textsf{ifelse}\left({{x} > {0}}, {\textsf{sin}\left({x}\right)}, {{x} + {{\textsf{cos}\left({x}\right)} ^ {2}}}\right)"
166166
return
167167
end
168168

@@ -189,13 +189,12 @@ function test_extension_expression_addmul(
189189
)
190190
model = ModelType()
191191
@variable(model, x)
192-
@test string(@expression(model, x + 3 * sin(x))) == "x + (3 * sin(x))"
193-
@test string(@expression(model, 2 * x + 3 * sin(x))) ==
194-
"(2 x) + (3 * sin(x))"
192+
@test string(@expression(model, x + 3 * sin(x))) == "x + 3 * sin(x)"
193+
@test string(@expression(model, 2 * x + 3 * sin(x))) == "(2 x) + 3 * sin(x)"
195194
@test string(@expression(model, x^2 + 3 * sin(x))) ==
196-
"($(x^2)) + (3 * sin(x))"
195+
"($(x^2)) + 3 * sin(x)"
197196
@test string(@expression(model, sin(x) + 3 * sin(x))) ==
198-
"sin(x) + (3 * sin(x))"
197+
"sin(x) + 3 * sin(x)"
199198
@test string(@expression(model, sin(x) + 3 * x)) == "sin(x) + (3 x)"
200199
@test string(@expression(model, sin(x) + 3 * x * x)) ==
201200
"sin(x) + (3 $(x^2))"
@@ -209,12 +208,12 @@ function test_extension_expression_explicit_add_mul(
209208
model = ModelType()
210209
@variable(model, x)
211210
f = sin(x)
212-
@test string(MA.operate!!(MA.add_mul, 1, 2, f)) == "1 + (2 * $f)"
213-
@test string(MA.operate!!(MA.add_mul, 1, f, 2)) == "1 + ($f * 2)"
214-
@test string(MA.operate!!(MA.add_mul, 1, f, f)) == "1 + ($f * $f)"
215-
@test string(MA.operate!!(MA.add_mul, f, 2, f)) == "$f + (2 * $f)"
216-
@test string(MA.operate!!(MA.add_mul, f, f, 2)) == "$f + ($f * 2)"
217-
@test string(MA.operate!!(MA.add_mul, f, f, f)) == "$f + ($f * $f)"
211+
@test string(MA.operate!!(MA.add_mul, 1, 2, f)) == "1 + 2 * $f"
212+
@test string(MA.operate!!(MA.add_mul, 1, f, 2)) == "1 + $f * 2"
213+
@test string(MA.operate!!(MA.add_mul, 1, f, f)) == "1 + $f * $f"
214+
@test string(MA.operate!!(MA.add_mul, f, 2, f)) == "$f + 2 * $f"
215+
@test string(MA.operate!!(MA.add_mul, f, f, 2)) == "$f + $f * 2"
216+
@test string(MA.operate!!(MA.add_mul, f, f, f)) == "$f + $f * $f"
218217
return
219218
end
220219

@@ -224,13 +223,12 @@ function test_extension_expression_submul(
224223
)
225224
model = ModelType()
226225
@variable(model, x)
227-
@test string(@expression(model, x - 3 * sin(x))) == "x - (3 * sin(x))"
228-
@test string(@expression(model, 2 * x - 3 * sin(x))) ==
229-
"(2 x) - (3 * sin(x))"
226+
@test string(@expression(model, x - 3 * sin(x))) == "x - 3 * sin(x)"
227+
@test string(@expression(model, 2 * x - 3 * sin(x))) == "(2 x) - 3 * sin(x)"
230228
@test string(@expression(model, x^2 - 3 * sin(x))) ==
231-
"($(x^2)) - (3 * sin(x))"
229+
"($(x^2)) - 3 * sin(x)"
232230
@test string(@expression(model, sin(x) - 3 * sin(x))) ==
233-
"sin(x) - (3 * sin(x))"
231+
"sin(x) - 3 * sin(x)"
234232
@test string(@expression(model, sin(x) - 3 * x)) == "sin(x) - (3 x)"
235233
@test string(@expression(model, sin(x) - 3 * x * x)) ==
236234
"sin(x) - (3 $(x^2))"
@@ -247,7 +245,7 @@ function test_extension_aff_expr_convert(
247245
@test _to_string(AffExpr(0.0)) == "+(0)"
248246
@test _to_string(AffExpr(1.0)) == "+(1)"
249247
@test _to_string(x + 1) == "x + 1"
250-
@test _to_string(2x + 1) == "(2 * x) + 1"
248+
@test _to_string(2x + 1) == "2 * x + 1"
251249
@test _to_string(2x) == "2 * x"
252250
return
253251
end
@@ -261,15 +259,15 @@ function test_extension_quad_expr_convert(
261259
_to_string(x) = string(convert(GenericNonlinearExpr{VariableRefType}, x))
262260
@test _to_string(QuadExpr(AffExpr(0.0))) == "+(0)"
263261
@test _to_string(QuadExpr(AffExpr(1.0))) == "+(1)"
264-
@test _to_string(x^2 + 1) == "(x * x) + 1"
265-
@test _to_string(2x^2 + 1) == "(2 * x * x) + 1"
262+
@test _to_string(x^2 + 1) == "x * x + 1"
263+
@test _to_string(2x^2 + 1) == "2 * x * x + 1"
266264
@test _to_string(2x^2) == "2 * x * x"
267-
@test _to_string(x^2 + x + 1) == "x + (x * x) + 1"
268-
@test _to_string(2x^2 + x + 1) == "x + (2 * x * x) + 1"
269-
@test _to_string(2x^2 + x) == "x + (2 * x * x)"
270-
@test _to_string(x^2 + 2x + 1) == "(2 * x) + (x * x) + 1"
271-
@test _to_string(2x^2 + 2x + 1) == "(2 * x) + (2 * x * x) + 1"
272-
@test _to_string(2x^2 + 2x) == "(2 * x) + (2 * x * x)"
265+
@test _to_string(x^2 + x + 1) == "x + x * x + 1"
266+
@test _to_string(2x^2 + x + 1) == "x + 2 * x * x + 1"
267+
@test _to_string(2x^2 + x) == "x + 2 * x * x"
268+
@test _to_string(x^2 + 2x + 1) == "2 * x + x * x + 1"
269+
@test _to_string(2x^2 + 2x + 1) == "2 * x + 2 * x * x + 1"
270+
@test _to_string(2x^2 + 2x) == "2 * x + 2 * x * x"
273271
return
274272
end
275273

@@ -458,7 +456,7 @@ function test_extension_expr_mle(
458456
sum((data[i] - x)^2 for i in 1:n) / (2 * y^2)
459457
)
460458
@test string(obj) ==
461-
"(2 * log(1 / (2 $(y^2)))) - ((4 $(x^2) - 30 x + 85) / (2 $(y^2)))"
459+
"2 * log(1 / (2 $(y^2))) - (4 $(x^2) - 30 x + 85) / (2 $(y^2))"
462460
return
463461
end
464462

@@ -1081,11 +1079,11 @@ function test_printing_truncation()
10811079
@variable(model, x[1:100])
10821080
y = @expression(model, sum(sin.(x) .* 2))
10831081
@test occursin(
1084-
"(sin(x[30]) * 2) + [[...40 terms omitted...]] + (sin(x[71]) * 2)",
1082+
"sin(x[30]) * 2 + [[...40 terms omitted...]] + sin(x[71]) * 2",
10851083
function_string(MIME("text/plain"), y),
10861084
)
10871085
@test occursin(
1088-
"{\\left({\\textsf{sin}\\left({x_{30}}\\right)} * {2}\\right) + {[[\\ldots\\text{40 terms omitted}\\ldots]]} + {\\left({\\textsf{sin}\\left({x_{71}}\\right)} * {2}\\right)}",
1086+
"{\\textsf{sin}\\left({x_{30}}\\right)} * {2} + {[[\\ldots\\text{40 terms omitted}\\ldots]]} + {{\\textsf{sin}\\left({x_{71}}\\right)} * {2}}",
10891087
function_string(MIME("text/latex"), y),
10901088
)
10911089
return
@@ -1466,4 +1464,22 @@ function test_moi_function_no_owner_model()
14661464
return
14671465
end
14681466

1467+
function test_extension_expression_bedmas_parentheses(
1468+
ModelType = Model,
1469+
VariableRefType = VariableRef,
1470+
)
1471+
model = ModelType()
1472+
@variable(model, x)
1473+
@expression(model, y, sin(x))
1474+
@test string(@expression(model, y + 2 * y)) == "sin(x) + 2 * sin(x)"
1475+
@test string(@expression(model, y + 2 / y)) == "sin(x) + 2 / sin(x)"
1476+
@test string(@expression(model, y + 2 ^ y)) == "sin(x) + 2 ^ sin(x)"
1477+
@test string(@expression(model, y + 2 - y)) == "(sin(x) + 2) - sin(x)"
1478+
@test string(@expression(model, y - 2 * y)) == "sin(x) - 2 * sin(x)"
1479+
@test string(@expression(model, y - 2 / y)) == "sin(x) - 2 / sin(x)"
1480+
@test string(@expression(model, y - 2 ^ y)) == "sin(x) - 2 ^ sin(x)"
1481+
@test string(@expression(model, y - 2 + y)) == "(sin(x) - 2) + sin(x)"
1482+
return
1483+
end
1484+
14691485
end # module

0 commit comments

Comments
 (0)