diff --git a/include/openmc/cross_sections.h b/include/openmc/cross_sections.h index 2b0473becd2..06140a6a8cf 100644 --- a/include/openmc/cross_sections.h +++ b/include/openmc/cross_sections.h @@ -62,6 +62,11 @@ extern vector libraries; //! libraries void read_cross_sections_xml(); +//! Read cross sections file (either XML or multigroup H5) and populate data +//! libraries +//! \param[in] root node of the cross_sections.xml +void read_cross_sections_xml(pugi::xml_node root); + //! Load nuclide and thermal scattering data from HDF5 files // //! \param[in] nuc_temps Temperatures for each nuclide in [K] diff --git a/include/openmc/file_utils.h b/include/openmc/file_utils.h index f9c23468dfd..cb52b33c89b 100644 --- a/include/openmc/file_utils.h +++ b/include/openmc/file_utils.h @@ -3,14 +3,31 @@ #include // for ifstream #include +#include namespace openmc { +// TODO: replace with std::filesystem when switch to C++17 is made +//! Determine if a path is a directory +//! \param[in] path Path to check +//! \return Whether the path is a directory +inline bool dir_exists(const std::string& path) +{ + struct stat s; + if (stat(path.c_str(), &s) != 0) return false; + + return s.st_mode & S_IFDIR; +} + //! Determine if a file exists //! \param[in] filename Path to file //! \return Whether file exists inline bool file_exists(const std::string& filename) { + // rule out file being a path to a directory + if (dir_exists(filename)) + return false; + std::ifstream s {filename}; return s.good(); } diff --git a/include/openmc/geometry_aux.h b/include/openmc/geometry_aux.h index b248d491ac2..cf62debc040 100644 --- a/include/openmc/geometry_aux.h +++ b/include/openmc/geometry_aux.h @@ -10,6 +10,7 @@ #include #include "openmc/vector.h" +#include "openmc/xml_interface.h" namespace openmc { @@ -19,8 +20,13 @@ extern std::unordered_map> extern std::unordered_map universe_level_counts; } // namespace model +//! Read geometry from XML file void read_geometry_xml(); +//! Read geometry from XML node +//! \param[in] root node of geometry XML element +void read_geometry_xml(pugi::xml_node root); + //============================================================================== //! Replace Universe, Lattice, and Material IDs with indices. //============================================================================== diff --git a/include/openmc/initialize.h b/include/openmc/initialize.h index 869be444144..a9b8b336f96 100644 --- a/include/openmc/initialize.h +++ b/include/openmc/initialize.h @@ -1,6 +1,8 @@ #ifndef OPENMC_INITIALIZE_H #define OPENMC_INITIALIZE_H +#include + #ifdef OPENMC_MPI #include "mpi.h" #endif @@ -11,7 +13,13 @@ int parse_command_line(int argc, char* argv[]); #ifdef OPENMC_MPI void initialize_mpi(MPI_Comm intracomm); #endif -void read_input_xml(); + +//! Read material, geometry, settings, and tallies from a single XML file +bool read_model_xml(); +//! Read inputs from separate XML files +void read_separate_xml_files(); +//! Write some output that occurs right after initialization +void initial_output(); } // namespace openmc diff --git a/include/openmc/material.h b/include/openmc/material.h index b251a3ca858..81f4e1421c8 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -221,6 +221,10 @@ double density_effect(const vector& f, const vector& e_b_sq, //! Read material data from materials.xml void read_materials_xml(); +//! Read material data XML node +//! \param[in] root node of materials XML element +void read_materials_xml(pugi::xml_node root); + void free_memory_material(); } // namespace openmc diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 650b7e16a1c..a415b174731 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -279,6 +279,10 @@ void voxel_finalize(hid_t dspace, hid_t dset, hid_t memspace); //! Read plot specifications from a plots.xml file void read_plots_xml(); +//! Read plot specifications from an XML Node +//! \param[in] XML node containing plot info +void read_plots_xml(pugi::xml_node root); + //! Clear memory void free_memory_plot(); diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 1e061b235b7..cd0ab477c47 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -127,9 +127,12 @@ extern double weight_survive; //!< Survival weight after Russian roulette //============================================================================== //! Read settings from XML file -//! \param[in] root XML node for void read_settings_xml(); +//! Read settings from XML node +//! \param[in] root XML node for +void read_settings_xml(pugi::xml_node root); + void free_memory_settings(); } // namespace openmc diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 3cead91dc7d..56a51370a84 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -48,10 +48,10 @@ class Tally { void set_nuclides(const vector& nuclides); //! returns vector of indices corresponding to the tally this is called on - const vector& filters() const { return filters_; } + const vector& filters() const { return filters_; } //! \brief Returns the tally filter at index i - int32_t filters(int i) const { return filters_[i]; } + int32_t filters(int i) const { return filters_[i]; } void set_filters(gsl::span filters); @@ -178,6 +178,10 @@ extern double global_tally_leakage; //! Read tally specification from tallies.xml void read_tallies_xml(); +//! Read tally specification from an XML node +//! \param[in] root node of tallies XML element +void read_tallies_xml(pugi::xml_node root); + //! \brief Accumulate the sum of the contributions from each history within the //! batch to a new random variable void accumulate_tallies(); diff --git a/openmc/_xml.py b/openmc/_xml.py index 32679fd89ec..6799a4e2d87 100644 --- a/openmc/_xml.py +++ b/openmc/_xml.py @@ -1,22 +1,40 @@ -def clean_indentation(element, level=0, spaces_per_level=2): - """ - copy and paste from https://effbot.org/zone/element-lib.htm#prettyprint - it basically walks your tree and adds spaces and newlines so the tree is - printed in a nice way +def clean_indentation(element, level=0, spaces_per_level=2, trailing_indent=True): + """Set indentation of XML element and its sub-elements. + Copied and pasted from https://effbot.org/zone/element-lib.htm#prettyprint. + It walks your tree and adds spaces and newlines so the tree is + printed in a nice way. + + Parameters + ---------- + level : int + Indentation level for the element passed in (default 0) + spaces_per_level : int + Number of spaces per indentation level (default 2) + trailing_indent : bool + Whether or not to add indentation after closing the element + """ i = "\n" + level*spaces_per_level*" " + # ensure there's always some tail for the element passed in + if not element.tail: + element.tail = "" + if len(element): if not element.text or not element.text.strip(): element.text = i + spaces_per_level*" " - if not element.tail or not element.tail.strip(): + if trailing_indent and (not element.tail or not element.tail.strip()): element.tail = i for sub_element in element: + # `trailing_indent` is intentionally not forwarded to the recursive + # call. Any child element of the topmost element should add + # indentation at the end to ensure its parent's indentation is + # correct. clean_indentation(sub_element, level+1, spaces_per_level) if not sub_element.tail or not sub_element.tail.strip(): sub_element.tail = i else: - if level and (not element.tail or not element.tail.strip()): + if trailing_indent and level and (not element.tail or not element.tail.strip()): element.tail = i diff --git a/openmc/cell.py b/openmc/cell.py index 0cea73b3242..d03052623df 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -650,7 +650,7 @@ def from_xml_element(cls, elem, surfaces, materials, get_universe): surfaces : dict Dictionary mapping surface IDs to :class:`openmc.Surface` instances materials : dict - Dictionary mapping material IDs to :class:`openmc.Material` + Dictionary mapping material ID strings to :class:`openmc.Material` instances (defined in :math:`openmc.Geometry.from_xml`) get_universe : function Function returning universe (defined in diff --git a/openmc/executor.py b/openmc/executor.py index a73d896ffea..22862a7cf90 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -9,7 +9,7 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, plot=False, restart_file=None, threads=None, tracks=False, event_based=None, - openmc_exec='openmc', mpi_args=None): + openmc_exec='openmc', mpi_args=None, path_input=None): """Converts user-readable flags in to command-line arguments to be run with the OpenMC executable via subprocess. @@ -42,6 +42,9 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, mpi_args : list of str, optional MPI execute command and any additional MPI arguments to pass, e.g. ['mpiexec', '-n', '8']. + path_input : str or Pathlike + Path to a single XML file or a directory containing XML files for the + OpenMC executable to read. .. versionadded:: 0.13.0 @@ -82,6 +85,9 @@ def _process_CLI_arguments(volume=False, geometry_debug=False, particles=None, if mpi_args is not None: args = mpi_args + args + if path_input is not None: + args += [path_input] + return args @@ -118,7 +124,7 @@ def _run(args, output, cwd): raise RuntimeError(error_msg) -def plot_geometry(output=True, openmc_exec='openmc', cwd='.'): +def plot_geometry(output=True, openmc_exec='openmc', cwd='.', path_input=None): """Run OpenMC in plotting mode Parameters @@ -129,6 +135,9 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'): Path to OpenMC executable cwd : str, optional Path to working directory to run in + path_input : str + Path to a single XML file or a directory containing XML files for the + OpenMC executable to read. Raises ------ @@ -136,10 +145,13 @@ def plot_geometry(output=True, openmc_exec='openmc', cwd='.'): If the `openmc` executable returns a non-zero status """ - _run([openmc_exec, '-p'], output, cwd) + args = [openmc_exec, '-p'] + if path_input is not None: + args += [path_input] + _run(args, output, cwd) -def plot_inline(plots, openmc_exec='openmc', cwd='.'): +def plot_inline(plots, openmc_exec='openmc', cwd='.', path_input=None): """Display plots inline in a Jupyter notebook. .. versionchanged:: 0.13.0 @@ -155,6 +167,9 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'): Path to OpenMC executable cwd : str, optional Path to working directory to run in + path_input : str + Path to a single XML file or a directory containing XML files for the + OpenMC executable to read. Raises ------ @@ -171,7 +186,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'): openmc.Plots(plots).export_to_xml(cwd) # Run OpenMC in geometry plotting mode - plot_geometry(False, openmc_exec, cwd) + plot_geometry(False, openmc_exec, cwd, path_input) if plots is not None: images = [_get_plot_image(p, cwd) for p in plots] @@ -179,7 +194,8 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.'): def calculate_volumes(threads=None, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None): + openmc_exec='openmc', mpi_args=None, + path_input=None): """Run stochastic volume calculations in OpenMC. This function runs OpenMC in stochastic volume calculation mode. To specify @@ -210,6 +226,10 @@ def calculate_volumes(threads=None, output=True, cwd='.', cwd : str, optional Path to working directory to run in. Defaults to the current working directory. + path_input : str or Pathlike + Path to a single XML file or a directory containing XML files for the + OpenMC executable to read. + Raises ------ @@ -223,14 +243,16 @@ def calculate_volumes(threads=None, output=True, cwd='.', """ args = _process_CLI_arguments(volume=True, threads=threads, - openmc_exec=openmc_exec, mpi_args=mpi_args) + openmc_exec=openmc_exec, mpi_args=mpi_args, + path_input=path_input) _run(args, output, cwd) def run(particles=None, threads=None, geometry_debug=False, restart_file=None, tracks=False, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None, event_based=False): + openmc_exec='openmc', mpi_args=None, event_based=False, + path_input=None): """Run an OpenMC simulation. Parameters @@ -239,17 +261,17 @@ def run(particles=None, threads=None, geometry_debug=False, Number of particles to simulate per generation. threads : int, optional Number of OpenMP threads. If OpenMC is compiled with OpenMP threading - enabled, the default is implementation-dependent but is usually equal - to the number of hardware threads available (or a value set by the + enabled, the default is implementation-dependent but is usually equal to + the number of hardware threads available (or a value set by the :envvar:`OMP_NUM_THREADS` environment variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. restart_file : str, optional Path to restart file to use tracks : bool, optional - Enables the writing of particles tracks. The number of particle - tracks written to tracks.h5 is limited to 1000 unless - Settings.max_tracks is set. Defaults to False. + Enables the writing of particles tracks. The number of particle tracks + written to tracks.h5 is limited to 1000 unless Settings.max_tracks is + set. Defaults to False. output : bool Capture OpenMC output from standard out cwd : str, optional @@ -258,13 +280,17 @@ def run(particles=None, threads=None, geometry_debug=False, openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. mpi_args : list of str, optional - MPI execute command and any additional MPI arguments to pass, - e.g. ['mpiexec', '-n', '8']. + MPI execute command and any additional MPI arguments to pass, e.g. + ['mpiexec', '-n', '8']. event_based : bool, optional Turns on event-based parallelism, instead of default history-based .. versionadded:: 0.12 + path_input : str or Pathlike + Path to a single XML file or a directory containing XML files for the + OpenMC executable to read. + Raises ------ RuntimeError @@ -275,6 +301,7 @@ def run(particles=None, threads=None, geometry_debug=False, args = _process_CLI_arguments( volume=False, geometry_debug=geometry_debug, particles=particles, restart_file=restart_file, threads=threads, tracks=tracks, - event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args) + event_based=event_based, openmc_exec=openmc_exec, mpi_args=mpi_args, + path_input=path_input) _run(args, output, cwd) diff --git a/openmc/geometry.py b/openmc/geometry.py index 3b69333625b..a43899daafd 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -103,19 +103,15 @@ def add_volume_information(self, volume_calc): if universe.id in volume_calc.volumes: universe.add_volume_information(volume_calc) - def export_to_xml(self, path='geometry.xml', remove_surfs=False): - """Export geometry to an XML file. + def to_xml_element(self, remove_surfs=False): + """Creates a 'geometry' element to be written to an XML file. Parameters ---------- - path : str - Path to file to write. Defaults to 'geometry.xml'. remove_surfs : bool Whether or not to remove redundant surfaces from the geometry when exporting - .. versionadded:: 0.12 - """ # Find and remove redundant surfaces from the geometry if remove_surfs: @@ -127,15 +123,34 @@ def export_to_xml(self, path='geometry.xml', remove_surfs=False): self.remove_redundant_surfaces() # Create XML representation - root_element = ET.Element("geometry") - self.root_universe.create_xml_subelement(root_element, memo=set()) + element = ET.Element("geometry") + self.root_universe.create_xml_subelement(element, memo=set()) # Sort the elements in the file - root_element[:] = sorted(root_element, key=lambda x: ( + element[:] = sorted(element, key=lambda x: ( x.tag, int(x.get('id')))) # Clean the indentation in the file to be user-readable - xml.clean_indentation(root_element) + xml.clean_indentation(element) + xml.reorder_attributes(element) # TODO: Remove when support is Python 3.8+ + + return element + + def export_to_xml(self, path='geometry.xml', remove_surfs=False): + """Export geometry to an XML file. + + Parameters + ---------- + path : str + Path to file to write. Defaults to 'geometry.xml'. + remove_surfs : bool + Whether or not to remove redundant surfaces from the geometry when + exporting + + .. versionadded:: 0.12 + + """ + root_element = self.to_xml_element(remove_surfs) # Check if path is a directory p = Path(path) @@ -143,18 +158,17 @@ def export_to_xml(self, path='geometry.xml', remove_surfs=False): p /= 'geometry.xml' # Write the XML Tree to the geometry.xml file - xml.reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+ tree = ET.ElementTree(root_element) tree.write(str(p), xml_declaration=True, encoding='utf-8') @classmethod - def from_xml(cls, path='geometry.xml', materials=None): - """Generate geometry from XML file + def from_xml_element(cls, elem, materials=None): + """Generate geometry from an XML element Parameters ---------- - path : str, optional - Path to geometry XML file + elem : xml.etree.ElementTree.Element + XML element materials : openmc.Materials or None Materials used to assign to cells. If None, an attempt is made to generate it from the materials.xml file. @@ -165,6 +179,11 @@ def from_xml(cls, path='geometry.xml', materials=None): Geometry object """ + mats = dict() + if materials is not None: + mats.update({str(m.id): m for m in materials}) + mats['void'] = None + # Helper function for keeping a cache of Universe instances universes = {} def get_universe(univ_id): @@ -173,13 +192,10 @@ def get_universe(univ_id): universes[univ_id] = univ return universes[univ_id] - tree = ET.parse(path) - root = tree.getroot() - # Get surfaces surfaces = {} periodic = {} - for surface in root.findall('surface'): + for surface in elem.findall('surface'): s = openmc.Surface.from_xml_element(surface) surfaces[s.id] = s @@ -193,24 +209,24 @@ def get_universe(univ_id): surfaces[s1].periodic_surface = surfaces[s2] # Add any DAGMC universes - for elem in root.findall('dagmc_universe'): - dag_univ = openmc.DAGMCUniverse.from_xml_element(elem) + for e in elem.findall('dagmc_universe'): + dag_univ = openmc.DAGMCUniverse.from_xml_element(e) universes[dag_univ.id] = dag_univ # Dictionary that maps each universe to a list of cells/lattices that - # contain it (needed to determine which universe is the root) + # contain it (needed to determine which universe is the elem) child_of = defaultdict(list) - for elem in root.findall('lattice'): - lat = openmc.RectLattice.from_xml_element(elem, get_universe) + for e in elem.findall('lattice'): + lat = openmc.RectLattice.from_xml_element(e, get_universe) universes[lat.id] = lat if lat.outer is not None: child_of[lat.outer].append(lat) for u in lat.universes.ravel(): child_of[u].append(lat) - for elem in root.findall('hex_lattice'): - lat = openmc.HexLattice.from_xml_element(elem, get_universe) + for e in elem.findall('hex_lattice'): + lat = openmc.HexLattice.from_xml_element(e, get_universe) universes[lat.id] = lat if lat.outer is not None: child_of[lat.outer].append(lat) @@ -224,15 +240,8 @@ def get_universe(univ_id): for u in ring: child_of[u].append(lat) - # Create dictionary to easily look up materials - if materials is None: - filename = Path(path).parent / 'materials.xml' - materials = openmc.Materials.from_xml(str(filename)) - mats = {str(m.id): m for m in materials} - mats['void'] = None - - for elem in root.findall('cell'): - c = openmc.Cell.from_xml_element(elem, surfaces, mats, get_universe) + for e in elem.findall('cell'): + c = openmc.Cell.from_xml_element(e, surfaces, mats, get_universe) if c.fill_type in ('universe', 'lattice'): child_of[c.fill].append(c) @@ -244,6 +253,34 @@ def get_universe(univ_id): else: raise ValueError('Error determining root universe.') + @classmethod + def from_xml(cls, path='geometry.xml', materials=None): + """Generate geometry from XML file + + Parameters + ---------- + path : str, optional + Path to geometry XML file + materials : openmc.Materials or None + Materials used to assign to cells. If None, an attempt is made to + generate it from the materials.xml file. + + Returns + ------- + openmc.Geometry + Geometry object + + """ + # Create dictionary to easily look up materials + if materials is None: + filename = Path(path).parent / 'materials.xml' + materials = openmc.Materials.from_xml(str(filename)) + + tree = ET.parse(path) + root = tree.getroot() + + return cls.from_xml_element(root, materials) + def find(self, point): """Find cells/universes/lattices which contain a given point diff --git a/openmc/material.py b/openmc/material.py index 420a0852168..aa778cab64e 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1449,6 +1449,57 @@ def make_isotropic_in_lab(self): for material in self: material.make_isotropic_in_lab() + def _write_xml(self, file, header=True, level=0, spaces_per_level=2, trailing_indent=True): + """Writes XML content of the materials to an open file handle. + + Parameters + ---------- + file : IOTextWrapper + Open file handle to write content into. + header : bool + Whether or not to write the XML header + level : int + Indentation level of materials element + spaces_per_level : int + Number of spaces per indentation + trailing_indentation : bool + Whether or not to write a trailing indentation for the materials element + + """ + indentation = level*spaces_per_level*' ' + # Write the header and the opening tag for the root element. + if header: + file.write("\n") + file.write(indentation+'\n') + + # Write the element. + if self.cross_sections is not None: + element = ET.Element('cross_sections') + element.text = str(self.cross_sections) + clean_indentation(element, level=level+1) + element.tail = element.tail.strip(' ') + file.write((level+1)*spaces_per_level*' ') + reorder_attributes(element) # TODO: Remove when support is Python 3.8+ + ET.ElementTree(element).write(file, encoding='unicode') + + # Write the elements. + for material in sorted(self, key=lambda x: x.id): + element = material.to_xml_element() + clean_indentation(element, level=level+1) + element.tail = element.tail.strip(' ') + file.write((level+1)*spaces_per_level*' ') + reorder_attributes(element) # TODO: Remove when support is Python 3.8+ + ET.ElementTree(element).write(file, encoding='unicode') + + # Write the closing tag for the root element. + file.write(indentation+'\n') + + # Write a trailing indentation for the next element + # at this level if needed + if trailing_indent: + file.write(indentation) + + def export_to_xml(self, path: PathLike = 'materials.xml'): """Export material collection to an XML file. @@ -1468,41 +1519,16 @@ def export_to_xml(self, path: PathLike = 'materials.xml'): # one go. with open(str(p), 'w', encoding='utf-8', errors='xmlcharrefreplace') as fh: - - # Write the header and the opening tag for the root element. - fh.write("\n") - fh.write('\n') - - # Write the element. - if self.cross_sections is not None: - element = ET.Element('cross_sections') - element.text = str(self.cross_sections) - clean_indentation(element, level=1) - element.tail = element.tail.strip(' ') - fh.write(' ') - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ - ET.ElementTree(element).write(fh, encoding='unicode') - - # Write the elements. - for material in sorted(self, key=lambda x: x.id): - element = material.to_xml_element() - clean_indentation(element, level=1) - element.tail = element.tail.strip(' ') - fh.write(' ') - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ - ET.ElementTree(element).write(fh, encoding='unicode') - - # Write the closing tag for the root element. - fh.write('\n') + self._write_xml(fh) @classmethod - def from_xml(cls, path: PathLike = 'materials.xml'): + def from_xml_element(cls, elem): """Generate materials collection from XML file Parameters ---------- - path : str - Path to materials XML file + elem : xml.etree.ElementTree.Element + XML element Returns ------- @@ -1510,17 +1536,34 @@ def from_xml(cls, path: PathLike = 'materials.xml'): Materials collection """ - tree = ET.parse(path) - root = tree.getroot() - # Generate each material materials = cls() - for material in root.findall('material'): + for material in elem.findall('material'): materials.append(Material.from_xml_element(material)) # Check for cross sections settings - xs = tree.find('cross_sections') + xs = elem.find('cross_sections') if xs is not None: materials.cross_sections = xs.text return materials + + @classmethod + def from_xml(cls, path: PathLike = 'materials.xml'): + """Generate materials collection from XML file + + Parameters + ---------- + path : str + Path to materials XML file + + Returns + ------- + openmc.Materials + Materials collection + + """ + tree = ET.parse(path) + root = tree.getroot() + + return cls.from_xml_element(root) \ No newline at end of file diff --git a/openmc/model/model.py b/openmc/model/model.py index 98136b2d59d..2f448908444 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -6,10 +6,12 @@ from numbers import Integral from tempfile import NamedTemporaryFile import warnings +from xml.etree import ElementTree as ET import h5py import openmc +import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value @@ -238,6 +240,35 @@ def from_xml(cls, geometry='geometry.xml', materials='materials.xml', plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None return cls(geometry, materials, settings, tallies, plots) + @classmethod + def from_model_xml(cls, path='model.xml'): + """Create model from single XML file + + .. vesionadded:: 0.13.3 + + Parameters + ---------- + path : str or Pathlike + Path to model.xml file + """ + tree = ET.parse(path) + root = tree.getroot() + + model = cls() + + meshes = {} + model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes) + model.materials = openmc.Materials.from_xml_element(root.find('materials')) + model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials) + + if root.find('tallies'): + model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes) + + if root.find('plots'): + model.plots = openmc.Plots.from_xml_element(root.find('plots')) + + return model + def init_lib(self, threads=None, geometry_debug=False, restart_file=None, tracks=False, output=True, event_based=None, intracomm=None): """Initializes the model in memory via the C API @@ -399,7 +430,7 @@ def deplete(self, timesteps, method='cecm', final_step=True, depletion_operator.finalize() def export_to_xml(self, directory='.', remove_surfs=False): - """Export model to XML files. + """Export model to separate XML files. Parameters ---------- @@ -418,6 +449,50 @@ def export_to_xml(self, directory='.', remove_surfs=False): d.mkdir(parents=True) self.settings.export_to_xml(d) + self.geometry.export_to_xml(d, remove_surfs=remove_surfs) + + # If a materials collection was specified, export it. Otherwise, look + # for all materials in the geometry and use that to automatically build + # a collection. + if self.materials: + self.materials.export_to_xml(d) + else: + materials = openmc.Materials(self.geometry.get_all_materials() + .values()) + materials.export_to_xml(d) + + if self.tallies: + self.tallies.export_to_xml(d) + if self.plots: + self.plots.export_to_xml(d) + + def export_to_model_xml(self, path='model.xml', remove_surfs=False): + """Export model to a single XML file. + + .. versionadded:: 0.13.3 + + Parameters + ---------- + path : str or Pathlike + Location of the XML file to write (default is 'model.xml'). Can be a + directory or file path. + remove_surfs : bool + Whether or not to remove redundant surfaces from the geometry when + exporting. + + """ + xml_path = Path(path) + # if the provided path doesn't end with the XML extension, assume the + # input path is meant to be a directory. If the directory does not + # exist, create it and place a 'model.xml' file there. + if not str(xml_path).endswith('.xml') and not xml_path.exists(): + os.mkdir(xml_path) + xml_path /= 'model.xml' + # if this is an XML file location and the file's parent directory does + # not exist, create it before continuing + elif not xml_path.parent.exists(): + os.mkdir(xml_path.parent) + if remove_surfs: warnings.warn("remove_surfs kwarg will be deprecated soon, please " "set the Geometry.merge_surfaces attribute instead.") @@ -425,22 +500,43 @@ def export_to_xml(self, directory='.', remove_surfs=False): # Can be used to modify tallies in case any surfaces are redundant redundant_surfaces = self.geometry.remove_redundant_surfaces() - self.geometry.export_to_xml(d) + # provide a memo to track which meshes have been written + mesh_memo = set() + settings_element = self.settings.to_xml_element(mesh_memo) + geometry_element = self.geometry.to_xml_element() + + xml.clean_indentation(geometry_element, level=1) + xml.clean_indentation(settings_element, level=1) # If a materials collection was specified, export it. Otherwise, look # for all materials in the geometry and use that to automatically build # a collection. if self.materials: - self.materials.export_to_xml(d) + materials = self.materials else: materials = openmc.Materials(self.geometry.get_all_materials() .values()) - materials.export_to_xml(d) - if self.tallies: - self.tallies.export_to_xml(d) - if self.plots: - self.plots.export_to_xml(d) + with open(xml_path, 'w', encoding='utf-8', errors='xmlcharrefreplace') as fh: + # write the XML header + fh.write("\n") + fh.write("\n") + # Write the materials collection to the open XML file first. + # This will write the XML header also + materials._write_xml(fh, False, level=1) + # Write remaining elements as a tree + ET.ElementTree(geometry_element).write(fh, encoding='unicode') + ET.ElementTree(settings_element).write(fh, encoding='unicode') + + if self.tallies: + tallies_element = self.tallies.to_xml_element(mesh_memo) + xml.clean_indentation(tallies_element, level=1, trailing_indent=self.plots) + ET.ElementTree(tallies_element).write(fh, encoding='unicode') + if self.plots: + plots_element = self.plots.to_xml_element() + xml.clean_indentation(plots_element, level=1, trailing_indent=False) + ET.ElementTree(plots_element).write(fh, encoding='unicode') + fh.write("\n") def import_properties(self, filename): """Import physical properties diff --git a/openmc/plots.py b/openmc/plots.py index 2708683b1a4..0a045162592 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -909,13 +909,13 @@ def _create_plot_subelements(self): self._plots_file.append(xml_element) - def export_to_xml(self, path='plots.xml'): - """Export plot specifications to an XML file. + def to_xml_element(self): + """Create a 'plots' element to be written to an XML file. - Parameters - ---------- - path : str - Path to file to write. Defaults to 'plots.xml'. + Returns + ------- + element : xml.etree.ElementTree.Element + XML element containing all plot elements """ # Reset xml element tree @@ -925,17 +925,50 @@ def export_to_xml(self, path='plots.xml'): # Clean the indentation in the file to be user-readable clean_indentation(self._plots_file) + reorder_attributes(self._plots_file) # TODO: Remove when support is Python 3.8+ + + return self._plots_file + def export_to_xml(self, path='plots.xml'): + """Export plot specifications to an XML file. + + Parameters + ---------- + path : str + Path to file to write. Defaults to 'plots.xml'. + + """ # Check if path is a directory p = Path(path) if p.is_dir(): p /= 'plots.xml' + self.to_xml_element() # Write the XML Tree to the plots.xml file - reorder_attributes(self._plots_file) # TODO: Remove when support is Python 3.8+ tree = ET.ElementTree(self._plots_file) tree.write(str(p), xml_declaration=True, encoding='utf-8') + @classmethod + def from_xml_element(cls, elem): + """Generate plots collection from XML file + + Parameters + ---------- + elem : xml.etree.ElementTree.Element + XML element + + Returns + ------- + openmc.Plots + Plots collection + + """ + # Generate each plot + plots = cls() + for e in elem.findall('plot'): + plots.append(Plot.from_xml_element(e)) + return plots + @classmethod def from_xml(cls, path='plots.xml'): """Generate plots collection from XML file @@ -953,9 +986,6 @@ def from_xml(cls, path='plots.xml'): """ tree = ET.parse(path) root = tree.getroot() + return cls.from_xml_element(root) + - # Generate each plot - plots = cls() - for elem in root.findall('plot'): - plots.append(Plot.from_xml_element(elem)) - return plots diff --git a/openmc/settings.py b/openmc/settings.py index 7d327ce7cf9..4e6f144feed 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1073,25 +1073,35 @@ def _create_cutoff_subelement(self, root): subelement = ET.SubElement(element, key) subelement.text = str(value) - def _create_entropy_mesh_subelement(self, root): - if self.entropy_mesh is not None: - # use default heuristic for entropy mesh if not set by user - if self.entropy_mesh.dimension is None: - if self.particles is None: - raise RuntimeError("Number of particles must be set in order to " \ - "use entropy mesh dimension heuristic") - else: - n = ceil((self.particles / 20.0)**(1.0 / 3.0)) - d = len(self.entropy_mesh.lower_left) - self.entropy_mesh.dimension = (n,)*d - - # See if a element already exists -- if not, add it - path = f"./mesh[@id='{self.entropy_mesh.id}']" - if root.find(path) is None: - root.append(self.entropy_mesh.to_xml_element()) - - subelement = ET.SubElement(root, "entropy_mesh") - subelement.text = str(self.entropy_mesh.id) + def _create_entropy_mesh_subelement(self, root, mesh_memo=None): + if self.entropy_mesh is None: + return + + # use default heuristic for entropy mesh if not set by user + if self.entropy_mesh.dimension is None: + if self.particles is None: + raise RuntimeError("Number of particles must be set in order to " \ + "use entropy mesh dimension heuristic") + else: + n = ceil((self.particles / 20.0)**(1.0 / 3.0)) + d = len(self.entropy_mesh.lower_left) + self.entropy_mesh.dimension = (n,)*d + + # add mesh ID to this element + subelement = ET.SubElement(root, "entropy_mesh") + subelement.text = str(self.entropy_mesh.id) + + # If this mesh has already been written outside the + # settings element, skip writing it again + if mesh_memo and self.entropy_mesh.id in mesh_memo: + return + + # See if a element already exists -- if not, add it + path = f"./mesh[@id='{self.entropy_mesh.id}']" + if root.find(path) is None: + root.append(self.entropy_mesh.to_xml_element()) + if mesh_memo is not None: + mesh_memo.add(self.entropy_mesh.id) def _create_trigger_subelement(self, root): if self._trigger_active is not None: @@ -1142,15 +1152,21 @@ def _create_track_subelement(self, root): element = ET.SubElement(root, "track") element.text = ' '.join(map(str, itertools.chain(*self._track))) - def _create_ufs_mesh_subelement(self, root): - if self.ufs_mesh is not None: - # See if a element already exists -- if not, add it - path = f"./mesh[@id='{self.ufs_mesh.id}']" - if root.find(path) is None: - root.append(self.ufs_mesh.to_xml_element()) + def _create_ufs_mesh_subelement(self, root, mesh_memo=None): + if self.ufs_mesh is None: + return + + subelement = ET.SubElement(root, "ufs_mesh") + subelement.text = str(self.ufs_mesh.id) - subelement = ET.SubElement(root, "ufs_mesh") - subelement.text = str(self.ufs_mesh.id) + if mesh_memo and self.ufs_mesh.id in mesh_memo: + return + + # See if a element already exists -- if not, add it + path = f"./mesh[@id='{self.ufs_mesh.id}']" + if root.find(path) is None: + root.append(self.ufs_mesh.to_xml_element()) + if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id) def _create_resonance_scattering_subelement(self, root): res = self.resonance_scattering @@ -1207,15 +1223,21 @@ def _create_write_initial_source_subelement(self, root): elem = ET.SubElement(root, "write_initial_source") elem.text = str(self._write_initial_source).lower() - def _create_weight_windows_subelement(self, root): + def _create_weight_windows_subelement(self, root, mesh_memo=None): for ww in self._weight_windows: # Add weight window information root.append(ww.to_xml_element()) + # if this mesh has already been written, + # skip writing the mesh element + if mesh_memo and ww.mesh.id in mesh_memo: + continue + # See if a element already exists -- if not, add it path = f"./mesh[@id='{ww.mesh.id}']" if root.find(path) is None: root.append(ww.mesh.to_xml_element()) + if mesh_memo is not None: mesh_memo.add(ww.mesh.id) if self._weight_windows_on is not None: elem = ET.SubElement(root, "weight_windows_on") @@ -1396,13 +1418,15 @@ def _cutoff_from_xml_element(self, root): if value is not None: self.cutoff[key] = float(value) - def _entropy_mesh_from_xml_element(self, root): + def _entropy_mesh_from_xml_element(self, root, meshes=None): text = get_text(root, 'entropy_mesh') if text is not None: path = f"./mesh[@id='{int(text)}']" elem = root.find(path) if elem is not None: self.entropy_mesh = RegularMesh.from_xml_element(elem) + if meshes is not None and self.entropy_mesh is not None: + meshes[self.entropy_mesh.id] = self.entropy_mesh def _trigger_from_xml_element(self, root): elem = root.find('trigger') @@ -1462,13 +1486,15 @@ def _track_from_xml_element(self, root): values = [int(x) for x in text.split()] self.track = list(zip(values[::3], values[1::3], values[2::3])) - def _ufs_mesh_from_xml_element(self, root): + def _ufs_mesh_from_xml_element(self, root, meshes=None): text = get_text(root, 'ufs_mesh') if text is not None: path = f"./mesh[@id='{int(text)}']" elem = root.find(path) if elem is not None: self.ufs_mesh = RegularMesh.from_xml_element(elem) + if meshes is not None and self.ufs_mesh is not None: + meshes[self.ufs_mesh.id] = self.ufs_mesh def _resonance_scattering_from_xml_element(self, root): elem = root.find('resonance_scattering') @@ -1520,7 +1546,7 @@ def _write_initial_source_from_xml_element(self, root): if text is not None: self.write_initial_source = text in ('true', '1') - def _weight_windows_from_xml_element(self, root): + def _weight_windows_from_xml_element(self, root, meshes=None): for elem in root.findall('weight_windows'): ww = WeightWindows.from_xml_element(elem, root) self.weight_windows.append(ww) @@ -1529,6 +1555,9 @@ def _weight_windows_from_xml_element(self, root): if text is not None: self.weight_windows_on = text in ('true', '1') + if meshes is not None and self.weight_windows: + meshes.update({ww.mesh.id: ww.mesh for ww in self.weight_windows}) + def _max_splits_from_xml_element(self, root): text = get_text(root, 'max_splits') if text is not None: @@ -1539,6 +1568,68 @@ def _max_tracks_from_xml_element(self, root): if text is not None: self.max_tracks = int(text) + def to_xml_element(self, mesh_memo=None): + """Create a 'settings' element to be written to an XML file. + + Parameters + ---------- + mesh_memo : set of ints + A set of mesh IDs to keep track of whether a mesh has already been written. + """ + # Reset xml element tree + element = ET.Element("settings") + + self._create_run_mode_subelement(element) + self._create_particles_subelement(element) + self._create_batches_subelement(element) + self._create_inactive_subelement(element) + self._create_max_lost_particles_subelement(element) + self._create_rel_max_lost_particles_subelement(element) + self._create_generations_per_batch_subelement(element) + self._create_keff_trigger_subelement(element) + self._create_source_subelement(element) + self._create_output_subelement(element) + self._create_statepoint_subelement(element) + self._create_sourcepoint_subelement(element) + self._create_surf_source_read_subelement(element) + self._create_surf_source_write_subelement(element) + self._create_confidence_intervals(element) + self._create_electron_treatment_subelement(element) + self._create_energy_mode_subelement(element) + self._create_max_order_subelement(element) + self._create_photon_transport_subelement(element) + self._create_ptables_subelement(element) + self._create_seed_subelement(element) + self._create_survival_biasing_subelement(element) + self._create_cutoff_subelement(element) + self._create_entropy_mesh_subelement(element, mesh_memo) + self._create_trigger_subelement(element) + self._create_no_reduce_subelement(element) + self._create_verbosity_subelement(element) + self._create_tabular_legendre_subelements(element) + self._create_temperature_subelements(element) + self._create_trace_subelement(element) + self._create_track_subelement(element) + self._create_ufs_mesh_subelement(element, mesh_memo) + self._create_resonance_scattering_subelement(element) + self._create_volume_calcs_subelement(element) + self._create_create_fission_neutrons_subelement(element) + self._create_delayed_photon_scaling_subelement(element) + self._create_event_based_subelement(element) + self._create_max_particles_in_flight_subelement(element) + self._create_material_cell_offsets_subelement(element) + self._create_log_grid_bins_subelement(element) + self._create_write_initial_source_subelement(element) + self._create_weight_windows_subelement(element, mesh_memo) + self._create_max_splits_subelement(element) + self._create_max_tracks_subelement(element) + + # Clean the indentation in the file to be user-readable + clean_indentation(element) + reorder_attributes(element) # TODO: Remove when support is Python 3.8+ + + return element + def export_to_xml(self, path: PathLike = 'settings.xml'): """Export simulation settings to an XML file. @@ -1548,57 +1639,7 @@ def export_to_xml(self, path: PathLike = 'settings.xml'): Path to file to write. Defaults to 'settings.xml'. """ - - # Reset xml element tree - root_element = ET.Element("settings") - - self._create_run_mode_subelement(root_element) - self._create_particles_subelement(root_element) - self._create_batches_subelement(root_element) - self._create_inactive_subelement(root_element) - self._create_max_lost_particles_subelement(root_element) - self._create_rel_max_lost_particles_subelement(root_element) - self._create_generations_per_batch_subelement(root_element) - self._create_keff_trigger_subelement(root_element) - self._create_source_subelement(root_element) - self._create_output_subelement(root_element) - self._create_statepoint_subelement(root_element) - self._create_sourcepoint_subelement(root_element) - self._create_surf_source_read_subelement(root_element) - self._create_surf_source_write_subelement(root_element) - self._create_confidence_intervals(root_element) - self._create_electron_treatment_subelement(root_element) - self._create_energy_mode_subelement(root_element) - self._create_max_order_subelement(root_element) - self._create_photon_transport_subelement(root_element) - self._create_ptables_subelement(root_element) - self._create_seed_subelement(root_element) - self._create_survival_biasing_subelement(root_element) - self._create_cutoff_subelement(root_element) - self._create_entropy_mesh_subelement(root_element) - self._create_trigger_subelement(root_element) - self._create_no_reduce_subelement(root_element) - self._create_verbosity_subelement(root_element) - self._create_tabular_legendre_subelements(root_element) - self._create_temperature_subelements(root_element) - self._create_trace_subelement(root_element) - self._create_track_subelement(root_element) - self._create_ufs_mesh_subelement(root_element) - self._create_resonance_scattering_subelement(root_element) - self._create_volume_calcs_subelement(root_element) - self._create_create_fission_neutrons_subelement(root_element) - self._create_delayed_photon_scaling_subelement(root_element) - self._create_event_based_subelement(root_element) - self._create_max_particles_in_flight_subelement(root_element) - self._create_material_cell_offsets_subelement(root_element) - self._create_log_grid_bins_subelement(root_element) - self._create_write_initial_source_subelement(root_element) - self._create_weight_windows_subelement(root_element) - self._create_max_splits_subelement(root_element) - self._create_max_tracks_subelement(root_element) - - # Clean the indentation in the file to be user-readable - clean_indentation(root_element) + root_element = self.to_xml_element() # Check if path is a directory p = Path(path) @@ -1606,10 +1647,78 @@ def export_to_xml(self, path: PathLike = 'settings.xml'): p /= 'settings.xml' # Write the XML Tree to the settings.xml file - reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+ tree = ET.ElementTree(root_element) tree.write(str(p), xml_declaration=True, encoding='utf-8') + @classmethod + def from_xml_element(cls, elem, meshes=None): + """Generate settings from XML element + + Parameters + ---------- + elem : xml.etree.ElementTree.Element + XML element + meshes : dict or None + A dictionary with mesh IDs as keys and mesh instances as values that + have already been read from XML. Pre-existing meshes are used + and new meshes are added to when creating tally objects. + + Returns + ------- + openmc.Settings + Settings object + + """ + settings = cls() + settings._eigenvalue_from_xml_element(elem) + settings._run_mode_from_xml_element(elem) + settings._particles_from_xml_element(elem) + settings._batches_from_xml_element(elem) + settings._inactive_from_xml_element(elem) + settings._max_lost_particles_from_xml_element(elem) + settings._rel_max_lost_particles_from_xml_element(elem) + settings._generations_per_batch_from_xml_element(elem) + settings._keff_trigger_from_xml_element(elem) + settings._source_from_xml_element(elem) + settings._volume_calcs_from_xml_element(elem) + settings._output_from_xml_element(elem) + settings._statepoint_from_xml_element(elem) + settings._sourcepoint_from_xml_element(elem) + settings._surf_source_read_from_xml_element(elem) + settings._surf_source_write_from_xml_element(elem) + settings._confidence_intervals_from_xml_element(elem) + settings._electron_treatment_from_xml_element(elem) + settings._energy_mode_from_xml_element(elem) + settings._max_order_from_xml_element(elem) + settings._photon_transport_from_xml_element(elem) + settings._ptables_from_xml_element(elem) + settings._seed_from_xml_element(elem) + settings._survival_biasing_from_xml_element(elem) + settings._cutoff_from_xml_element(elem) + settings._entropy_mesh_from_xml_element(elem, meshes) + settings._trigger_from_xml_element(elem) + settings._no_reduce_from_xml_element(elem) + settings._verbosity_from_xml_element(elem) + settings._tabular_legendre_from_xml_element(elem) + settings._temperature_from_xml_element(elem) + settings._trace_from_xml_element(elem) + settings._track_from_xml_element(elem) + settings._ufs_mesh_from_xml_element(elem, meshes) + settings._resonance_scattering_from_xml_element(elem) + settings._create_fission_neutrons_from_xml_element(elem) + settings._delayed_photon_scaling_from_xml_element(elem) + settings._event_based_from_xml_element(elem) + settings._max_particles_in_flight_from_xml_element(elem) + settings._material_cell_offsets_from_xml_element(elem) + settings._log_grid_bins_from_xml_element(elem) + settings._write_initial_source_from_xml_element(elem) + settings._weight_windows_from_xml_element(elem, meshes) + settings._max_splits_from_xml_element(elem) + settings._max_tracks_from_xml_element(elem) + + # TODO: Get volume calculations + return settings + @classmethod def from_xml(cls, path: PathLike = 'settings.xml'): """Generate settings from XML file @@ -1629,54 +1738,4 @@ def from_xml(cls, path: PathLike = 'settings.xml'): """ tree = ET.parse(path) root = tree.getroot() - - settings = cls() - settings._eigenvalue_from_xml_element(root) - settings._run_mode_from_xml_element(root) - settings._particles_from_xml_element(root) - settings._batches_from_xml_element(root) - settings._inactive_from_xml_element(root) - settings._max_lost_particles_from_xml_element(root) - settings._rel_max_lost_particles_from_xml_element(root) - settings._generations_per_batch_from_xml_element(root) - settings._keff_trigger_from_xml_element(root) - settings._source_from_xml_element(root) - settings._volume_calcs_from_xml_element(root) - settings._output_from_xml_element(root) - settings._statepoint_from_xml_element(root) - settings._sourcepoint_from_xml_element(root) - settings._surf_source_read_from_xml_element(root) - settings._surf_source_write_from_xml_element(root) - settings._confidence_intervals_from_xml_element(root) - settings._electron_treatment_from_xml_element(root) - settings._energy_mode_from_xml_element(root) - settings._max_order_from_xml_element(root) - settings._photon_transport_from_xml_element(root) - settings._ptables_from_xml_element(root) - settings._seed_from_xml_element(root) - settings._survival_biasing_from_xml_element(root) - settings._cutoff_from_xml_element(root) - settings._entropy_mesh_from_xml_element(root) - settings._trigger_from_xml_element(root) - settings._no_reduce_from_xml_element(root) - settings._verbosity_from_xml_element(root) - settings._tabular_legendre_from_xml_element(root) - settings._temperature_from_xml_element(root) - settings._trace_from_xml_element(root) - settings._track_from_xml_element(root) - settings._ufs_mesh_from_xml_element(root) - settings._resonance_scattering_from_xml_element(root) - settings._create_fission_neutrons_from_xml_element(root) - settings._delayed_photon_scaling_from_xml_element(root) - settings._event_based_from_xml_element(root) - settings._max_particles_in_flight_from_xml_element(root) - settings._material_cell_offsets_from_xml_element(root) - settings._log_grid_bins_from_xml_element(root) - settings._write_initial_source_from_xml_element(root) - settings._weight_windows_from_xml_element(root) - settings._max_splits_from_xml_element(root) - settings._max_tracks_from_xml_element(root) - - # TODO: Get volume calculations - - return settings + return cls.from_xml_element(root) diff --git a/openmc/tallies.py b/openmc/tallies.py index d0355f14ee9..b22cc4cf0ad 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -3119,17 +3119,17 @@ def _create_tally_subelements(self, root_element): for tally in self: root_element.append(tally.to_xml_element()) - def _create_mesh_subelements(self, root_element): - already_written = set() + def _create_mesh_subelements(self, root_element, memo=None): + already_written = memo if memo else set() for tally in self: for f in tally.filters: if isinstance(f, openmc.MeshFilter): - if f.mesh.id not in already_written: - if len(f.mesh.name) > 0: - root_element.append(ET.Comment(f.mesh.name)) - - root_element.append(f.mesh.to_xml_element()) - already_written.add(f.mesh.id) + if f.mesh.id in already_written: + continue + if len(f.mesh.name) > 0: + root_element.append(ET.Comment(f.mesh.name)) + root_element.append(f.mesh.to_xml_element()) + already_written.add(f.mesh.id) def _create_filter_subelements(self, root_element): already_written = dict() @@ -3155,6 +3155,22 @@ def _create_derivative_subelements(self, root_element): for d in derivs: root_element.append(d.to_xml_element()) + def to_xml_element(self, memo=None): + """Creates a 'tallies' element to be written to an XML file. + """ + element = ET.Element("tallies") + self._create_mesh_subelements(element, memo) + self._create_filter_subelements(element) + self._create_tally_subelements(element) + self._create_derivative_subelements(element) + + # Clean the indentation in the file to be user-readable + clean_indentation(element) + reorder_attributes(element) # TODO: Remove when support is Python 3.8+ + + return element + + def export_to_xml(self, path='tallies.xml'): """Create a tallies.xml file that can be used for a simulation. @@ -3164,15 +3180,7 @@ def export_to_xml(self, path='tallies.xml'): Path to file to write. Defaults to 'tallies.xml'. """ - - root_element = ET.Element("tallies") - self._create_mesh_subelements(root_element) - self._create_filter_subelements(root_element) - self._create_tally_subelements(root_element) - self._create_derivative_subelements(root_element) - - # Clean the indentation in the file to be user-readable - clean_indentation(root_element) + root_element = self.to_xml_element() # Check if path is a directory p = Path(path) @@ -3180,18 +3188,21 @@ def export_to_xml(self, path='tallies.xml'): p /= 'tallies.xml' # Write the XML Tree to the tallies.xml file - reorder_attributes(root_element) # TODO: Remove when support is Python 3.8+ tree = ET.ElementTree(root_element) tree.write(str(p), xml_declaration=True, encoding='utf-8') @classmethod - def from_xml(cls, path='tallies.xml'): - """Generate tallies from XML file + def from_xml_element(cls, elem, meshes=None): + """Generate tallies from an XML element Parameters ---------- - path : str, optional - Path to tallies XML file + elem : xml.etree.ElementTree.Element + XML element + meshes : dict or None + A dictionary with mesh IDs as keys and mesh instances as values that + have already been read from XML. Pre-existing meshes are used + and new meshes are added to when creating tally objects. Returns ------- @@ -3199,33 +3210,49 @@ def from_xml(cls, path='tallies.xml'): Tallies object """ - tree = ET.parse(path) - root = tree.getroot() - # Read mesh elements - meshes = {} - for elem in root.findall('mesh'): - mesh = MeshBase.from_xml_element(elem) + meshes = {} if meshes is None else meshes + for e in elem.findall('mesh'): + mesh = MeshBase.from_xml_element(e) meshes[mesh.id] = mesh # Read filter elements filters = {} - for elem in root.findall('filter'): - filter = openmc.Filter.from_xml_element(elem, meshes=meshes) + for e in elem.findall('filter'): + filter = openmc.Filter.from_xml_element(e, meshes=meshes) filters[filter.id] = filter # Read derivative elements derivatives = {} - for elem in root.findall('derivative'): - deriv = openmc.TallyDerivative.from_xml_element(elem) + for e in elem.findall('derivative'): + deriv = openmc.TallyDerivative.from_xml_element(e) derivatives[deriv.id] = deriv # Read tally elements tallies = [] - for elem in root.findall('tally'): + for e in elem.findall('tally'): tally = openmc.Tally.from_xml_element( - elem, filters=filters, derivatives=derivatives + e, filters=filters, derivatives=derivatives ) tallies.append(tally) return cls(tallies) + + @classmethod + def from_xml(cls, path='tallies.xml'): + """Generate tallies from XML file + + Parameters + ---------- + path : str, optional + Path to tallies XML file + + Returns + ------- + openmc.Tallies + Tallies object + + """ + tree = ET.parse(path) + root = tree.getroot() + return cls.from_xml_element(root) diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index afb0a1f36cb..2094fd551f3 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -91,8 +91,7 @@ Library::Library(pugi::xml_node node, const std::string& directory) // Non-member functions //============================================================================== -void read_cross_sections_xml() -{ +void read_cross_sections_xml() { pugi::xml_document doc; std::string filename = settings::path_input + "materials.xml"; // Check if materials.xml exists @@ -104,6 +103,11 @@ void read_cross_sections_xml() auto root = doc.document_element(); + read_cross_sections_xml(root); +} + +void read_cross_sections_xml(pugi::xml_node root) +{ // Find cross_sections.xml file -- the first place to look is the // materials.xml file. If no file is found there, then we check the // OPENMC_CROSS_SECTIONS environment variable diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 26147198402..83e8494de6a 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -40,8 +40,7 @@ void update_universe_cell_count(int32_t a, int32_t b) } } -void read_geometry_xml() -{ +void read_geometry_xml() { // Display output message write_message("Reading geometry XML file...", 5); @@ -61,6 +60,11 @@ void read_geometry_xml() // Get root element pugi::xml_node root = doc.document_element(); + read_geometry_xml(root); +} + +void read_geometry_xml(pugi::xml_node root) +{ // Read surfaces, cells, lattice read_surfaces(root); read_cells(root); diff --git a/src/initialize.cpp b/src/initialize.cpp index aa353aa9cbb..0c137795bb9 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -14,6 +14,7 @@ #include "openmc/constants.h" #include "openmc/cross_sections.h" #include "openmc/error.h" +#include "openmc/file_utils.h" #include "openmc/geometry_aux.h" #include "openmc/hdf5_interface.h" #include "openmc/material.h" @@ -105,7 +106,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm) openmc::openmc_set_seed(DEFAULT_SEED); // Read XML input files - read_input_xml(); + if (!read_model_xml()) read_separate_xml_files(); + + // Write some initial output under the header if needed + initial_output(); // Check for particle restart run if (settings::particle_restart_run) @@ -177,10 +181,8 @@ int parse_command_line(int argc, char* argv[]) } else if (arg == "-e" || arg == "--event") { settings::event_based = true; - } else if (arg == "-r" || arg == "--restart") { i += 1; - // Check what type of file this is hid_t file_id = file_open(argv[i], 'r', true); std::string filetype; @@ -280,7 +282,15 @@ int parse_command_line(int argc, char* argv[]) if (argc > 1 && last_flag < argc - 1) { settings::path_input = std::string(argv[last_flag + 1]); - // Add slash at end of directory if it isn't there + // check that the path is either a valid directory or file + if (!dir_exists(settings::path_input) && + !file_exists(settings::path_input)) { + fatal_error(fmt::format( + "The path specified to the OpenMC executable '{}' does not exist.", + settings::path_input)); + } + + // Add slash at end of directory if it isn't the if (!ends_with(settings::path_input, "/")) { settings::path_input += "/"; } @@ -289,7 +299,101 @@ int parse_command_line(int argc, char* argv[]) return 0; } -void read_input_xml() +bool read_model_xml() { + std::string model_filename = + settings::path_input.empty() ? "." : settings::path_input; + + // some string cleanup + // a trailing "/" is applied to path_input if it's specified, + // remove it for the first attempt at reading the input file + if (ends_with(model_filename, "/")) + model_filename.pop_back(); + + // if the current filename is a directory, append the default model filename + if (dir_exists(model_filename)) + model_filename += "/model.xml"; + + // if this file doesn't exist, stop here + if (!file_exists(model_filename)) return false; + + // try to process the path input as an XML file + pugi::xml_document doc; + if (!doc.load_file(model_filename.c_str())) { + fatal_error(fmt::format( + "Error reading from single XML input file '{}'", model_filename)); + } + + pugi::xml_node root = doc.document_element(); + + // Read settings + if (!check_for_node(root, "settings")) { + fatal_error("No node present in the model.xml file."); + } + auto settings_root = root.child("settings"); + + // Verbosity + if (check_for_node(settings_root, "verbosity")) { + settings::verbosity = std::stoi(get_node_value(settings_root, "verbosity")); + } + + // To this point, we haven't displayed any output since we didn't know what + // the verbosity is. Now that we checked for it, show the title if necessary + if (mpi::master) { + if (settings::verbosity >= 2) + title(); + } + + write_message(fmt::format("Reading model XML file '{}' ...", model_filename), 5); + + read_settings_xml(settings_root); + + // If other XML files are present, display warning + // that they will be ignored + auto other_inputs = {"materials.xml", "geometry.xml", "settings.xml", "tallies.xml", "plots.xml"}; + for (const auto& input : other_inputs) { + if (file_exists(settings::path_input + input)) { + warning((fmt::format("Other XML file input(s) are present. These files " + "will be ignored in favor of the {} file.", + model_filename))); + break; + } + } + + // Read materials and cross sections + if (!check_for_node(root, "materials")) { + fatal_error(fmt::format( + "No node present in the {} file.", model_filename)); + } + + read_cross_sections_xml(root.child("materials")); + read_materials_xml(root.child("materials")); + + // Read geometry + if (!check_for_node(root, "geometry")) { + fatal_error(fmt::format( + "No node present in the {} file.", model_filename)); + } + read_geometry_xml(root.child("geometry")); + + // Final geometry setup and assign temperatures + finalize_geometry(); + + // Finalize cross sections having assigned temperatures + finalize_cross_sections(); + + if (check_for_node(root, "tallies")) + read_tallies_xml(root.child("tallies")); + + // Initialize distribcell_filters + prepare_distribcell(); + + if (check_for_node(root, "plots")) + read_plots_xml(root.child("plots")); + + return true; +} + +void read_separate_xml_files() { read_settings_xml(); read_cross_sections_xml(); @@ -310,6 +414,10 @@ void read_input_xml() // Read the plots.xml regardless of plot mode in case plots are requested // via the API read_plots_xml(); +} + +void initial_output() { + // write initial output if (settings::run_mode == RunMode::PLOTTING) { // Read plots.xml if it exists if (mpi::master && settings::verbosity >= 5) diff --git a/src/material.cpp b/src/material.cpp index 30dfa5ed506..f2e23aede2f 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1264,8 +1264,7 @@ double density_effect(const vector& f, const vector& e_b_sq, return delta - w_sq * (1.0 - beta_sq); } -void read_materials_xml() -{ +void read_materials_xml() { write_message("Reading materials XML file...", 5); pugi::xml_document doc; @@ -1281,6 +1280,12 @@ void read_materials_xml() // Loop over XML material elements and populate the array. pugi::xml_node root = doc.document_element(); + + read_materials_xml(root); +} + +void read_materials_xml(pugi::xml_node root) +{ for (pugi::xml_node material_node : root.children("material")) { model::materials.push_back(make_unique(material_node)); } diff --git a/src/plot.cpp b/src/plot.cpp index b012ed1311b..1a62fe26319 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -124,8 +124,7 @@ extern "C" int openmc_plot_geometry() return 0; } -void read_plots_xml() -{ +void read_plots_xml() { // Check if plots.xml exists; this is only necessary when the plot runmode is // initiated. Otherwise, we want to read plots.xml because it may be called // later via the API. In that case, its ok for a plots.xml to not exist @@ -141,6 +140,12 @@ void read_plots_xml() doc.load_file(filename.c_str()); pugi::xml_node root = doc.document_element(); + + read_plots_xml(root); +} + +void read_plots_xml(pugi::xml_node root) +{ for (auto node : root.children("plot")) { model::plots.emplace_back(node); model::plot_map[model::plots.back().id_] = model::plots.size() - 1; diff --git a/src/settings.cpp b/src/settings.cpp index eb0d4936c76..74d45543d81 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -211,20 +211,19 @@ void get_run_parameters(pugi::xml_node node_base) } } -void read_settings_xml() -{ +void read_settings_xml() { using namespace settings; using namespace pugi; - // Check if settings.xml exists - std::string filename = path_input + "settings.xml"; + std::string filename = settings::path_input + "settings.xml"; if (!file_exists(filename)) { if (run_mode != RunMode::PLOTTING) { fatal_error( fmt::format("Settings XML file '{}' does not exist! In order " "to run OpenMC, you first need a set of input files; at a " "minimum, this " - "includes settings.xml, geometry.xml, and materials.xml. " + "includes settings.xml, geometry.xml, and materials.xml " + "or a single XML file containing all of these files. " "Please consult " "the user's guide at https://docs.openmc.org for further " "information.", @@ -256,8 +255,17 @@ void read_settings_xml() if (verbosity >= 2) title(); } + write_message("Reading settings XML file...", 5); + read_settings_xml(root); +} + +void read_settings_xml(pugi::xml_node root) +{ + using namespace settings; + using namespace pugi; + // Find if a multi-group or continuous-energy simulation is desired if (check_for_node(root, "energy_mode")) { std::string temp_str = get_node_value(root, "energy_mode", true, true); diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 51194684ae7..1c1f274d79d 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -705,8 +705,7 @@ std::string Tally::nuclide_name(int nuclide_idx) const // Non-member functions //============================================================================== -void read_tallies_xml() -{ +void read_tallies_xml() { // Check if tallies.xml exists. If not, just return since it is optional std::string filename = settings::path_input + "tallies.xml"; if (!file_exists(filename)) @@ -719,6 +718,11 @@ void read_tallies_xml() doc.load_file(filename.c_str()); pugi::xml_node root = doc.document_element(); + read_tallies_xml(root); +} + +void read_tallies_xml(pugi::xml_node root) +{ // Check for setting if (check_for_node(root, "assume_separate")) { settings::assume_separate = get_node_value_bool(root, "assume_separate"); diff --git a/tests/regression_tests/model_xml/__init__.py b/tests/regression_tests/model_xml/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat b/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat new file mode 100644 index 00000000000..1c4516f42dd --- /dev/null +++ b/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 10000 + 10 + 5 + + + -4.0 -4.0 -4.0 4.0 4.0 4.0 + + + + diff --git a/tests/regression_tests/model_xml/energy_laws_inputs_true.dat b/tests/regression_tests/model_xml/energy_laws_inputs_true.dat new file mode 100644 index 00000000000..c89ccc97ee0 --- /dev/null +++ b/tests/regression_tests/model_xml/energy_laws_inputs_true.dat @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + diff --git a/tests/regression_tests/model_xml/inputs_true.dat b/tests/regression_tests/model_xml/inputs_true.dat new file mode 100644 index 00000000000..c408026316c --- /dev/null +++ b/tests/regression_tests/model_xml/inputs_true.dat @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + fixed source + 10000 + 1 + + + 0 0 0 + + + + 14000000.0 1.0 + + + ttb + true + + 1000.0 + + + + + 16 + + + neutron photon electron positron + + + 1 2 + current + + + 2 + Al27 total + total (n,gamma) + tracklength + + + 2 + Al27 total + total heating (n,gamma) + collision + + + 2 + Al27 total + total heating (n,gamma) + analog + + + diff --git a/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat b/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat new file mode 100644 index 00000000000..0eed3c2015a --- /dev/null +++ b/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + 1.2 1.2 + 1 + 2 2 + -1.2 -1.2 + +2 1 +1 1 + + + 2.4 2.4 + 2 2 + -2.4 -2.4 + +4 4 +4 4 + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat new file mode 100644 index 00000000000..0b2b434681a --- /dev/null +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + fixed source + 10000 + 1 + + + 0 0 0 + + + + 14000000.0 1.0 + + + ttb + true + + 1000.0 + + + + + 1 + + + neutron photon electron positron + + + 1 2 + current + + + 2 + Al27 total + total (n,gamma) + tracklength + + + 2 + Al27 total + total heating (n,gamma) + collision + + + 2 + Al27 total + total heating (n,gamma) + analog + + + diff --git a/tests/regression_tests/model_xml/test.py b/tests/regression_tests/model_xml/test.py new file mode 100644 index 00000000000..c67a72ed372 --- /dev/null +++ b/tests/regression_tests/model_xml/test.py @@ -0,0 +1,100 @@ +from difflib import unified_diff +import glob +import filecmp +import os +from pathlib import Path + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness, colorize + +# use a few models from other tests to make sure the same results are +# produced when using a single model.xml file as input +from ..adj_cell_rotation.test import model as adj_cell_rotation_model +from ..lattice_multiple.test import model as lattice_multiple_model +from ..energy_laws.test import model as energy_laws_model +from ..photon_production.test import model as photon_production_model + + +class ModelXMLTestHarness(PyAPITestHarness): + """Accept a results file to check against and assume inputs_true is the contents of a model.xml file. + """ + def __init__(self, model=None, inputs_true=None, results_true=None): + statepoint_name = f'statepoint.{model.settings.batches}.h5' + super().__init__(statepoint_name, model, inputs_true) + + self.results_true = 'results_true.dat' if results_true is None else results_true + + def _build_inputs(self): + self._model.export_to_model_xml() + + def _get_inputs(self): + return open('model.xml').read() + + def _compare_results(self): + """Make sure the current results agree with the reference.""" + compare = filecmp.cmp('results_test.dat', self.results_true) + if not compare: + expected = open(self.results_true).readlines() + actual = open('results_test.dat').readlines() + diff = unified_diff(expected, actual, self.results_true, + 'results_test.dat') + print('Result differences:') + print(''.join(colorize(diff))) + os.rename('results_test.dat', 'results_error.dat') + assert compare, 'Results do not agree' + + def _cleanup(self): + super()._cleanup() + if os.path.exists('model.xml'): + os.remove('model.xml') + + +test_names = [ + 'adj_cell_rotation', + 'lattice_multiple', + 'energy_laws', + 'photon_production' +] + + +@pytest.mark.parametrize("test_name", test_names, ids=lambda test: test) +def test_model_xml(test_name, request): + openmc.reset_auto_ids() + + test_path = '../' + test_name + results = test_path + "/results_true.dat" + inputs = test_name + "_inputs_true.dat" + model_name = test_name + "_model" + harness = ModelXMLTestHarness(request.getfixturevalue(model_name), inputs, results) + harness.main() + +def test_input_arg(run_in_tmpdir): + + pincell = openmc.examples.pwr_pin_cell() + + pincell.settings.particles = 100 + + # export to separate XML files and run + pincell.export_to_xml() + openmc.run() + + # make sure the executable isn't falling back on the separate XMLs + for f in glob.glob('*.xml'): + os.remove(f) + # now export to a single XML file with a custom name + pincell.export_to_model_xml('pincell.xml') + assert Path('pincell.xml').exists() + + # run by specifying that single file + openmc.run(path_input='pincell.xml') + + # check that this works for plotting too + openmc.plot_geometry(path_input='pincell.xml') + + # now ensure we get an error for an incorrect filename, + # even in the presence of other, valid XML files + pincell.export_to_model_xml() + with pytest.raises(RuntimeError, match='ex-em-ell.xml'): + openmc.run(path_input='ex-em-ell.xml') \ No newline at end of file diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 9b05ed2a5c9..6c483aeb94b 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1,5 +1,6 @@ from math import pi from pathlib import Path +import os import numpy as np import pytest @@ -529,3 +530,40 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert openmc.lib.materials[3].volume == mats[2].volume test_model.finalize_lib() + +def test_model_xml(run_in_tmpdir): + + # load a model from examples + pwr_model = openmc.examples.pwr_core() + + # export to separate XMLs manually + pwr_model.settings.export_to_xml('settings_ref.xml') + pwr_model.materials.export_to_xml('materials_ref.xml') + pwr_model.geometry.export_to_xml('geometry_ref.xml') + + # now write and read a model.xml file + pwr_model.export_to_model_xml() + new_model = openmc.Model.from_model_xml() + + # make sure we can also export this again to separate + # XML files + new_model.export_to_xml() + +def test_single_xml_exec(run_in_tmpdir): + + pincell_model = openmc.examples.pwr_pin_cell() + + pincell_model.export_to_model_xml('pwr_pincell.xml') + + openmc.run(path_input='pwr_pincell.xml') + + with pytest.raises(RuntimeError, match='ex-em-ell.xml'): + openmc.run(path_input='ex-em-ell.xml') + + # test that a file in a different directory can be used + os.mkdir('inputs') + pincell_model.export_to_model_xml('./inputs/pincell.xml') + openmc.run(path_input='./inputs/pincell.xml') + + with pytest.raises(RuntimeError, match='input_dir'): + openmc.run(path_input='input_dir/pincell.xml') \ No newline at end of file