• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MilesCranmer / SymbolicRegression.jl / 11394658450

17 Oct 2024 11:32PM UTC coverage: 95.332% (+0.6%) from 94.757%
11394658450

Pull #355

github

web-flow
Merge a9e5332c7 into 3892a6659
Pull Request #355: Create `TemplateExpression` for providing a pre-defined functional structure and constraints

253 of 257 new or added lines in 15 files covered. (98.44%)

3 existing lines in 2 files now uncovered.

2818 of 2956 relevant lines covered (95.33%)

38419396.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.32
/src/MutationFunctions.jl
1
module MutationFunctionsModule
2

3
using Random: default_rng, AbstractRNG
4
using DynamicExpressions:
5
    AbstractExpressionNode,
6
    AbstractExpression,
7
    AbstractNode,
8
    NodeSampler,
9
    get_contents,
10
    with_contents,
11
    constructorof,
12
    set_node!,
13
    count_nodes,
14
    has_constants,
15
    has_operators
16
using ..CoreModule: AbstractOptions, DATA_TYPE
17

18
"""
19
    get_contents_for_mutation(ex::AbstractExpression, rng::AbstractRNG)
20

21
Return the contents of an expression, which can be mutated.
22
You can overload this function for custom expression types that
23
need to be mutated in a specific way.
24

25
The second return value is an optional context object that will be
26
passed to the `with_contents_for_mutation` function.
27
"""
28
function get_contents_for_mutation(ex::AbstractExpression, rng::AbstractRNG)
98,927,594✔
29
    return get_contents(ex), nothing
98,928,842✔
30
end
31

32
"""
33
    with_contents_for_mutation(ex::AbstractExpression, context)
34

35
Replace the contents of an expression with the given context object.
36
You can overload this function for custom expression types that
37
need to be mutated in a specific way.
38
"""
39
function with_contents_for_mutation(ex::AbstractExpression, new_contents, ::Nothing)
98,952,230✔
40
    return with_contents(ex, new_contents)
98,942,507✔
41
end
42

43
"""
44
    random_node(tree::AbstractNode; filter::F=Returns(true))
45

46
Return a random node from the tree. You may optionally
47
filter the nodes matching some condition before sampling.
48
"""
49
function random_node(
×
50
    tree::AbstractNode, rng::AbstractRNG=default_rng(); filter::F=Returns(true)
51
) where {F<:Function}
52
    Base.depwarn(
×
53
        "Instead of `random_node(tree, filter)`, use `rand(NodeSampler(; tree, filter))`",
54
        :random_node,
55
    )
56
    return rand(rng, NodeSampler(; tree, filter))
×
57
end
58

59
"""Swap operands in binary operator for ops like pow and divide"""
60
function swap_operands(ex::AbstractExpression, rng::AbstractRNG=default_rng())
918,762✔
61
    tree, context = get_contents_for_mutation(ex, rng)
1,092,244✔
62
    ex = with_contents_for_mutation(ex, swap_operands(tree, rng), context)
1,830,617✔
63
    return ex
918,430✔
64
end
65
function swap_operands(tree::AbstractNode, rng::AbstractRNG=default_rng())
897,133✔
66
    if !any(node -> node.degree == 2, tree)
2,714,830✔
67
        return tree
334✔
68
    end
69
    node = rand(rng, NodeSampler(; tree, filter=t -> t.degree == 2))
23,252,328✔
70
    node.l, node.r = node.r, node.l
896,807✔
71
    return tree
896,807✔
72
end
73

74
"""Randomly convert an operator into another one (binary->binary; unary->unary)"""
75
function mutate_operator(
4,994,510✔
76
    ex::AbstractExpression{T}, options::AbstractOptions, rng::AbstractRNG=default_rng()
77
) where {T<:DATA_TYPE}
78
    tree, context = get_contents_for_mutation(ex, rng)
5,978,605✔
79
    ex = with_contents_for_mutation(ex, mutate_operator(tree, options, rng), context)
6,030,567✔
80
    return ex
4,994,368✔
81
end
82
function mutate_operator(
4,895,214✔
83
    tree::AbstractExpressionNode{T},
84
    options::AbstractOptions,
85
    rng::AbstractRNG=default_rng(),
86
) where {T}
87
    if !(has_operators(tree))
4,895,221✔
88
        return tree
690✔
89
    end
90
    node = rand(rng, NodeSampler(; tree, filter=t -> t.degree != 0))
117,798,708✔
91
    if node.degree == 1
4,894,741✔
92
        node.op = rand(rng, 1:(options.nuna))
1,417,008✔
93
    else
94
        node.op = rand(rng, 1:(options.nbin))
3,477,843✔
95
    end
96
    return tree
4,894,861✔
97
end
98

99
"""Randomly perturb a constant"""
100
function mutate_constant(
1,987,270✔
101
    ex::AbstractExpression{T},
102
    temperature,
103
    options::AbstractOptions,
104
    rng::AbstractRNG=default_rng(),
105
) where {T<:DATA_TYPE}
106
    tree, context = get_contents_for_mutation(ex, rng)
2,447,323✔
107
    ex = with_contents_for_mutation(
2,452,477✔
108
        ex, mutate_constant(tree, temperature, options, rng), context
109
    )
110
    return ex
1,986,959✔
111
end
112
function mutate_constant(
2,068,067✔
113
    tree::AbstractExpressionNode{T},
114
    temperature,
115
    options::AbstractOptions,
116
    rng::AbstractRNG=default_rng(),
117
) where {T<:DATA_TYPE}
118
    # T is between 0 and 1.
119

120
    if !(has_constants(tree))
4,133,036✔
121
        return tree
14,666✔
122
    end
123
    node = rand(rng, NodeSampler(; tree, filter=t -> (t.degree == 0 && t.constant)))
40,078,725✔
124

125
    node.val *= mutate_factor(T, temperature, options, rng)
2,053,463✔
126

127
    return tree
2,053,228✔
128
end
129

130
function mutate_factor(::Type{T}, temperature, options, rng) where {T<:DATA_TYPE}
2,144,999✔
131
    bottom = 1//10
2,145,005✔
132
    maxChange = options.perturbation_factor * temperature + 1 + bottom
2,145,005✔
133
    factor = T(maxChange^rand(rng, T))
2,145,000✔
134
    makeConstBigger = rand(rng, Bool)
2,144,810✔
135

136
    factor = makeConstBigger ? factor : 1 / factor
3,215,404✔
137

138
    if rand(rng) > options.probability_negate_constant
2,144,724✔
139
        factor *= -1
2,123,113✔
140
    end
141
    return factor
2,144,698✔
142
end
143

144
# TODO: Shouldn't we add a mutate_feature here?
145

146
"""Add a random unary/binary operation to the end of a tree"""
147
function append_random_op(
9,188,731✔
148
    ex::AbstractExpression{T},
149
    options::AbstractOptions,
150
    nfeatures::Int,
151
    rng::AbstractRNG=default_rng();
152
    makeNewBinOp::Union{Bool,Nothing}=nothing,
153
) where {T<:DATA_TYPE}
154
    tree, context = get_contents_for_mutation(ex, rng)
5,568,060✔
155
    ex = with_contents_for_mutation(
5,614,850✔
156
        ex, append_random_op(tree, options, nfeatures, rng; makeNewBinOp), context
157
    )
158
    return ex
4,570,740✔
159
end
160
function append_random_op(
17,339,811✔
161
    tree::AbstractExpressionNode{T},
162
    options::AbstractOptions,
163
    nfeatures::Int,
164
    rng::AbstractRNG=default_rng();
165
    makeNewBinOp::Union{Bool,Nothing}=nothing,
166
) where {T<:DATA_TYPE}
167
    node = rand(rng, NodeSampler(; tree, filter=t -> t.degree == 0))
185,572,398✔
168

169
    if makeNewBinOp === nothing
8,673,994✔
170
        choice = rand(rng)
8,316,745✔
171
        makeNewBinOp = choice < options.nbin / (options.nuna + options.nbin)
8,316,544✔
172
    end
173

174
    if makeNewBinOp
8,673,788✔
175
        newnode = constructorof(typeof(tree))(;
6,367,909✔
176
            op=rand(rng, 1:(options.nbin)),
177
            l=make_random_leaf(nfeatures, T, typeof(tree), rng, options),
178
            r=make_random_leaf(nfeatures, T, typeof(tree), rng, options),
179
        )
180
    else
181
        newnode = constructorof(typeof(tree))(;
2,306,222✔
182
            op=rand(rng, 1:(options.nuna)),
183
            l=make_random_leaf(nfeatures, T, typeof(tree), rng, options),
184
        )
185
    end
186

187
    set_node!(node, newnode)
8,674,312✔
188

189
    return tree
8,675,174✔
190
end
191

192
"""Insert random node"""
193
function insert_random_op(
56,874,123✔
194
    ex::AbstractExpression{T},
195
    options::AbstractOptions,
196
    nfeatures::Int,
197
    rng::AbstractRNG=default_rng(),
198
) where {T<:DATA_TYPE}
199
    tree, context = get_contents_for_mutation(ex, rng)
69,177,668✔
200
    ex = with_contents_for_mutation(
69,784,024✔
201
        ex, insert_random_op(tree, options, nfeatures, rng), context
202
    )
203
    return ex
56,851,381✔
204
end
205
function insert_random_op(
55,737,199✔
206
    tree::AbstractExpressionNode{T},
207
    options::AbstractOptions,
208
    nfeatures::Int,
209
    rng::AbstractRNG=default_rng(),
210
) where {T<:DATA_TYPE}
211
    node = rand(rng, NodeSampler(; tree))
55,725,639✔
212
    choice = rand(rng)
55,805,390✔
213
    makeNewBinOp = choice < options.nbin / (options.nuna + options.nbin)
55,794,580✔
214
    left = copy(node)
55,779,366✔
215

216
    if makeNewBinOp
55,791,342✔
217
        right = make_random_leaf(nfeatures, T, typeof(tree), rng, options)
46,538,004✔
218
        newnode = constructorof(typeof(tree))(;
46,539,774✔
219
            op=rand(rng, 1:(options.nbin)), l=left, r=right
220
        )
221
    else
222
        newnode = constructorof(typeof(tree))(; op=rand(rng, 1:(options.nuna)), l=left)
9,258,304✔
223
    end
224
    set_node!(node, newnode)
55,785,191✔
225
    return tree
55,792,439✔
226
end
227

228
"""Add random node to the top of a tree"""
229
function prepend_random_op(
4,571,323✔
230
    ex::AbstractExpression{T},
231
    options::AbstractOptions,
232
    nfeatures::Int,
233
    rng::AbstractRNG=default_rng(),
234
) where {T<:DATA_TYPE}
235
    tree, context = get_contents_for_mutation(ex, rng)
5,567,380✔
236
    ex = with_contents_for_mutation(
5,613,820✔
237
        ex, prepend_random_op(tree, options, nfeatures, rng), context
238
    )
239
    return ex
4,570,922✔
240
end
241
function prepend_random_op(
4,482,906✔
242
    tree::AbstractExpressionNode{T},
243
    options::AbstractOptions,
244
    nfeatures::Int,
245
    rng::AbstractRNG=default_rng(),
246
) where {T<:DATA_TYPE}
247
    node = tree
4,482,869✔
248
    choice = rand(rng)
4,482,854✔
249
    makeNewBinOp = choice < options.nbin / (options.nuna + options.nbin)
4,482,851✔
250
    left = copy(tree)
4,482,822✔
251

252
    if makeNewBinOp
4,483,180✔
253
        right = make_random_leaf(nfeatures, T, typeof(tree), rng, options)
3,713,349✔
254
        newnode = constructorof(typeof(tree))(;
3,713,373✔
255
            op=rand(rng, 1:(options.nbin)), l=left, r=right
256
        )
257
    else
258
        newnode = constructorof(typeof(tree))(; op=rand(rng, 1:(options.nuna)), l=left)
769,881✔
259
    end
260
    set_node!(node, newnode)
4,483,160✔
261
    return node
4,483,177✔
262
end
263

