Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Algebra for AbstractExpression objects; and StructuredExpression #92

Merged
merged 53 commits into from
Jul 27, 2024

Conversation

MilesCranmer
Copy link
Member

@MilesCranmer MilesCranmer commented Jul 2, 2024

Algebra for AbstractExpression objects

This lets you create new expressions with regular mathematical operators. Unlike the equivalent interface for Node <: AbstractExpressionNode, this does not rely on the admittedly hacky global LATEST_OPERATORS, but on the actual internal operators stored in the expression. Thus, this is much more robust.

For example, say we create two expressions, f and g, via parse_expression:

julia> using DynamicExpressions

julia> kws = (;
           binary_operators=[+, -, *, /],
           unary_operators=[-, cos, exp],  # Use - as a unary operator too
           variable_names=["x", "y"],
       )
(binary_operators = Function[+, -, *, /], unary_operators = Function[-, cos, exp], variable_names = ["x", "y"])

julia> f = parse_expression(:(x * x - cos(2.5f0 * y + -0.5f0)); kws...)
(x * x) - cos((2.5 * y) + -0.5)

julia> g = parse_expression(:(exp(-(y * y))); kws...)
exp(-(y * y))

we can compose these using any of the declared operators, into a new expression type:

julia> f + g
((x * x) - cos((2.5 * y) + -0.5)) + exp(-(y * y))

julia> (f + g)(randn(2, 100));  # Evaluate as normal (without needing to pass operators)

julia> f + g |> typeof
Expression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(-), typeof(cos), typeof(exp)}}, variable_names::Vector{String}}}

Note that this requires the type of the expression to be identical for this to work. So, for example, since OperatorEnum types the functions, each expression much have a priori declared the right operators!

This also means you can't use operators outside the enum:

julia> f \ g
ERROR: ArgumentError: Operator \ not found in operators for expression type Expression{Float32, Node{Float32}, @NamedTuple{operators::OperatorEnum{Tuple{typeof(+), typeof(-), typeof(*), typeof(/)}, Tuple{typeof(-), typeof(cos), typeof(exp)}}, variable_names::Vector{String}}} with binary operators (+, -, *, /)
Stacktrace:
...

StructuredExpression

This also introduces a StructuredExpression, which builds on the above point. It lets you declare an expression with fixed expression structure. You could also think of this as lazily building the expression based on a static expression.

The nice part about this is it works directly on other AbstractExpression, putting them into a NamedTuple, and then puts them together when calling get_tree. For example, using the previous example:

using DynamicExpressions
kws = (;
    binary_operators=[+, -, *, /],
    unary_operators=[-, cos, exp],
    variable_names=["x", "y"],
)
f = parse_expression(:(x * x - cos(2.5f0 * y + -0.5f0)); kws...)
g = parse_expression(:(exp(-(y * y))); kws...)
# ^ Regular `Expression` type

We can create f_plus_g which a fixed + operation in the middle. This function is fixed into the expression type itself, so this should be very fast:

julia> f_plus_g = StructuredExpression((; f, g), trees -> trees.f + trees.g)
((x * x) - cos((2.5 * y) + -0.5)) + exp(-(y * y))

Note the string printout – this is because get_tree calls the structure function, patching together the tree! But the overall structure is fixed.

We can look at the individual components:

julia> get_contents(f_plus_g) |> keys
(:f, :g)

julia> get_contents(f_plus_g).f
(x * x) - cos((2.5 * y) + -0.5)

julia> get_contents(f_plus_g).g
exp(-(y * y))

and get the function that puts them together:

julia> get_metadata(f_plus_g).structure
#3 (generic function with 1 method)

which takes a named tuple as input.

This is all type stable, and should be a nice feature for SymbolicRegression.jl!

It also already has get_constants and set_constants! declared – these simply loop through the individual sub-expressions and get or set constants.


cc @gca30 @AlCap23 @avik-pal in case of interest

This comment was marked as resolved.

@coveralls

This comment was marked as resolved.

@coveralls

This comment was marked as resolved.

This comment was marked as resolved.

2 similar comments

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Contributor

github-actions bot commented Jul 4, 2024

Pull Request Test Coverage Report for Build 9799648670

Details

  • 84 of 90 (93.33%) changed or added relevant lines in 2 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.08%) to 95.434%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/ExpressionAlgebra.jl 46 52 88.46%
Totals Coverage Status
Change from base Build 9793838744: -0.08%
Covered Lines: 2278
Relevant Lines: 2387

💛 - Coveralls

1 similar comment
@coveralls
Copy link

coveralls commented Jul 4, 2024

Pull Request Test Coverage Report for Build 9799648670

Details

  • 84 of 90 (93.33%) changed or added relevant lines in 2 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.08%) to 95.434%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/ExpressionAlgebra.jl 46 52 88.46%
Totals Coverage Status
Change from base Build 9793838744: -0.08%
Covered Lines: 2278
Relevant Lines: 2387

💛 - Coveralls

@coveralls
Copy link

coveralls commented Jul 27, 2024

Pull Request Test Coverage Report for Build 10126841842

Details

  • 129 of 129 (100.0%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.5%) to 95.901%

Totals Coverage Status
Change from base Build 9829024598: 0.5%
Covered Lines: 2410
Relevant Lines: 2513

💛 - Coveralls

@MilesCranmer MilesCranmer merged commit f650e28 into master Jul 27, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants