Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- Added a generic cost model for converters [PR 622](https://github.com/NatLabRockies/H2Integrate/pull/622)
- Updated the `StorageAutoSizingModel` model to be compatible with Pyomo control strategies [PR 621](https://github.com/NatLabRockies/H2Integrate/pull/621)
- 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)
- 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)

## 0.7.1 [March 13, 2026]

Expand Down
Binary file added docs/user_guide/figures/example_08_xdsm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions docs/user_guide/how_to_set_up_an_analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,90 @@ The `resource_to_tech_connections` section defines how resources (like wind or s
For more information on how to define and interpret technology interconnections, see the {ref}`connecting_technologies` page.
```

## Visualizing the model structure
There are two basic methods for visualizing the model structure of your H2Integrate system model.
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.
The XDSM diagram is primarily useful for publications and presentations.
The N2 diagram is primarily useful for debugging. Details for generating XDSM and N2 diagrams of your H2Integrate model are given below.

### XDSM diagram (static and simplified)

Use the built-in `create_xdsm()` method to generate a static system diagram from the
`technology_interconnections` section of your plant config.

```python
from h2integrate.core.h2integrate_model import H2IntegrateModel
import os


# Change to an example directory
os.chdir("../../examples/08_wind_electrolyzer/")

# Build the model from the top-level config file
h2i_model = H2IntegrateModel("wind_plant_electrolyzer.yaml")

# Write XDSM output to connections_xdsm.pdf
h2i_model.create_xdsm(outfile="connections_xdsm")
```

This creates a PDF named `connections_xdsm.pdf` in your current working directory.

```{figure} figures/example_08_xdsm.png
:width: 70%
:align: center
```
*Figure: XDSM diagram generated from the technology interconnections.*

### N2 diagram (interactive and complete)

Use OpenMDAO's `n2` utility to generate an interactive HTML diagram of the full model.

```{code-cell} ipython3
from h2integrate.core.h2integrate_model import H2IntegrateModel
import openmdao.api as om
import os


# Change to an example directory
os.chdir("../../examples/08_wind_electrolyzer/")

# Build and set up the model
h2i_model = H2IntegrateModel("wind_plant_electrolyzer.yaml")
h2i_model.setup()

# Write interactive N2 HTML diagram
om.n2(
h2i_model.prob,
outfile="h2i_n2.html",
display_in_notebook=False, # set to True to display in-line in a notebook
show_browser=False, # set to True to open in a browser at run time
)
```

Open `h2i_n2.html` in a browser to explore model groups, components, and variable connections.

```{code-cell} ipython3
:tags: [remove-input]
import html
from pathlib import Path
from IPython.display import HTML, display

n2_html = "h2i_n2.html"
n2_srcdoc = html.escape(Path(n2_html).read_text(encoding="utf-8"))
display(
HTML(
f'<div style="width:100%; height:600px; overflow:auto; margin:0; padding:0; border:0;">'
f'<iframe srcdoc="{n2_srcdoc}" '
'style="display:block; width:200%; height:600px; border:0; margin:0; padding:0; background:transparent;" '
'loading="lazy"></iframe>'
'</div>'
)
)
```
*Figure: Interactive OpenMDAO N2 diagram showing the full model structure and variable connections.*



## Running the analysis

Once you have the config files defined, you can run the analysis using a simple Python script that inputs the top-level config yaml.
Expand Down
32 changes: 26 additions & 6 deletions h2integrate/core/h2integrate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,12 +1303,6 @@ def connect_technologies(self):
f"{dispatching_tech_name}.dispatch_block_rule_function_{tech_name}",
)

if (pyxdsm is not None) and (len(technology_interconnections) > 0):
try:
create_xdsm_from_config(self.plant_config)
except FileNotFoundError as e:
print(f"Unable to create system XDSM diagram. Error: {e}")

def create_driver_model(self):
"""
Add the driver to the OpenMDAO model and add recorder.
Expand Down Expand Up @@ -1533,3 +1527,29 @@ def _structured(meta_list):
"explicit_outputs": _structured(explicit_meta),
"implicit_outputs": _structured(implicit_meta),
}

def create_xdsm(self, outfile="connections_xdsm"):
"""Create an XDSM diagram from the plant technology interconnections.

This method reads ``technology_interconnections`` from ``self.plant_config``
and delegates diagram generation to
:func:`h2integrate.core.utilities.create_xdsm_from_config`.

Args:
outfile (str, optional): Base filename for the generated XDSM output.
The default is ``"connections_xdsm"``.

Raises:
ValueError: If ``technology_interconnections`` is empty or missing from
the plant configuration.
"""

technology_interconnections = self.plant_config.get("technology_interconnections", [])

if len(technology_interconnections) > 0:
create_xdsm_from_config(self.plant_config, output_file=outfile)
else:
raise ValueError(
"Generating an XDSM diagram requires technology interconnections, "
"but none were found."
)
68 changes: 68 additions & 0 deletions h2integrate/core/test/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import shutil
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch

import yaml
import numpy as np
import pytest

import h2integrate.core.h2integrate_model as h2i_model_module
from h2integrate import EXAMPLE_DIR
from h2integrate.core.h2integrate_model import H2IntegrateModel
from h2integrate.core.inputs.validation import load_tech_yaml, load_plant_yaml, load_driver_yaml
Expand Down Expand Up @@ -501,3 +503,69 @@ def test_no_sites_entry(temp_dir):
assert flow_out.mean() > 0.0

os.chdir(Path(__file__).parent)


@pytest.mark.unit
def test_create_xdsm_calls_create_xdsm_from_config_default_outfile():
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
model = object.__new__(H2IntegrateModel)
model.plant_config = plant_config

with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
model.create_xdsm()

mock_fn.assert_called_once_with(plant_config, output_file="connections_xdsm")


@pytest.mark.unit
def test_create_xdsm_calls_create_xdsm_from_config_custom_outfile():
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
model = object.__new__(H2IntegrateModel)
model.plant_config = plant_config
outfile = "my_custom_xdsm"

with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
model.create_xdsm(outfile=outfile)

mock_fn.assert_called_once_with(plant_config, output_file=outfile)


@pytest.mark.unit
def test_create_xdsm_raises_when_no_interconnections():
plant_config = {"technology_interconnections": []}
model = object.__new__(H2IntegrateModel)
model.plant_config = plant_config

with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
with pytest.raises(ValueError, match="requires technology interconnections"):
model.create_xdsm()

mock_fn.assert_not_called()


@pytest.mark.unit
def test_create_xdsm_raises_when_interconnections_key_missing():
plant_config = {}
model = object.__new__(H2IntegrateModel)
model.plant_config = plant_config

with patch.object(h2i_model_module, "create_xdsm_from_config") as mock_fn:
with pytest.raises(ValueError, match="requires technology interconnections"):
model.create_xdsm()

mock_fn.assert_not_called()


@pytest.mark.unit
def test_create_xdsm_propagates_file_not_found_error():
plant_config = {"technology_interconnections": [("wind", "electrolyzer", "electricity")]}
model = object.__new__(H2IntegrateModel)
model.plant_config = plant_config

with patch.object(
h2i_model_module,
"create_xdsm_from_config",
side_effect=FileNotFoundError("latex not found"),
):
with pytest.raises(FileNotFoundError, match="latex not found"):
model.create_xdsm()
Loading