264
function make_random_leaf(
57,406,481✔
265
    nfeatures::Int,
266
    ::Type{T},
267
    ::Type{N},
268
    rng::AbstractRNG=default_rng(),
269
    ::Union{AbstractOptions,Nothing}=nothing,
270
) where {T<:DATA_TYPE,N<:AbstractExpressionNode}
271
    if rand(rng, Bool)
57,391,892✔
272
        return constructorof(N)(T; val=randn(rng, T))
28,713,509✔
273
    else
274
        return constructorof(N)(T; feature=rand(rng, 1:nfeatures))
28,706,774✔
275
    end
276
end
277

278
"""Return a random node from the tree with parent, and side ('n' for no parent)"""
279
function random_node_and_parent(tree::AbstractNode, rng::AbstractRNG=default_rng())
28,647,910✔
280
    if tree.degree == 0
28,647,352✔
281
        return tree, tree, 'n'
47,271✔
282
    end
283
    parent = rand(rng, NodeSampler(; tree, filter=t -> t.degree != 0))
769,615,759✔
284
    if parent.degree == 1 || rand(rng, Bool)
50,238,467✔
285
        return (parent.l, parent, 'l')
17,786,141✔
286
    else
287
        return (parent.r, parent, 'r')
10,821,723✔
288
    end
289
end
290

291
"""Select a random node, and splice it out of the tree."""
292
function delete_random_op!(
16,734,359✔
293
    ex::AbstractExpression{T},
294
    options::AbstractOptions,
295
    nfeatures::Int,
296
    rng::AbstractRNG=default_rng(),
297
) where {T<:DATA_TYPE}
298
    tree, context = get_contents_for_mutation(ex, rng)
19,936,976✔
299
    ex = with_contents_for_mutation(
20,129,810✔
300
        ex, delete_random_op!(tree, options, nfeatures, rng), context
301
    )
302
    return ex
16,734,276✔
303
end
304
function delete_random_op!(
16,366,161✔
305
    tree::AbstractExpressionNode{T},
306
    options::AbstractOptions,
307
    nfeatures::Int,
308
    rng::AbstractRNG=default_rng(),
309
) where {T<:DATA_TYPE}
310
    node, parent, side = random_node_and_parent(tree, rng)
29,602,627✔
311
    isroot = side == 'n'
16,368,867✔
312

313
    if node.degree == 0
16,368,978✔
314
        # Replace with new constant
315
        newnode = make_random_leaf(nfeatures, T, typeof(tree), rng, options)
9,775,491✔
316
        set_node!(node, newnode)
15,257,878✔
317
    elseif node.degree == 1
6,595,170✔
318
        # Join one of the children with the parent
319
        if isroot
2,242,868✔
320
            return node.l
×
321
        elseif parent.l == node
3,926,458✔
322
            parent.l = node.l
1,464,746✔
323
        else
324
            parent.r = node.l
778,174✔
325
        end
326
    else
327
        # Join one of the children with the parent
328
        if rand(rng, Bool)
4,352,613✔
329
            if isroot
2,177,287✔
330
                return node.l
×
331
            elseif parent.l == node
3,960,594✔
332
                parent.l = node.l
1,450,112✔
333
            else
334
                parent.r = node.l
727,235✔
335
            end
336
        else
337
            if isroot
2,175,420✔
338
                return node.r
×
339
            elseif parent.l == node
3,957,246✔
340
                parent.l = node.r
1,449,234✔
341
            else
342
                parent.r = node.r
726,227✔
343
            end
344
        end
345
    end
346
    return tree
16,370,348✔
347
end
348

349
function randomize_tree(
631,371✔
350
    ex::AbstractExpression,
351
    curmaxsize::Int,
352
    options::AbstractOptions,
353
    nfeatures::Int,
354
    rng::AbstractRNG=default_rng(),
355
)
356
    tree, context = get_contents_for_mutation(ex, rng)
792,616✔
357
    ex = with_contents_for_mutation(
792,214✔
358
        ex, randomize_tree(tree, curmaxsize, options, nfeatures, rng), context
359
    )
360
    return ex
