More Checking on technology_interconnections - #832
Conversation
elenya-grant
left a comment
There was a problem hiding this comment.
really high-level initial review - thanks for addressing this issue! I think it'd be nice to see some tests with some "fake" systems that are more complex than is possible with models in H2I right now to test the limitations of this. For example, looking at systems with converter and storage technologies that have multiple input or output streams.
Some examples that come to mind are:
- storage tech that has two input commodities (electricity and hydrogen)
- converter that has two input commodities and two output commodities
I also think that the examples in H2I will do a good job of testing this functionality. Thanks John! Will re-review again later (I want to give it a deeper look)
| for source, dest, commodity in self.technology_graph.edges(data="commodity"): | ||
| if commodity is None: | ||
| continue # length-3 connections carry no commodity; skip them | ||
| out_degs_l4[source] = out_degs_l4.get(source, 0) + 1 | ||
| in_degs_l4[dest] = in_degs_l4.get(dest, 0) + 1 |
There was a problem hiding this comment.
I'm wondering if theres a way to do this with dict(self.technology_graph.in_degree) - but then removing non 4-length connections. I can't imagine that it'd actually make the code any simpler - so I like your approach.
There was a problem hiding this comment.
Thanks for this comment and context! I haven't changed anything here, but good to be aware.
| raise ValueError( | ||
| f"Storage technology {storage_tech!r} has {n_in} input connection(s) in " | ||
| f"the technology graph but should have exactly 1." | ||
| ) |
There was a problem hiding this comment.
is there a future case where a storage technology would have 2 different commodity inputs? Like - power and hydrogen? If possible - it'd be nice to check that theres only one input connection per commodity input to the storage.
There was a problem hiding this comment.
I think your code would be fine if (for some imaginary storage that takes electricity and hydrogen input) if the electricity and hydrogen come from the same upstream tech. I think that it'd cause an unnecessary error if electricity came from source_a and hydrogen came from source_b.
There was a problem hiding this comment.
This is a good point for future cases; I modified the behavior of this check. Now, a storage tech receiving electricity from source_a and hydrogen from source_b now passes cleanly - two edges but each commodity has exactly one source. The only invalid case is when the same commodity arrives from two different technologies, e.g. hydrogen from both electrolyzer and h2_storage_bypass, when we should just use a combiner.
There was a problem hiding this comment.
I agree - that sounds great! Thank you! I think thats perfect.
There was a problem hiding this comment.
In PR #816, I added a fake system that handles another type of complex system I imagine we could have in H2I. I don't think that you have to use that specific fake system - but could be good to use something like that to test these checks for the connections into/out of a converter tech (h2integrate/control/control_strategies/system_level/test/test_slc_baseclass.py::test_find_converter_techs_fake_system)
| if "splitter" in tech or "combiner" in tech: | ||
| continue | ||
| if self.tech_control_classifiers.get(tech) == "storage": | ||
| continue # already validated in check 2 |
There was a problem hiding this comment.
I think we should change the _control_classifier for the splitter to be splitter and the _control_classifier for the combiner to be named combiner. This way - we can check the control classifier rather than having logic based on technology names (i.e., a splitter tech has to have "splitter" in the name and a combiner has to have "combiner" in the name)
There was a problem hiding this comment.
Good call, I changed these two cases:
for combiner: "connector" ->"combiner"
for splitter: "connector" -> "splitter"
| # this will naturally grow as we mature the interconnected tech | ||
| technology_interconnections: | ||
| - [nuclear, htse, [heat_out, heat_in]] | ||
| - [nuclear, htse, heat, pipe] |
There was a problem hiding this comment.
can you use the generic transport model here instead? I think that we should have more examples showing that model because I think it'll be very useful as we develop new models with new commodity streams. I don't think we should add heat as a supposed transport item to pipe.
There was a problem hiding this comment.
I've meant to make an issue about this - but I was talking to @genevievestarke a while back and we think that we should more examples using the generic transport model - and I think here is a good opportunity to do that.
elenya-grant
left a comment
There was a problem hiding this comment.
just left some small comments
| # this will naturally grow as we mature the interconnected tech | ||
| technology_interconnections: | ||
| - [nuclear, htse, [heat_out, heat_in]] | ||
| - [nuclear, htse, heat, pipe] |
There was a problem hiding this comment.
I've meant to make an issue about this - but I was talking to @genevievestarke a while back and we think that we should more examples using the generic transport model - and I think here is a good opportunity to do that.
| elif transport_item == "heat": | ||
| units = "kW" |
There was a problem hiding this comment.
see my comment earlier for example 36. I don't think we should add heat to the pipe right now.
|
Thanks for your comments, @elenya-grant! I've just removed heat from the pipe model and moved to use the generic transporter model in example 36. Also expanded the tests in |
elenya-grant
left a comment
There was a problem hiding this comment.
Just left some comments on some edge cases that I think are valid but would cause errors when they shouldn't. I can share the tests I made to check these if you want. I will finish this review tommorrow! Looks good so far! Only some unlikely (at the moment) edge-cases that I found that this would throw unnecessary errors for
| source_param, dest_param = connected_parameter | ||
| if not isinstance(source_param, str) or not isinstance(dest_param, str): | ||
| continue | ||
| if source_param.endswith("_out") and dest_param.endswith("_in"): |
There was a problem hiding this comment.
Not trying to nit-pick but I just remembered PR #774 and wonder if we should do:
if source_param.split("[")[0].endswith("_out") and dest_param.split("[")[0].endswith("_in"):| f"length-3 format. Use a length-4 connection instead: " | ||
| f"[{source_tech!r}, {dest_tech!r}, {commodity_from_source!r}, " | ||
| f"'<transport_tech>']. Use the generic transport component if " | ||
| f"no specific transport is needed." |
There was a problem hiding this comment.
Could we rephrase this part of the error message: "Use the generic transport component if no specific transport is needed"?
I think that the generic transport component is useful if a) no specific transport is needed and b) it's a commodity that isn't supported by cable or pipe models (perhaps because its a new commodity). I think the most simple re-phrasing would be "You can use the GenericTransporterPerformanceModel to transport {commodity_from_source!r}"? But I'm open to other ideas
| def test_validate_interconnections_multi_commodity_storage(subtests): | ||
| """Storage tech with two different-commodity inputs should be allowed. | ||
|
|
||
| Models a future storage that accepts both electricity (for charging) and | ||
| hydrogen (as the stored commodity) from two separate upstream technologies. | ||
| Each commodity has exactly one source, so no error should be raised. | ||
| """ | ||
| interconnections = [ | ||
| ["source_elec", "storage", "electricity", "cable"], | ||
| ["source_h2", "storage", "hydrogen", "pipe"], | ||
| ["storage", "h2_combiner", "hydrogen", "pipe"], | ||
| ] | ||
| classifiers = { | ||
| "storage": "storage", | ||
| "h2_combiner": "combiner", | ||
| } | ||
| fake = _make_fake_model(interconnections, classifiers) | ||
|
|
||
| with subtests.test("two-commodity storage passes validation"): |
There was a problem hiding this comment.
I know that this use-case is still not something we imagine happening a ton yet - but in my head, this should be a valid system of interconnections but an error is thrown:
interconnections = [
["source_elec", "storage", "electricity", "cable"],
["source_h2", "storage", "hydrogen", "pipe"],
["storage", "h2_combiner", "hydrogen", "pipe"],
["source_h2", "h2_combiner", "hydrogen", "pipe"],
["source_elec", "elec_combiner", "electricity", "cable"],
["storage", "elec_combiner", "electricity", "cable"],
]
classifiers = {
"storage": "storage",
"h2_combiner": "combiner",
"elec_combiner": "combiner",
}There was a problem hiding this comment.
Thanks for the example connections. The main difference here is that a singular storage technology accepts and stores both hydrogen and electricity, is that right? I don't immediately know of a physical system that behaves like that -- do you? I would argue we don't need to support this edge case at this moment, but let me know if you have a need or example case for it.
| for upstream_tech in upstream_techs_l4: | ||
| storage_upstream_techs.add(upstream_tech) | ||
| n_out_upstream = out_degs_l4.get(upstream_tech, 0) | ||
| if n_out_upstream > 2: | ||
| raise ValueError( | ||
| f"Technology {upstream_tech!r} feeds storage technology " | ||
| f"{storage_tech!r} but has {n_out_upstream} output connection(s). " | ||
| f"It should connect only to {storage_tech!r} and a combiner " | ||
| f"(at most 2 output streams)." | ||
| ) |
There was a problem hiding this comment.
I think that here we should check that n_out_upstream based on the commodity
| for upstream_tech in upstream_techs_l4: | |
| storage_upstream_techs.add(upstream_tech) | |
| n_out_upstream = out_degs_l4.get(upstream_tech, 0) | |
| if n_out_upstream > 2: | |
| raise ValueError( | |
| f"Technology {upstream_tech!r} feeds storage technology " | |
| f"{storage_tech!r} but has {n_out_upstream} output connection(s). " | |
| f"It should connect only to {storage_tech!r} and a combiner " | |
| f"(at most 2 output streams)." | |
| ) | |
| for upstream_tech in upstream_techs_l4: | |
| storage_upstream_techs.add(upstream_tech) | |
| for commodity in self.technology_graph.edges[upstream_tech, storage_tech].get("commodity"): | |
| n_out_upstream = out_commodity_dests.get(upstream_tech, {}).get(commodity,0) | |
| if n_out_upstream > 2: | |
| raise ValueError( | |
| f"Technology {upstream_tech!r} feeds storage technology " | |
| f"{storage_tech!r} but has {n_out_upstream} output connection(s). " | |
| f"It should connect only to {storage_tech!r} and a combiner " | |
| f"(at most 2 output streams)." | |
| ) |
| n_out = out_degs_l4.get(storage_tech, 0) | ||
| if n_out > 1: | ||
| raise ValueError( | ||
| f"Storage technology {storage_tech!r} has {n_out} output connection(s) in " | ||
| f"the technology graph but should have at most 1." | ||
| ) |
There was a problem hiding this comment.
| n_out = out_degs_l4.get(storage_tech, 0) | |
| if n_out > 1: | |
| raise ValueError( | |
| f"Storage technology {storage_tech!r} has {n_out} output connection(s) in " | |
| f"the technology graph but should have at most 1." | |
| ) | |
| for commodity, n_out in out_commodity_dests.get(storage_tech, {}).items(): | |
| if n_out > 1: | |
| raise ValueError( | |
| f"Storage technology {storage_tech!r} has {n_out} output connection(s) in " | |
| f"of commodity {commodity} but should have at most 1." | |
| ) |
More Checking on
technology_interconnectionsThis PR adds structured topology validation for
technology_interconnectionsinH2IntegrateModel, raising descriptive errors for common configuration mistakes that previously went undetected until OpenMDAO setup or silently produced wrong results.Summary of changes:
_validate_technology_interconnectionsmethod with three topology checks.tech_control_classifierspopulation to always run (was SLC-only).setup()before the existing commodity check.Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
tech_control_classifiersto non-SLC modelsType of Reviewer Feedback Requested (on Draft PR)
Implementation feedback:
Confirm that the three topology checks (length-3 commodity pair detection, storage topology,
and general max-in/out) match the expected behavior described in the issue.
Section 3: General PR Checklist
docs/files are up-to-date, or added when necessaryCHANGELOG.md"A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
XYZshould be replaced with the actual number.Section 4: Related Issues
Solves #809
Section 5: Impacted Areas of the Software
Section 5.1: New Files
N/A
Section 5.2: Modified Files
h2integrate/core/h2integrate_model.py_validate_technology_interconnectionsmethod.tech_control_classifierspopulation to non-SLC models.setup().h2integrate/core/test/test_framework.pytest_validate_technology_interconnectionsintegration test.test_check_tech_interconnectionssecond subtest to use a non-storage tech.Section 6: Additional Supporting Information
The three checks implemented are:
Discouraged length-3
[commodity_out, commodity_in]connections - When a length-3 connection passes a list pair like["hydrogen_out", "hydrogen_in"]where the prefixes match, aValueErroris raised prompting the user to use a length-4 connection with an explicit commodity and transport component.Storage technology topology - For each storage technology in
tech_control_classifiers: exactly 1 length-4 input is required; at most 1 length-4 output is allowed; and the direct upstream technology may have at most 2 output streams (one to storage, one to a combiner).General max-in/out for non-special technologies - All other technologies connected via length-4 connections (excluding splitters, combiners, storage techs, and the direct upstream of storage techs) must have at most 1 input stream and 1 output stream. The storage upstream exemption applies only to the output-stream count; input streams are still checked.
The fix to always populate
tech_control_classifiers(removing theself.slcguard) ensures checks 2 and 3 work correctly for non-SLC systems where storage is still present. All other usages oftech_control_classifierswere already guarded byif self.slc:so there are no unintended side effects.Section 7: Test Results, if applicable
Validated with targeted test runs on
h2integrate/core/test/test_framework.pyand the fullh2integrate/core/test/suite. All failures intest_pose_optimization.pyare pre-existing OpenMDAO API mismatches unrelated to this PR.h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py::test_size_mode_outputsAll targeted tests passed.
Section 8 (Optional): New Model Checklist
N/A