Skip to content

Add a default rule for custom blocks#3570

Merged
blnicho merged 16 commits intoPyomo:mainfrom
radhakrishnatg:custom-block
Jul 24, 2025
Merged

Add a default rule for custom blocks#3570
blnicho merged 16 commits intoPyomo:mainfrom
radhakrishnatg:custom-block

Conversation

@radhakrishnatg
Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
Contributor 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!

@github-project-automation github-project-automation bot moved this from Review In Progress to Reviewer Approved in August 2025 Release Jul 15, 2025
@blnicho blnicho self-requested a review July 15, 2025 19:01
@blnicho blnicho merged commit 367c2ff into Pyomo:main Jul 24, 2025
64 of 65 checks passed
@github-project-automation github-project-automation bot moved this from Reviewer Approved to Done in August 2025 Release Jul 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

5 participants