diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb89bdcb2..72aa0d1fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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]
diff --git a/docs/user_guide/figures/example_08_xdsm.png b/docs/user_guide/figures/example_08_xdsm.png
new file mode 100644
index 000000000..a9c6b9301
Binary files /dev/null and b/docs/user_guide/figures/example_08_xdsm.png differ
diff --git a/docs/user_guide/how_to_set_up_an_analysis.md b/docs/user_guide/how_to_set_up_an_analysis.md
index 2995ad02d..b64d4c7e3 100644
--- a/docs/user_guide/how_to_set_up_an_analysis.md
+++ b/docs/user_guide/how_to_set_up_an_analysis.md
@@ -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'
'
+ f''
+ '
'
+ )
+)
+```
+*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.
diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py
index 2e97b426c..ba1569c91 100644
--- a/h2integrate/core/h2integrate_model.py
+++ b/h2integrate/core/h2integrate_model.py
@@ -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.
@@ -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."
+ )
diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py
index c4d4aed32..d9540984d 100644
--- a/h2integrate/core/test/test_framework.py
+++ b/h2integrate/core/test/test_framework.py
@@ -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
@@ -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()