Skip to content

Add support for Broadcasted objects #64

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

Merged
merged 2 commits into from
Mar 31, 2025
Merged

Add support for Broadcasted objects #64

merged 2 commits into from
Mar 31, 2025

Conversation

Sbozzolo
Copy link
Member

@Sbozzolo Sbozzolo commented Aug 13, 2024

This PR adds support for specifying diagnostics with a compute function instead of compute!. compute(state, cache, time) can return either a Field or a Base.Broadcast.Broadcasted object (obtained with @lazy) and are now the recommended way to specify diagnostics. compute! is still supported to preserve existing code.

Closes #5
Closes #103

Summary by CodeRabbit

  • New Features

    • Introduced support for defining diagnostic variables with lazy expressions, enabling more concise and efficient computations.
    • Simplified diagnostic computation methods that now return results directly for improved performance.
    • Added new computation functions for lazy evaluation and field-based computations.
  • Bug Fixes

    • Enhanced error handling in the DiagnosticVariable constructor to ensure proper function requirements.
  • Documentation

    • Updated user and developer guides to showcase the new lazy evaluation approach with clear examples and revised usage instructions.
    • Added compatibility notes and usage patterns for the new compute function.

@charleskawczynski
Copy link
Member

Could we instead use LazyBroadcasts @lazy to reduce code duplication, instead? That way we can also easily fuse expressions.

@Sbozzolo
Copy link
Member Author

Yes, my goal is to eventually allow for kernel fusion (which is what I meant with "pathway for future improvement"). However, I don't know how to use LazyBroadcast for that, so I was going for something that would allow us to use it internally in the future, while starting to reduce code verbosity.

Is it just a matter of adding @lazy_broadcasted in front of $(esc(out)) .= $(esc(rhs2)) and explicitely calling materialize! when needed?

Maybe this is a good opportunity to add some user documentation to LazyBroadcast.jl.

@charleskawczynski
Copy link
Member

Yes, my goal is to eventually allow for kernel fusion (which is what I meant with "pathway for future improvement"). However, I don't know how to use LazyBroadcast for that, so I was going for something that would allow us to use it internally in the future, while starting to reduce code verbosity.

Is it just a matter of adding @lazy_broadcasted in front of $(esc(out)) .= $(esc(rhs2)) and explicitely calling materialize! when needed?

Maybe this is a good opportunity to add some user documentation to LazyBroadcast.jl.

I'm happy to add more docs to LazyBroadcast, what do you think is missing?

Using LazyBroadcast, compute_ta (from the docs) could be re-written as:

using LazyBroadcast: @lazy
compute_ta(state, cache, time) = @lazy @. state.ta

and the result could later be used as Base.Broadcast.materialize!(out, bc) where bc is the result of compute_ta.

@Sbozzolo
Copy link
Member Author

Sbozzolo commented Aug 14, 2024

Thank you! I got something to work and I managed to cluster all the materialize! together in a for loop so that it should be easy to fuse them. Two questions:

  • Should the broadcasted expressions be re-computed every time, or is it enough to re-materialize them?
  • With eager evaluation, we need to copy the first time we call compute! because ClimaDiagnostics modifies the output (to do accumulation/reduction). Do we need to do this for lazy evaluation too?

As for documentation: I am looking at LazyBroadcasts.jl and all the docs I see is the readme which does not mention @lazy or compute_ta.

This is what I think could be improved in the readme:

  • the problem that the package solves is not clearly identified
  • it assumes that the reader has specialized knowledge of how Brocasted objects work and how to use them
  • the only example provided is a test case which comes with no explanation and components that are not pedagogically useful (all the Test related stuff)
  • it is not explained how one would use bce or bco
  • the difference between bce and bco is left to the reader to understand
  • the fact that the package has restrictions is not mentioned
  • it does not guide the reader to further documentation, so the way I find myself trying to figure things out is by browsing the testcases

Here are few things I tried to get the code to work:

julia> import LazyBroadcast: @lazy
julia> compute_ta(state, cache, time) = @lazy @. state.ta
ERROR: LoadError: Invalid expression given to materialize_args
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] materialize_args(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/utils.jl:8
 [3] transform(e::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:30 [4] _lazy_broadcasted(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:43 [5] var"@lazy"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:54in expression starting at REPL[17]:1

julia> compute_ta(state, cache, time) = @lazy @. state
ERROR: LoadError: type Symbol has no field args
Stacktrace:
 [1] getproperty(x::Symbol, f::Symbol)
   @ Base ./Base.jl:37
 [2] code_info(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/code_lowered_single_expression.jl:10
 [3] code_lowered_single_expression(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/code_lowered_single_expression.jl:12
 [4] transform(e::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:29 [5] _lazy_broadcasted(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:43 [6] var"@lazy"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:54in expression starting at REPL[18]:1

I also found that these do not work:

julia> a, b = [1], [2]

julia> @lazy a .= b
Expr
  head: Symbol .=
  args: Array{Any}((2,))
    1: Symbol a
    2: Symbol b
dump(expr) = nothing
Expr
  head: Symbol .=
  args: Array{Any}((2,))
    1: Symbol a
    2: Symbol b
dump(expr) = nothing
ERROR: LoadError: Uncaught edge case
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] check_restrictions(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/utils.jl:48
 [3] _lazy_broadcasted(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:42 [4] var"@lazy"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:54in expression starting at REPL[12]:1

julia> @lazy @dot a = b
Expr
  head: Symbol macrocall
  args: Array{Any}((3,))
    1: Symbol @dot
    2: LineNumberNode
      line: Int64 1
      file: Symbol REPL[19]
    3: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol a
        2: Symbol b
dump(expr) = nothing
Expr
  head: Symbol macrocall
  args: Array{Any}((3,))
    1: Symbol @dot
    2: LineNumberNode
      line: Int64 1
      file: Symbol REPL[19]
    3: Expr
      head: Symbol =
      args: Array{Any}((2,))
        1: Symbol a
        2: Symbol b
dump(expr) = nothing
ERROR: LoadError: Uncaught edge case
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] check_restrictions(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/utils.jl:48
 [3] _lazy_broadcasted(expr::Expr)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:42 [4] var"@lazy"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ LazyBroadcast ~/.julia/packages/LazyBroadcast/SVKU8/src/LazyBroadcast.jl:54in expression starting at REPL[19]:1

Eventually I found that it works with @. (it returns Base.Broadcast.Broadcasted(identity, ([2],)))

From here, I think I understand that the correct function to write is

compute_ta(out, state, cache, time) = @lazy @. out = state.ta

This seems to work in my integration test case.

@charleskawczynski
Copy link
Member

Thank you! I got something to work and I managed to cluster all the materialize! together in a for loop so that it should be easy to fuse them. Two questions:

  • Should the broadcasted expressions be re-computed every time, or is it enough to re-materialize them?

Great question. That's safe to do so long the data passed into the broadcasted objects point to the same memory. That said, it's actually very cheap to do this because all it's doing is making an instance of a struct with a bunch of pointers, so I recommend we reconstruct the object every time to be safe.

  • With eager evaluation, we need to copy the first time we call compute! because ClimaDiagnostics modifies the output (to do accumulation/reduction). Do we need to do this for lazy evaluation too?

We may be able to use Base.Broadcast.materialize(bc) if we want an instance (for the first time), and then use Base.Broadcast.materialize!(out, bc) thereafter. I don't think we need to explicitly use copy, since I think Base.Broadcast.materialize will do that for us.

As for documentation: I am looking at LazyBroadcasts.jl and all the docs I see is the readme which does not mention @lazy or compute_ta.

Yeah, the compute_ta is implementation specific. I think we can provide some some simple example use-cases, though.

This is what I think could be improved in the readme:

  • the problem that the package solves is not clearly identified
  • it assumes that the reader has specialized knowledge of how Brocasted objects work and how to use them
  • the only example provided is a test case which comes with no explanation and components that are not pedagogically useful (all the Test related stuff)
  • it is not explained how one would use bce or bco
  • the difference between bce and bco is left to the reader to understand
  • the fact that the package has restrictions is not mentioned
  • it does not guide the reader to further documentation, so the way I find myself trying to figure things out is by browsing the testcases

Yeah, I agree on all of these points. LazyBroadcast.jl was born because it was originally a bunch of utilities in MultiBroadcastFusion.jl, and I realized that they were more generally useful, so I split those utilities off into a separate package. I hadn't fully thought out the use-cases, and if/what pieces could be made more flexible (like the broken examples you showed below), so I wasn't sure how to best document things. I'll open an issue with these points.

Here are few things I tried to get the code to work:...

julia> import LazyBroadcast: @lazy
julia> compute_ta(state, cache, time) = @lazy @. state.ta
ERROR: LoadError: Invalid expression given to materialize_args

julia> compute_ta(state, cache, time) = @lazy @. state
ERROR: LoadError: type Symbol has no field args
...

I also found that these do not work:

julia> a, b = [1], [2]

julia> @lazy a .= b
Expr
  head: Symbol .=
...
ERROR: LoadError: Uncaught edge case

Yeah, this is a bit technical I think, and it does seem that there are some sharp edges (I didn't expect that last example to fail). It might be easier to discuss some of this over zoom. One tricky aspect of this is that, for example, foo(x) could return a vector, in which case, we wouldn't want to assume that the user meant @lazy foo.(x). So, it allows for more flexibility (there are some such examples in the test suite, but not much discussion in the docs about this).

compute_ta(out, state, cache, time) = @lazy @. out = state.ta

Yeah, this will work, and that's how I'd expect it be used. There might be something we can do to remove needing out, but that should be fine for now.

@Sbozzolo Sbozzolo force-pushed the gb/assign branch 3 times, most recently from ba077ff to 34bf23f Compare August 14, 2024 17:59
@Sbozzolo Sbozzolo changed the title Add @assign Add @assign, add support for Broadcasted objects Aug 14, 2024
@Sbozzolo
Copy link
Member Author

Passing ClimaAtmos build: https://buildkite.com/clima/climaatmos-ci/builds/20044

I did not change the EDMF and radiation diagnostics because they are much more complex.

The allocation flame graphs are failing because I added more for loops.

@charleskawczynski
Copy link
Member

Passing ClimaAtmos build: https://buildkite.com/clima/climaatmos-ci/builds/20044

I did not change the EDMF and radiation diagnostics because they are much more complex.

The allocation flame graphs are failing because I added more for loops.

Nice! Do we still need @assign? I thought that it wouldn't be necessary since @lazy changes things to be functional

@Sbozzolo
Copy link
Member Author

Passing ClimaAtmos build: https://buildkite.com/clima/climaatmos-ci/builds/20044
I did not change the EDMF and radiation diagnostics because they are much more complex.
The allocation flame graphs are failing because I added more for loops.

Nice! Do we still need @assign? I thought that it wouldn't be necessary since @lazy changes things to be functional

I am not sure. Does @lazy support diagnostics like this

Fields.array2field(
            cache.radiation.rrtmgp_model.face_sw_flux_dn,
            axes(state.f),
        )

?

@charleskawczynski
Copy link
Member

Fields.array2field(
            cache.radiation.rrtmgp_model.face_sw_flux_dn,
            axes(state.f),
        )

For something like that, we don't even need @lazy, since there's no broadcasting. That said, we can still make a broadcasted object so that it's similar to what @lazy returns:

x = Fields.array2field(
            cache.radiation.rrtmgp_model.face_sw_flux_dn,
            axes(state.f),
        )
bc = Base.broadcasted(identity, x)

@charleskawczynski
Copy link
Member

In fact, any diagnostic that simply returns a field, could just wrap it in Base.broadcasted(identity, ::Fields.Field), and that way everything could be a broadcasted object that will work with Base.Broadcast.materialize!/Base.Broadcast.materialize

@Sbozzolo
Copy link
Member Author

Waiting for
CliMA/LazyBroadcast.jl#8
CliMA/LazyBroadcast.jl#7

@Sbozzolo Sbozzolo marked this pull request as draft September 9, 2024 15:32
@Sbozzolo Sbozzolo force-pushed the gb/assign branch 2 times, most recently from cd7e940 to 93ec15f Compare March 20, 2025 23:17
@Sbozzolo Sbozzolo changed the title Add @assign, add support for Broadcasted objects Add support for Broadcasted objects Mar 20, 2025
@Sbozzolo Sbozzolo force-pushed the gb/assign branch 3 times, most recently from 540752b to 1c7f4ef Compare March 21, 2025 17:59
@Sbozzolo Sbozzolo force-pushed the gb/assign branch 2 times, most recently from 6d08d4b to d237e8f Compare March 26, 2025 23:05
Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

5 similar comments
Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

Copy link

Warning

You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Member

@charleskawczynski charleskawczynski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left one more comment (to use Base.AbstractBroadcasted instead of Base.Broadcast.Broadcasted, as it's a bit more general). I think we're good to merge after that. Thanks for taking this on, I think this will be a nice upgrade! 🚀

@Sbozzolo
Copy link
Member Author

I left one more comment (to use Base.AbstractBroadcasted instead of Base.Broadcast.Broadcasted, as it's a bit more general). I think we're good to merge after that. Thanks for taking this on, I think this will be a nice upgrade! 🚀

I will wait to merge this: I want to fully debug the current issue with CUDA 5.7 before introducing more changes into the diagnostics stack

Copy link

coderabbitai bot commented Mar 31, 2025

Walkthrough

This pull request integrates the LazyBroadcast dependency into the project by updating configuration files and documentation. It revises diagnostic computation functions by replacing in-place operations (using compute!) with direct lazy evaluations (using compute) and updates their signatures. The documentation has been adjusted to reflect these changes, including revised examples and function names. Additionally, tests have been added to validate the new diagnostic variable constructor behavior and the handling of lazy versus field-based computation methods.

Changes

File(s) Change Summary
.buildkite/Project.toml, Project.toml Added LazyBroadcast as a dependency (with UUID and version details) in the [deps], [extras] sections, and updated the test target list.
docs/src/developer_guide.md, docs/src/index.md, docs/src/user_guide.md, docs/src/writers.md Updated documentation to reflect the new diagnostic computation method signatures (removing the out parameter and switching from compute! to compute), added details on lazy expressions, and corrected method names in examples.
src/DiagnosticVariables.jl, src/clima_diagnostics.jl Modified DiagnosticVariable struct to differentiate between compute and compute! functions, added new conditional logic for handling lazy vs. in-place evaluations in the diagnostic computation process.
test/diagnostic_variable.jl, test/integration_test.jl Introduced new test cases for the DiagnosticVariable constructor errors and added tests for lazy and field-based computation functions (compute_my_var_lazy and compute_my_var_field), as well as scheduled diagnostic instances.

Assessment against linked issues

Objective Addressed Explanation
Use lazy broadcasted expressions [#5] The new lazy evaluation using LazyBroadcast is integrated.
Remove duplicated documentation [#103] Duplicated documentation is not consolidated in this PR.

Possibly related issues


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@Sbozzolo
Copy link
Member Author

@coderabbitai full review

Copy link

coderabbitai bot commented Mar 31, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (9)
NEWS.md (1)

19-22: Consider mentioning performance & memory benefits.

Highlight the advantages of using un-evaluated expressions, such as reduced memory allocation or improved kernel fusion, to emphasize why LazyBroadcast may be beneficial.

src/clima_diagnostics.jl (2)

61-65: Enhance the docstring with usage details.

Highlight the preference order: if compute! is defined, it uses in-place, otherwise it falls back to compute. This clarifies behavior for new users.


85-87: Potential scalability concern.

This custom unique method has O(N^2) complexity. For large diagnostic sets, consider more efficient deduplication strategies (e.g., hashing or set membership).

docs/src/user_guide.md (4)

54-59: Clarify the need for copying
The documentation states copy is needed to avoid unintended modifications to state.ta. However, the example compute_ta function on lines 40–41 does not actually call copy. Consider updating the example or clarifying why a direct return without copy is acceptable here, to prevent confusion.


62-62: Use fenced code blocks
This code block is indented rather than fenced, triggering a markdown style warning. Switch to fenced code blocks (e.g., ```julia) to match project styling guidelines.

-    function compute_ta!(out, state, cache, time)
-        if isnothing(out)
-            return state.ta
-        ...
+```julia
+function compute_ta!(out, state, cache, time)
+    if isnothing(out)
+        return state.ta
+    ...
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

62-62: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


99-99: Fix grammatical error
Replace “should be compute:” with “should be computed:”

-    should be compute: Using `LazyBroadcast.jl`, ...
+    should be computed: Using `LazyBroadcast.jl`, ...
🧰 Tools
🪛 LanguageTool

[grammar] ~99-~99: There may an error in the verb form ‘be compute’.
Context: ...t a recipe on how the diagnostic should be compute: Using LazyBroadcast.jl, the snippet ...

(MD_BE_NON_VBP)


109-109: Add a missing comma
Add a comma after “efficiently” to avoid ambiguity.

- knows how to handle it efficiently avoiding the intermediate allocations.
+ knows how to handle it efficiently, avoiding the intermediate allocations.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~109-~109: Possible missing comma found.
Context: ...limaDiagnostics` knows how to handle it efficiently avoiding the intermediate allocations. ...

(AI_HYDRA_LEO_MISSING_COMMA)

src/DiagnosticVariables.jl (1)

39-41: Correct spelling of LazyBroadcast
The docstring references “LazyBoardcast” instead of “LazyBroadcast”.

- (e.g., with `LazyBoardcast.lazy`).
+ (e.g., with `LazyBroadcast.lazy`).
docs/src/developer_guide.md (1)

240-245: Guidance on Leveraging LazyBroadcast
The added instructions emphasize using LazyBroadcast.jl to avoid intermediate allocations. For clarity, consider rephrasing “avoid intermediate allocations” to “prevent unnecessary temporary allocations.”

🧰 Tools
🪛 LanguageTool

[style] ~243-~243: Consider a more expressive alternative.
Context: ... and avoid intermediate allocations. To do that, add LazyBroadcast to your depen...

(DO_ACHIEVE)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b19f9f and b2171e9.

📒 Files selected for processing (11)
  • .buildkite/Project.toml (1 hunks)
  • NEWS.md (3 hunks)
  • Project.toml (2 hunks)
  • docs/src/developer_guide.md (7 hunks)
  • docs/src/index.md (1 hunks)
  • docs/src/user_guide.md (2 hunks)
  • docs/src/writers.md (1 hunks)
  • src/DiagnosticVariables.jl (2 hunks)
  • src/clima_diagnostics.jl (4 hunks)
  • test/diagnostic_variable.jl (1 hunks)
  • test/integration_test.jl (4 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/src/user_guide.md

[uncategorized] ~90-~90: Possible missing comma found.
Context: ...yBroadcast.jl)). Consider the following example where we want to shift the temperature ...

(AI_HYDRA_LEO_MISSING_COMMA)


[grammar] ~99-~99: There may an error in the verb form ‘be compute’.
Context: ...t a recipe on how the diagnostic should be compute: Using LazyBroadcast.jl, the snippet ...

(MD_BE_NON_VBP)


[uncategorized] ~109-~109: Possible missing comma found.
Context: ...limaDiagnostics` knows how to handle it efficiently avoiding the intermediate allocations. ...

(AI_HYDRA_LEO_MISSING_COMMA)

docs/src/developer_guide.md

[style] ~243-~243: Consider a more expressive alternative.
Context: ... and avoid intermediate allocations. To do that, add LazyBroadcast to your depen...

(DO_ACHIEVE)

🪛 markdownlint-cli2 (0.17.2)
docs/src/user_guide.md

62-62: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: downstream ClimaCoupler.jl
  • GitHub Check: downstream ClimaCoupler.jl
  • GitHub Check: downstream ClimaLand.jl
  • GitHub Check: downstream ClimaLand.jl
  • GitHub Check: test (1.11)
  • GitHub Check: downstream ClimaAtmos.jl
  • GitHub Check: downstream ClimaAtmos.jl
🔇 Additional comments (33)
.buildkite/Project.toml (1)

15-15: Added LazyBroadcast dependency as required.

The addition of LazyBroadcast as a dependency is appropriate and aligns with the PR objective of supporting lazy evaluation for diagnostic variables. This enables the new compute function to return Base.Broadcast.Broadcasted objects created using the @lazy macro.

docs/src/index.md (1)

27-28: Documentation updated with new LazyBroadcast feature

The added bullet point clearly documents the new support for lazy expressions using LazyBroadcast.jl. This is an essential documentation update that reflects the changes in functionality introduced by this PR.

test/diagnostic_variable.jl (2)

25-29: Good error testing for mutually exclusive compute functions

This test correctly verifies that an error is thrown when both compute and compute! functions are provided to the DiagnosticVariable constructor, enforcing that they should be mutually exclusive.


31-32: Proper validation for required compute function

This test ensures that the DiagnosticVariable constructor throws an error when neither compute nor compute! is provided, verifying that at least one compute function is required.

docs/src/writers.md (1)

44-44: Method name corrected for consistency

The method name has been updated from FakePressureLevelMethod() to FakePressureLevelsMethod() to ensure consistency with the actual method name referenced in line 91. This correction improves the accuracy of the documentation.

Project.toml (3)

27-27: Consider specifying a version range for LazyBroadcast.

Pinning it to "1" may limit flexibility and could introduce constraints. You might consider a compatible range (e.g., LazyBroadcast = "1" up to <2.0) if feasible.


45-45: Verify correct UUID usage.

Confirm that "9dccce8e-a116-406d-9fcc-a88ed4f510c8" is indeed the current UUID associated with LazyBroadcast.jl, ensuring mismatches do not cause resolution issues.


52-52: Check that tests require LazyBroadcast.

Since LazyBroadcast is now included in test dependencies, verify that existing or upcoming tests actually use this package, and ensure coverage is robust.

NEWS.md (3)

17-18: Good descriptive heading.

This heading clearly delineates the new lazy capability introduced in this release.


23-32: Legacy snippet is sufficiently explained.

The example for the old in-place approach provides good clarity on the motivation for the new feature.


33-46: Great illustration of the new lazy pattern.

This clear example demonstrates how returning a lazy expression simplifies code. Examples for both broadcasted calculations and Fields are well-written.

src/clima_diagnostics.jl (5)

125-137: Well-documented dispatch logic.

The explanatory comments for the three cases (in-place compute, straightforward compute, lazy broadcast compute) are clear and match the code flow.


138-139: In-place computation detection is fine.

has_inplace_compute = !isnothing(variable.compute!) works well to differentiate compute methods without complicating introspection logic.


140-152: Robust branching for multiple compute paths.

This snippet correctly handles both in-place and broadcast-based computations. It’s concise and aligns with your design goal.


240-263: Consistent orchestration logic.

The code correctly mirrors earlier logic, materializing broadcasted results or invoking compute! in place, and storing them accurately.


266-270: Time reduction flow looks solid.

Nothing appears problematic in applying the stored reduction function and ensuring data is successfully accumulated.

docs/src/user_guide.md (2)

40-41: Straightforward function example
These lines correctly illustrate a minimal compute function returning state.ta.


50-50:

✅ Verification successful

Confirm downstream usage
compute = compute_ta looks fine, but please ensure all references to compute_ta align with this new function signature throughout the codebase.

Run the following script to identify calls to compute_ta and confirm none rely on the old in-place usage:


🏁 Script executed:

#!/bin/bash
rg -A 5 "compute_ta"

Length of output: 2846


Downstream Usage Verified – No Further Changes Required

Based on our review, the usage of compute_ta in the assignment compute = compute_ta (as seen in docs/src/user_guide.md at line 50) is consistent with the updated function signature. Our search results confirmed that across the repository—including the definitions in both NEWS.md and docs/src/user_guide.md—all references to compute_ta (and its in-place variant compute_ta!) align with the intended behavior. Please double-check any specific downstream callers if needed, but no discrepancies were found.

src/DiagnosticVariables.jl (1)

70-85: Constructor logic looks good
Requiring that exactly one of compute or compute! is provided is a clean and safe approach. No further issues found.

test/integration_test.jl (5)

70-72: Lazy broadcasting usage
Returning lazy.(u.my_var .+ 10.0) properly defers the computation. This aligns well with lazy evaluation best practices.


74-76: Simple field-based function
Returning u.my_var directly is correct for a non-lazy approach.


84-88: Definition of simple_var_lazy
No issues found. This diagnostic variable setup appears consistent with the new compute design.


90-94: Definition of simple_var_field
Likewise looks good. Uses the straightforward, field-based compute method.


107-128:

❓ Verification inconclusive

Expanded diagnostics coverage
Adding lazy and field-based variants to scheduled diagnostics helps thoroughly test both paths. Confirm that regression tests fully validate correct output, especially for the lazy approach.

You can run the existing integration tests to ensure they pass and produce expected results:

Also applies to: 153-156


🏁 Script executed:

#!/bin/bash
# Ensure all tests pass, focusing on integration_test.jl
# This relies on the project’s typical test harness commands
# (not provided in your snippet), for example:
# julia --project=. -e 'using Pkg; Pkg.test()'
# or a specialized test script if present.
echo "Please run the project’s test suite to verify correctness."

Length of output: 126


Verify Integration Tests and Diagnostic Outputs
The changes introducing both lazy and field-based scheduled diagnostics expand our coverage. However, please confirm that the regression tests fully validate the expected behavior—especially for the lazy variant. In particular, ensure that:

  • The integration tests in test/integration_test.jl (lines 107–128 and 153–156) produce the correct outputs.
  • The implementations for ClimaDiagnostics.ScheduledDiagnostic (along with average_pre_output_hook! and the divisor-based schedule) behave as intended under both lazy and field conditions.
  • Running the full test suite (e.g., via julia --project=. -e 'using Pkg; Pkg.test()') confirms that these paths are correctly exercised.

After verifying these aspects, please update the review accordingly.

docs/src/developer_guide.md (9)

108-108: Example Lambda: Confirming New compute Signature
The lambda now correctly adheres to the new signature (u, p, t), returning the appropriate diagnostic field. Ensure that all similar examples across the documentation follow this updated pattern.


146-151: Parameter Signature Update: Using compute Instead of compute!
The function signature for add_diagnostic_variable! has been updated to accept compute (without the exclamation mark) rather than compute!. This aligns with the new evaluation strategy. Make sure that all related references in the documentation and code are updated accordingly.


176-179: Enhanced Documentation for compute Parameter
The docstring now clearly specifies that the compute function should return a Field or a Base.Broadcast.Broadcasted expression and must avoid allocating new Field objects. This clarification supports the intended lazy evaluation and performance improvement.


181-188: Updated Function Definition in add_diagnostic_variable!
The updated function definition now accepts compute as a keyword argument. This change is consistent with the new design direction and improves clarity regarding the expected function behavior.


204-204: Forwarding the compute Function
The compute parameter is correctly forwarded when constructing the DiagnosticVariable object. This ensures that the variable is configured with the updated lazy or direct computation logic as intended.


232-239: Diagnostic Example for rhoa Using New compute Format
The example for the air density diagnostic now correctly uses the compute function in place of compute!. This is in line with the PR objectives.


247-258: Lazy Evaluation Example for rhoa Diagnostic
This example demonstrates how to apply lazy broadcasting using the lazy macro. It clearly shows how to implement a more complex expression while avoiding intermediate allocations. Make sure that users are reminded to import lazy from LazyBroadcast in their code.


278-285: Implementation of compute_hus with Lazy Evaluation
The function that accepts a moisture model parameter now returns the specific humidity using lazy broadcasting. This implementation aligns with the new evaluation strategy provided that the ambiguity in the overloaded methods is resolved.


287-294: Diagnostic Addition for Specific Humidity (hus)
The registration of the diagnostic variable for specific humidity now uses the updated compute parameter (compute = compute_hus). Once the compute_hus overload ambiguity is addressed, this example should function as intended.

`LazyBroadcast.jl` provides a way to return an unevaluated function.
This is useful in two cases:
1. reduce code verbosity to handle the `isnothing(out)` case
2. allow clustering all the broadcasted expressions in a single place

In turn, 2. is useful because it is the first step in fusing different
broadcasted calls.

This commit adds support for such functions.
@Sbozzolo Sbozzolo enabled auto-merge (rebase) March 31, 2025 17:35
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
docs/src/developer_guide.md (4)

108-108: Example Update: New compute Signature in DiagnosticVariable Initialization

The example now shows initializing a DiagnosticVariable using a lambda with three parameters (u, p, t), reflecting the updated signature that returns a Field or a Base.Broadcast.Broadcasted expression. This update is clear and concise.

Consider adding a brief inline comment (or linking to further documentation) that reminds users this new convention replaces the previous in-place compute! approach.


176-179: Enhanced Documentation for the compute Parameter

The updated documentation now clearly specifies that the compute function must return a Field or a Base.Broadcast.Broadcasted expression and should not allocate new Field objects. This guidance is valuable for users optimizing performance with lazy evaluation.

Consider elaborating slightly on what constitutes a “Field” (or linking to a definition) and why using dot operations is a hint to switch to lazy evaluation.


241-246: Updated Guidance on Lazy Evaluation with LazyBroadcast

The revised text now advises users to leverage LazyBroadcast.jl to avoid intermediate allocations during computation. This is a clear and useful update.

Consider rephrasing “avoid intermediate allocations” to “prevent unnecessary intermediate memory allocations” for enhanced clarity.

🧰 Tools
🪛 LanguageTool

[style] ~243-~243: Consider a more expressive alternative.
Context: ... and avoid intermediate allocations. To do that, add LazyBroadcast to your depen...

(DO_ACHIEVE)


247-258: Practical Example of Lazy Evaluation in a Diagnostic Variable

The example for the "rhoa" diagnostic clearly demonstrates using lazy broadcast (lazy.(...)) to process the computation expression. This not only reinforces the new paradigm but also provides a practical reference for users.

It might be helpful to include a short inline note explaining why the multiplication by 1000 is applied—whether it’s a unit conversion or a simulation-specific scaling—to enhance the example’s instructional value.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2171e9 and 0d59671.

📒 Files selected for processing (10)
  • .buildkite/Project.toml (1 hunks)
  • NEWS.md (3 hunks)
  • Project.toml (2 hunks)
  • docs/src/developer_guide.md (7 hunks)
  • docs/src/index.md (1 hunks)
  • docs/src/user_guide.md (2 hunks)
  • src/DiagnosticVariables.jl (2 hunks)
  • src/clima_diagnostics.jl (4 hunks)
  • test/diagnostic_variable.jl (1 hunks)
  • test/integration_test.jl (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/src/index.md
  • .buildkite/Project.toml
  • Project.toml
  • src/clima_diagnostics.jl
  • test/diagnostic_variable.jl
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
docs/src/user_guide.md

61-61: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🪛 LanguageTool
docs/src/developer_guide.md

[style] ~243-~243: Consider a more expressive alternative.
Context: ... and avoid intermediate allocations. To do that, add LazyBroadcast to your depen...

(DO_ACHIEVE)

⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: downstream ClimaCoupler.jl
  • GitHub Check: downstream ClimaCoupler.jl
  • GitHub Check: downstream ClimaLand.jl
  • GitHub Check: downstream ClimaLand.jl
  • GitHub Check: test (1.11)
  • GitHub Check: downstream ClimaAtmos.jl
  • GitHub Check: climadiagnostics (1.11)
  • GitHub Check: downstream ClimaAtmos.jl
  • GitHub Check: test (1.10)
  • GitHub Check: climadiagnostics (1.10)
🔇 Additional comments (17)
NEWS.md (1)

17-46: Great addition of the lazy evaluation feature documentation!

The documentation clearly explains the new functionality allowing for un-evaluated expressions using LazyBroadcast.jl. The examples effectively demonstrate how the new compute function simplifies the diagnostic variable specification compared to the previous compute! approach.

docs/src/user_guide.md (4)

40-42: The new simplified compute function signature is a nice improvement.

The updated function signature removes the need for output management, making the code more straightforward.


50-50: Updated DiagnosticVariable constructor to use the new compute function.

This correctly implements the changes needed to support the new API.


85-109: Excellent explanation of the benefits of lazy evaluation.

The documentation effectively explains why returning un-evaluated expressions with LazyBroadcast.jl is more efficient than allocating full Fields, with clear examples of both approaches.


59-84:

❓ Verification inconclusive

Good backward compatibility documentation.

The compatibility notice clearly explains the differences between the new compute function and the legacy compute! function, which is important for users upgrading from earlier versions.


🏁 Script executed:

#!/bin/bash
# Verify that the markdown code block is properly formatted

# Check if any indented code blocks exist in the documentation
rg -n "^\s{4,}```" docs/src/

Length of output: 116


Action Required: Verify Markdown Verification Script

The backward compatibility section in docs/src/user_guide.md is clearly written and effectively explains the difference between compute and compute!. However, the shell script used to verify the Markdown code block formatting produced an error due to unescaped backticks. It appears that the error:

/.coderabbit_commands_96e691e5-ebd4-4fdc-abd5-b7b3d0021e04.sh: line 5: unexpected EOF while looking for matching ``'

is caused by Bash interpreting the triple backticks as the start of a command substitution.

Recommendations:

  • Update the verification command to prevent unintended command substitution (for example, use single quotes instead of double quotes):

    rg -n '^\s{4,}```' docs/src/
  • Re-run the updated command to manually confirm that the Markdown code blocks in the documentation are correctly formatted.

Please verify the updated script’s output manually to ensure that no formatting errors are present.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

61-61: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

test/integration_test.jl (5)

17-17: Properly importing the LazyBroadcast dependency.

The code correctly imports the lazy function from LazyBroadcast.


70-76: Good implementation of compute functions for testing.

The test includes two different compute function implementations:

  1. compute_my_var_lazy - correctly uses lazy broadcasting to defer evaluation
  2. compute_my_var_field - simply returns the field without modification

This covers both use cases of the new API.


84-94: Well-defined test DiagnosticVariables using the new compute function.

The test creates diagnostic variables using the new compute function properly, with different variants for lazy evaluation and direct field return.


107-128: Comprehensive testing of scheduled diagnostics with lazy evaluation.

The test creates various scheduled diagnostics using both the lazy and field-based variants, testing both instantaneous values and time averages.


153-156: Complete test coverage by adding new diagnostics to the scheduled list.

All the new diagnostic types are properly added to the scheduled_diagnostics array, ensuring they'll be executed during the test.

src/DiagnosticVariables.jl (4)

27-31: Clear compatibility notice for the new feature.

The compatibility notice clearly indicates that support for the compute function was introduced in version 0.2.13.


35-42: Well-documented function parameters in the docstring.

The docstring has been updated to clearly explain both the compute! and compute parameters, including their expected signatures and return types.


57-68: Good implementation of the DiagnosticVariable struct with optional functions.

The struct has been updated to support both the new compute and legacy compute! functions as Union{Function, Nothing} types, providing flexibility while maintaining backward compatibility.


70-96: Robust constructor with proper validation.

The constructor ensures that exactly one of compute or compute! is provided using xor, which is an elegant way to enforce this requirement. An appropriate error message is raised if this condition is not met.

docs/src/developer_guide.md (3)

146-151: Signature Update: Transition from compute! to compute in add_diagnostic_variable!

The function signature for add_diagnostic_variable! now reflects the removal of the in-place computation (compute!) in favor of the new compute function. This change is in line with the lazy evaluation approach described in the PR.

It would help to document any backward-compatibility considerations or migration instructions for users familiar with the old interface.


187-188: Parameter List Consistency in add_diagnostic_variable!

Including compute in the function’s keyword arguments is now consistent with the new functionality.

No further action required, as the update is straightforward.


293-293: Correct Associative Binding for Specific Humidity Diagnostic

Assigning compute = compute_hus for the "hus" diagnostic is now consistent with the updated interface, ensuring that the diagnostic variable correctly uses the new computation logic.

Comment on lines +270 to +275
compute_hus(state, cache, time) =
compute_hus(state, cache, time, cache.atmos.moisture_model)

compute_hus!(out, state, cache, time) =
compute_hus!(out, state, cache, time, cache.model.moisture_model)
compute_hus!(_, _, _, _, model::T) where {T} =
compute_hus(state, cache, time) =
compute_hus!(state, cache, time, cache.model.moisture_model)
compute_hus(_, _, _, model::T) where {T} =
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Potential Ambiguity in compute_hus Overloads

There are now two definitions for compute_hus(state, cache, time):

  • One dispatches with cache.atmos.moisture_model.
  • The other dispatches with cache.model.moisture_model (using the legacy compute_hus! in the call).

This duality may be confusing and could lead to ambiguity if both cache.atmos and cache.model exist.

Recommendation: Consider consolidating these definitions or providing clearer dispatch conditions (or renaming one of the methods) to avoid potential conflicts in method resolution. Documentation clarifying when each branch is chosen would also be beneficial.

@Sbozzolo Sbozzolo merged commit 74dce08 into main Mar 31, 2025
15 checks passed
@charleskawczynski
Copy link
Member

🎉🎉🎉

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.

Duplicate documentation Use lazy broadcasted expressions
2 participants