630,886✔
361
end
362
function randomize_tree(
630,834✔
363
    ::AbstractExpressionNode{T},
364
    curmaxsize::Int,
365
    options::AbstractOptions,
366
    nfeatures::Int,
367
    rng::AbstractRNG=default_rng(),
368
) where {T<:DATA_TYPE}
369
    tree_size_to_generate = rand(rng, 1:curmaxsize)
630,831✔
370
    return gen_random_tree_fixed_size(tree_size_to_generate, options, nfeatures, T, rng)
630,823✔
371
end
372

373
"""Create a random equation by appending random operators"""
374
function gen_random_tree(
284,258✔
375
    length::Int,
376
    options::AbstractOptions,
377
    nfeatures::Int,
378
    ::Type{T},
379
    rng::AbstractRNG=default_rng(),
380
) where {T<:DATA_TYPE}
381
    # Note that this base tree is just a placeholder; it will be replaced.
382
    tree = constructorof(options.node_type)(T; val=convert(T, 1))
285,128✔
383
    for i in 1:length
282,317✔
384
        # TODO: This can be larger number of nodes than length.
385
        tree = append_random_op(tree, options, nfeatures, rng)
1,066,817✔
386
    end
1,415,636✔
387
    return tree
283,752✔
388
end
389

390
function gen_random_tree_fixed_size(
630,893✔
391
    node_count::Int,
392
    options::AbstractOptions,
393
    nfeatures::Int,
394
    ::Type{T},
395
    rng::AbstractRNG=default_rng(),
396
) where {T<:DATA_TYPE}
397
    tree = make_random_leaf(nfeatures, T, options.node_type, rng, options)
630,867✔
398
    cur_size = count_nodes(tree)
630,833✔
399
    while cur_size < node_count
3,971,645✔
400
        if cur_size == node_count - 1  # only unary operator allowed.
3,341,116✔
401
            options.nuna == 0 && break # We will go over the requested amount, so we must break.
357,932✔
402
            tree = append_random_op(tree, options, nfeatures, rng; makeNewBinOp=false)
449,215✔
403
        else
404
            tree = append_random_op(tree, options, nfeatures, rng)
2,983,318✔
405
        end
406
        cur_size = count_nodes(tree)
6,679,364✔
407
    end
3,341,203✔
408
    return tree
630,875✔
409
end
410

411
function crossover_trees(
3,465,080✔
412
    ex1::E, ex2::E, rng::AbstractRNG=default_rng()
413
) where {T,E<:AbstractExpression{T}}
414
    if ex1 === ex2
3,465,079✔
NEW
415
        error("Attempted to crossover the same expression!")
×
416
    end
417
    tree1, context1 = get_contents_for_mutation(ex1, rng)
4,247,506✔
418
    tree2, context2 = get_contents_for_mutation(ex2, rng)
4,247,525✔
419
    out1, out2 = crossover_trees(tree1, tree2, rng)
4,247,574✔
420
    ex1 = with_contents_for_mutation(ex1, out1, context1)
4,331,630✔
421
    ex2 = with_contents_for_mutation(ex2, out2, context2)
4,331,215✔
422
    return ex1, ex2
3,464,741✔
423
end
424

425
"""Crossover between two expressions"""
426
function crossover_trees(
6,142,666✔
427
    tree1::N, tree2::N, rng::AbstractRNG=default_rng()
428
) where {T,N<:AbstractExpressionNode{T}}
429
    if tree1 === tree2
6,142,657✔
NEW
430
        error("Attempted to crossover the same tree!")
×
431
    end
432
    tree1 = copy(tree1)
6,142,643✔
433
    tree2 = copy(tree2)
6,142,661✔
434

435
    node1, parent1, side1 = random_node_and_parent(tree1, rng)
11,146,246✔
436
    node2, parent2, side2 = random_node_and_parent(tree2, rng)
11,146,327✔
437

438
    node1 = copy(node1)
6,142,737✔
439

440
    if side1 == 'l'
6,142,777✔
441
        parent1.l = copy(node2)
3,689,358✔
442
        # tree1 now contains this.
443
    elseif side1 == 'r'
2,453,520✔
444
        parent1.r = copy(node2)
2,431,416✔
445
        # tree1 now contains this.
446
    else # 'n'
447
        # This means that there is no parent2.
448
        tree1 = copy(node2)
22,104✔
449
    end
450

451
    if side2 == 'l'
6,142,756✔
452
        parent2.l = node1
3,691,901✔
453
    elseif side2 == 'r'
2,450,963✔
454
        parent2.r = node1
2,428,929✔
455
    else # 'n'
456
        tree2 = node1
22,035✔
457
    end
458
    return tree1, tree2
6,142,722✔
459
end
460

461
function get_two_nodes_without_loop(tree::AbstractNode, rng::AbstractRNG; max_attempts=10)
227,080✔
462
    for _ in 1:max_attempts
113,541✔
463
        parent = rand(rng, NodeSampler(; tree, filter=t -> t.degree != 0))
1,880,511✔
464
        new_child = rand(rng, NodeSampler(; tree, filter=t -> t !== tree))
1,880,469✔
465

466
        would_form_loop = any(t -> t === parent, new_child)
384,840✔
467
        if !would_form_loop
135,751✔
468
            return (parent, new_child, false)
113,547✔
469
        end
470
    end
44,412✔
471
    return (tree, tree, true)
×
472
end
473

474
function form_random_connection!(ex::AbstractExpression, rng::AbstractRNG=default_rng())
225,528✔
475
    tree, context = get_contents_for_mutation(ex, rng)
274,755✔
476
    return with_contents_for_mutation(ex, form_random_connection!(tree, rng), context)
281,727✔
477
end
478
function form_random_connection!(tree::AbstractNode, rng::AbstractRNG=default_rng())
118,771✔
479
    if length(tree) < 5
224,855✔
480
        return tree
5,225✔
481
    end
482

483
    parent, new_child, would_form_loop = get_two_nodes_without_loop(tree, rng)
138,410✔
484

485
    if would_form_loop
113,545✔
486
        return tree
×
487
    end
488

489
    # Set one of the children to be this new child:
490
    if parent.degree == 1 || rand(rng, Bool)
208,653✔
491
        parent.l = new_child
66,185✔
492
    else
493
        parent.r = new_child
47,362✔
494
    end
495
    return tree
113,546✔
496
end
497

498
function break_random_connection!(ex::AbstractExpression, rng::AbstractRNG=default_rng())
47,665✔
499
    tree, context = get_contents_for_mutation(ex, rng)
59,099✔
500
    return with_contents_for_mutation(ex, break_random_connection!(tree, rng), context)
58,840✔
501
end
502
function break_random_connection!(tree::AbstractNode, rng::AbstractRNG=default_rng())
25,833✔
503
    tree.degree == 0 && return tree
25,833✔
504
    parent = rand(rng, NodeSampler(; tree, filter=t -> t.degree != 0))
363,857✔
505
    if parent.degree == 1 || rand(rng, Bool)
46,375✔
506
        parent.l = copy(parent.l)
15,303✔
507
    else
508
        parent.r = copy(parent.r)
10,402✔
509
    end
510
    return tree
25,704✔
511
end
512

513
function is_valid_rotation_node(node::AbstractNode)
76,594,749✔
514
    return (node.degree > 0 && node.l.degree > 0) || (node.degree == 2 && node.r.degree > 0)
130,841,658✔
515
end
516

517
function randomly_rotate_tree!(ex::AbstractExpression, rng::AbstractRNG=default_rng())
2,790,352✔
518
    tree, context = get_contents_for_mutation(ex, rng)
3,315,056✔
519
    rotated_tree = randomly_rotate_tree!(tree, rng)
3,314,944✔
520
    return with_contents_for_mutation(ex, rotated_tree, context)
2,823,854✔
521
end
522
function randomly_rotate_tree!(tree::AbstractNode, rng::AbstractRNG=default_rng())
2,729,243✔
523
    num_rotation_nodes = count(is_valid_rotation_node, tree)
2,740,173✔
524

525
    # Return the tree if no valid nodes are found
526
    if num_rotation_nodes == 0
2,729,327✔
527
        return tree
235,244✔
528
    end
529

530
    root_is_valid_rotation_node = is_valid_rotation_node(tree)
2,707,688✔
531

532
    # Now, we decide if we want to rotate at the root, or at a random node
533
    rotate_at_root = root_is_valid_rotation_node && rand(rng) < 1.0 / num_rotation_nodes
2,494,072✔
534

535
    subtree_parent = if rotate_at_root
2,494,055✔
536
        tree
925,913✔
537
    else
538
        rand(
4,062,175✔
539
            rng,
540
            NodeSampler(;
541
                tree,
542
                filter=t -> (
89,061,038✔
543
                    (t.degree > 0 && is_valid_rotation_node(t.l)) ||
544
                    (t.degree == 2 && is_valid_rotation_node(t.r))
545
                ),
546
            ),
547
        )
548
    end
549

550
    subtree_side = if rotate_at_root
2,494,105✔
551
        :n
925,912✔
552
    elseif subtree_parent.degree == 1
1,568,182✔
553
        :l
147,838✔
554
    else
555
        if is_valid_rotation_node(subtree_parent.l) &&
1,749,946✔
556
            (!is_valid_rotation_node(subtree_parent.r) || rand(rng, Bool))
557
            :l
868,935✔
558
        else
559
            :r
5,482,520✔
560
        end
561
    end
562

563
    subtree_root = if rotate_at_root
2,494,061✔
564
        tree
925,906✔
565
    elseif subtree_side == :l
1,568,169✔
566
        subtree_parent.l
1,016,767✔
567
    else
568
        subtree_parent.r
4,062,217✔
569
    end
570

571
    # Perform the rotation
572
    # (reference: https://web.archive.org/web/20230326202118/https://upload.wikimedia.org/wikipedia/commons/1/15/Tree_Rotations.gif)
573
    right_rotation_valid = subtree_root.l.degree > 0
2,494,063✔
574
    left_rotation_valid = subtree_root.degree == 2 && subtree_root.r.degree > 0
2,494,061✔
575

576
    right_rotation = right_rotation_valid && (!left_rotation_valid || rand(rng, Bool))
3,358,993✔
577
    if right_rotation
2,494,052✔
578
        node_5 = subtree_root
1,612,628✔
579
        node_3 = leftmost(node_5)
1,612,630✔
580
        node_4 = rightmost(node_3)
2,684,685✔
581

582
        set_leftmost!(node_5, node_4)
1,908,881✔
583
        set_rightmost!(node_3, node_5)
2,684,679✔
584
        if rotate_at_root
1,612,628✔
585
            return node_3  # new root
590,143✔
586
        elseif subtree_side == :l
1,022,491✔
587
            subtree_parent.l = node_3
661,495✔
588
        else
589
            subtree_parent.r = node_3
361,008✔
590
        end
591
    else  # left rotation
592
        node_3 = subtree_root
881,477✔
593
        node_5 = rightmost(node_3)
1,762,952✔
594
        node_4 = leftmost(node_5)
881,480✔
595

596
        set_rightmost!(node_3, node_4)
1,762,954✔
597
        set_leftmost!(node_5, node_3)
1,051,419✔
598
        if rotate_at_root
881,474✔
599
            return node_5  # new root
335,777✔
600
        elseif subtree_side == :l
545,700✔
601
            subtree_parent.l = node_5
355,272✔
602
        else
603
            subtree_parent.r = node_5
190,432✔
604
        end
605
    end
606

607
    return tree
1,568,158✔
608
end
609

610
#! format: off
611
# These functions provide an easier way to work with unary nodes, by
612
# simply letting `.r` fall back to `.l` if the node is a unary operator.
613
leftmost(node::AbstractNode) = node.l
2,494,053✔
614
rightmost(node::AbstractNode) = node.degree == 1 ? node.l : node.r
4,447,519✔
615
set_leftmost!(node::AbstractNode, l::AbstractNode) = (node.l = l)
2,494,046✔
616
set_rightmost!(node::AbstractNode, r::AbstractNode) = node.degree == 1 ? (node.l = r) : (node.r = r)
4,447,523✔
617
#! format: on
618

619
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc