Skip to content

Commit 46692f3

Browse files
Move xdsm call in H2IntegrateModel to its own method (#629)
* move xdsm creation in H2IModel to a method so user has to call it if desired instead of having it be automatic * update changelog and docs and adjust create_xdsm logic to reflect that it is an optional call * update docs * update docs * update docs for visualization * change xdsm code cell to not execute on build * switch pdf for png * minor comments update in code example * remove xdsm call * Updated XDSM explanation link --------- Co-authored-by: John Jasa <johnjasa11@gmail.com>
1 parent 85ee3d1 commit 46692f3

5 files changed

Lines changed: 179 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- Added a generic cost model for converters [PR 622](https://github.com/NatLabRockies/H2Integrate/pull/622)
3030
- Updated the `StorageAutoSizingModel` model to be compatible with Pyomo control strategies [PR 621](https://github.com/NatLabRockies/H2Integrate/pull/621)
3131
- Removed a few usages of `shape_by_conn` due to issues with OpenMDAO v3.43.0 release on some computers [PR 632](https://github.com/NatLabRockies/H2Integrate/pull/632)
32+
- Made generating an XDSM diagram from connections in a model optional and added documentation on model visualization. [PR 629](https://github.com/NatLabRockies/H2Integrate/pull/629)
3233

3334
## 0.7.1 [March 13, 2026]
3435

17.9 KB
Loading

docs/user_guide/how_to_set_up_an_analysis.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,90 @@ The `resource_to_tech_connections` section defines how resources (like wind or s
121121
For more information on how to define and interpret technology interconnections, see the {ref}`connecting_technologies` page.
122122
```
123123

124+
## Visualizing the model structure
125+
There are two basic methods for visualizing the model structure of your H2Integrate system model.
126+
You can generate a simplified [XDSM diagram](https://openmdao.github.io/PracticalMDO/Notebooks/ModelConstruction/understanding_xdsm_diagrams.html) showing the technologies and connections specified in your config file, or you can generate an interactive [N2 diagram](https://openmdao.org/newdocs/versions/latest/features/model_visualization/n2_details/n2_details.html) of the full OpenMDAO model.
127+
The XDSM diagram is primarily useful for publications and presentations.
128+
The N2 diagram is primarily useful for debugging. Details for generating XDSM and N2 diagrams of your H2Integrate model are given below.
129+
130+
### XDSM diagram (static and simplified)
131+
132+
Use the built-in `create_xdsm()` method to generate a static system diagram from the
133+
`technology_interconnections` section of your plant config.
134+
135+
```python
136+
from h2integrate.core.h2integrate_model import H2IntegrateModel
137+
import os
138+
139+
140+
# Change to an example directory
141+
os.chdir("../../examples/08_wind_electrolyzer/")
142+
143+
# Build the model from the top-level config file
144+
h2i_model = H2IntegrateModel("wind_plant_electrolyzer.yaml")
145+
146+
# Write XDSM output to connections_xdsm.pdf
147+
h2i_model.create_xdsm(outfile="connections_xdsm")
148+
```
149+
150+
This creates a PDF named `connections_xdsm.pdf` in your current working directory.
151+
152+
```{figure} figures/example_08_xdsm.png
153+
:width: 70%
154+
:align: center
155+
```
156+
*Figure: XDSM diagram generated from the technology interconnections.*
157+
158+
### N2 diagram (interactive and complete)
159+
160+
Use OpenMDAO's `n2` utility to generate an interactive HTML diagram of the full model.
161+
162+
```{code-cell} ipython3
163+
from h2integrate.core.h2integrate_model import H2IntegrateModel
164+
import openmdao.api as om
165+
import os
166+
167+
168+
# Change to an example directory
169+
os.chdir("../../examples/08_wind_electrolyzer/")
170+
171+
# Build and set up the model
172+
h2i_model = H2IntegrateModel("wind_plant_electrolyzer.yaml")
173+
h2i_model.setup()
174+
175+
# Write interactive N2 HTML diagram
176+
om.n2(
177+
h2i_model.prob,
178+
outfile="h2i_n2.html",
179+
display_in_notebook=False, # set to True to display in-line in a notebook
180+
show_browser=False, # set to True to open in a browser at run time
181+
)
182+
```
183+
184+
Open `h2i_n2.html` in a browser to explore model groups, components, and variable connections.
185+
186+
```{code-cell} ipython3
187+
:tags: [remove-input]
188+
import html
189+
from pathlib import Path
190+
from IPython.display import HTML, display
191+
192+
n2_html = "h2i_n2.html"
193+
n2_srcdoc = html.escape(Path(n2_html).read_text(encoding="utf-8"))
194+
display(
195+
HTML(
196+
f'<div style="width:100%; height:600px; overflow:auto; margin:0; padding:0; border:0;">'
197+
f'<iframe srcdoc="{n2_srcdoc}" '
198+
'style="display:block; width:200%; height:600px; border:0; margin:0; padding:0; background:transparent;" '
199+
'loading="lazy"></iframe>'
200+
'</div>'
201+
)
202+
)
203+
```
204+
*Figure: Interactive OpenMDAO N2 diagram showing the full model structure and variable connections.*
205+
206+
207+
124208
## Running the analysis
125209

126210
Once you have the config files defined, you can run the analysis using a simple Python script that inputs the top-level config yaml.

h2integrate/core/h2integrate_model.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,12 +1303,6 @@ def connect_technologies(self):
13031303
f"{dispatching_tech_name}.dispatch_block_rule_function_{tech_name}",
13041304
)
13051305

1306-
if (pyxdsm is not None) and (len(technology_interconnections) > 0):
1307-
try:
1308-
create_xdsm_from_config(self.plant_config)
1309-
except FileNotFoundError as e:
1310-
print(f"Unable to create system XDSM diagram. Error: {e}")
1311-
13121306
def create_driver_model(self):
13131307
"""
13141308
Add the driver to the OpenMDAO model and add recorder.
@@ -1533,3 +1527,29 @@ def _structured(meta_list):
15331527
"explicit_outputs": _structured(explicit_meta),
15341528
"implicit_outputs": _structured(implicit_meta),
15351529
}
1530+
1531+
def create_xdsm(self, outfile="connections_xdsm"):
1532+
"""Create an XDSM diagram from the plant technology interconnections.
1533+
1534+
This method reads ``technology_interconnections`` from ``self.plant_config``
1535+
and delegates diagram generation to
1536+
:func:`h2integrate.core.utilities.create_xdsm_from_config`.
1537+
1538+
Args:
1539+
outfile (str, optional): Base filename for the generated XDSM output.
1540+
The default is ``"connections_xdsm"``.
1541+
1542+
Raises:
1543+
ValueError: If ``technology_interconnections`` is empty or missing from
1544+
the plant configuration.
1545+
"""
1546+
1547+
technology_interconnections = self.plant_config.get("technology_interconnections", [])
1548+
1549+
if len(technology_interconnections) > 0:
1550+
create_xdsm_from_config(self.plant_config, output_file=outfile)
1551+
else:
1552+
raise ValueError(
1553+
"Generating an XDSM diagram requires technology interconnections, "
1554+
"but none were found."
1555+
)

h2integrate/core/test/test_framework.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import shutil
33
from copy import deepcopy
44
from pathlib import Path
5+
from unittest.mock import patch
56

67
import yaml
78
import numpy as np
89
import pytest
910

11+
import h2integrate.core.h2integrate_model as h2i_model_module
1012
from h2integrate import EXAMPLE_DIR
1113
from h2integrate.core.h2integrate_model import H2IntegrateModel
1214
from h2integrate.core.inputs.validation import load_tech_yaml, load_plant_yaml, load_driver_yaml
@@ -501,3 +503,69 @@ def test_no_sites_entry(temp_dir):
501503
assert flow_out.mean() > 0.0
502504

503505
os.chdir(Path(__file__).parent)
506+
507+
508+
@pytest.mark.unit
509+
def test_create_xdsm_calls_create_xdsm_from_config_default_outfile():
510+
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
511+
model = object.__new__(H2IntegrateModel)
512+
model.plant_config = plant_config
513+
514+
with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
515+
model.create_xdsm()
516+
517+
mock_fn.assert_called_once_with(plant_config, output_file="connections_xdsm")
518+
519+
520+
@pytest.mark.unit
521+
def test_create_xdsm_calls_create_xdsm_from_config_custom_outfile():
522+
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
523+
model = object.__new__(H2IntegrateModel)
524+
model.plant_config = plant_config
525+
outfile = "my_custom_xdsm"
526+
527+
with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
528+
model.create_xdsm(outfile=outfile)
529+
530+
mock_fn.assert_called_once_with(plant_config, output_file=outfile)
531+
532+
533+
@pytest.mark.unit
534+
def test_create_xdsm_raises_when_no_interconnections():
535+
plant_config = {"technology_interconnections": []}
536+
model = object.__new__(H2IntegrateModel)
537+
model.plant_config = plant_config
538+
539+
with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
540+
with pytest.raises(ValueError, match="requires technology interconnections"):
541+
model.create_xdsm()
542+
543+
mock_fn.assert_not_called()
544+
545+
546+
@pytest.mark.unit
547+
def test_create_xdsm_raises_when_interconnections_key_missing():
548+
plant_config = {}
549+
model = object.__new__(H2IntegrateModel)
550+
model.plant_config = plant_config
551+
552+
with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
553+
with pytest.raises(ValueError, match="requires technology interconnections"):
554+
model.create_xdsm()
555+
556+
mock_fn.assert_not_called()
557+
558+
559+
@pytest.mark.unit
560+
def test_create_xdsm_propagates_file_not_found_error():
561+
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
562+
model = object.__new__(H2IntegrateModel)
563+
model.plant_config = plant_config
564+
565+
with patch.object(
566+
h2i_model_module,
567+
"create_xdsm_from_config",
568+
side_effect=FileNotFoundError("latex not found"),
569+
):
570+
with pytest.raises(FileNotFoundError, match="latex not found"):
571+
model.create_xdsm()

0 commit comments

Comments
 (0)