Skip to content

Add a default rule for custom blocks #3570

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

radhakrishnatg
Copy link

Fixes # .

Summary/Motivation:

Add a default rule for custom blocks. With this default rule, construction of custom blocks becomes easier.

@declare_custom_block(FooBlock)
class FooBlockData(BlockData):
    def build(self, *args, option_1, option_2):
        self.x = Var()
        self.y = Param(initialize=option_1)
        
        
m.blk = FooBlock([1, 2, 3], options={"option_1": 1, "option_2": 2})

Implementing build method is optional. If it is not implemented, an empty block will be returned.
Users can overwrite the default build method by passing the rule argument:
m.blk = FooBlock([1, 2, 3], rule=my_custom_block_rule) # Ignores the build method

Changes proposed in this PR:

  • Added default rule argument to the declare_custom_block decorator
  • Added tests to cover the changes.

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@blnicho blnicho requested a review from jsiirola April 29, 2025 18:50
@jsiirola
Copy link
Member

We have been debating this on the developer's call. I think we are leaning toward a slightly different (and a little more general) implementation:

Users want to (easily) create custom block classes (for that matter, we do, too). The catch is that they find themselves needing / wanting to pass in additional parameters / options to the class. IDAES does this by adding a options= keyword to the ProcessBlock class that takes a dict of user-specified options, stores them, and passes them on to the build() method (during construction).

This PR currently codifies the options= argument and adds it to the declare_custom_block decorator

Alternately, we could declare the options in the decorator and pass them as keyword arguments to the rule. This would allow for, e.g.:

@declare_custom_block("FooBlock", rule="build", rule_args=["capex", "opex"])
class FooBlockData(BlockData):
    def build(self, *args, *, capex, opex):
        self.x = Var(list(args))
        self.y = Var()

        self.capex = capex
        self.opex = opex

model.foo = FooBlock(m.I, capex=42, opex=21)

Another option is that we could just save all unexpected keyword arguments passed to the class __init__(). This is a bit of a change, as we currently have error-checking that raises exceptions if a user passes any keyword argument that we weren't expecting. That error checking would get deferred to the rule function. But, if we did this, then users could do something like:

@declare_custom_block("FooBlock", rule="build")
class FooBlockData(BlockData):
   def build(self, *args, *, capex, opex):
       self.x = Var(list(args))
       self.y = Var()
       self.capex = capex
       self.opex = opex

model.foo = FooBlock(m.I, capex=42, opex=21)

This would defer some error checking (you won't get the unexpected keyword argument error when you declare the component -- instead it would be when you construct it). BUT, this would make this change also work with all the existing components (not just blocks declared by @declare_custom_block (so users could pass keyword arguments to things like Constraints, etc.).

Thoughts / comments welcome. I believe that the consensus from the most recent dev call was to lean toward the second alternative proposal.

@radhakrishnatg
Copy link
Author

Thank you @jsiirola for the feedback! I agree that either of the suggested alternatives makes the code cleaner. If I understand things correctly, the first alternative is fairly easy to implement, and does not require too many changes. But the second alternative requires some major changes, and I'm not fully familiar with the code base to make the necessary changes. I agree that it is more general, but I have the following questions:

  1. Is there any scenario where we expect users to provide optional arguments for components (like Var, Constraint, etc.) other than Blocks? In such a case, would it be easier to just define a decorator similar to declare_custom_block (e.g.: declare_custom_var) rather than making changes that work for all component types?
  2. If we make this change, would it affect downstream repos, such as IDAES? Would it break the way IDAES ProcessBlock works, since it collects all the unexpected arguments and then passes it to the build method? (IDAES v1.13 and below looks for the default argument for model options. IDAES v2 and above allows the specification of model options directly as keyword arguments)

If the answer to both the questions is No, then I too agree that the second alternative would be much better. In that case, I will close this PR and try to implement the second approach. However, if the first alternative is sufficient, then I will make the necessary changes and push them to this PR. Please let me know your thoughts. Thank you!

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.

3 participants