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()