@@ -50,7 +50,8 @@ def get_microxs_and_flux(
5050 chain_file : PathLike | Chain | None = None ,
5151 path_statepoint : PathLike | None = None ,
5252 path_input : PathLike | None = None ,
53- run_kwargs = None
53+ run_kwargs = None ,
54+ reaction_rate_opts : dict | None = None ,
5455) -> tuple [list [np .ndarray ], list [MicroXS ]]:
5556 """Generate microscopic cross sections and fluxes for multiple domains.
5657
@@ -80,10 +81,12 @@ def get_microxs_and_flux(
8081 Energy group boundaries in [eV] or the name of the group structure.
8182 If left as None energies will default to [0.0, 100e6]
8283 reaction_rate_mode : {"direct", "flux"}, optional
83- Indicate how reaction rates should be calculated. The "direct" method
84- tallies reaction rates directly. The "flux" method tallies a multigroup
85- flux spectrum and then collapses multigroup reaction rates after a
86- transport solve (with an option to tally some reaction rates directly).
84+ The "direct" method tallies reaction rates directly (per energy
85+ group). The "flux" method tallies a multigroup flux spectrum and then
86+ collapses reaction rates after a transport solve. When
87+ `reaction_rate_opts` is provided with `reaction_rate_mode='flux'`, the
88+ specified nuclide/reaction pairs are tallied directly and those values
89+ override the flux-collapsed values.
8790 chain_file : PathLike or Chain, optional
8891 Path to the depletion chain XML file or an instance of
8992 openmc.deplete.Chain. Used to determine cross sections for materials not
@@ -99,6 +102,10 @@ def get_microxs_and_flux(
99102 not kept.
100103 run_kwargs : dict, optional
101104 Keyword arguments passed to :meth:`openmc.Model.run`
105+ reaction_rate_opts : dict, optional
106+ When `reaction_rate_mode="flux"`, allows selecting a subset of
107+ nuclide/reaction pairs to be computed via direct reaction-rate tallies
108+ (per energy group). Supported keys: "nuclides", "reactions".
102109
103110 Returns
104111 -------
@@ -153,12 +160,36 @@ def get_microxs_and_flux(
153160 flux_tally .scores = ['flux' ]
154161 model .tallies = [flux_tally ]
155162
163+ # Prepare reaction-rate tally for 'direct' or subset for 'flux' with opts
164+ rr_tally = None
165+ rr_nuclides : list [str ] = []
166+ rr_reactions : list [str ] = []
156167 if reaction_rate_mode == 'direct' :
168+ rr_nuclides = list (nuclides )
169+ rr_reactions = list (reactions )
170+ elif reaction_rate_mode == 'flux' and reaction_rate_opts :
171+ opts = reaction_rate_opts or {}
172+ rr_nuclides = list (opts .get ('nuclides' , []))
173+ rr_reactions = list (opts .get ('reactions' , []))
174+ # Keep only requested pairs within overall sets
175+ if rr_nuclides :
176+ rr_nuclides = [n for n in rr_nuclides if n in set (nuclides )]
177+ if rr_reactions :
178+ rr_reactions = [r for r in rr_reactions if r in set (reactions )]
179+
180+ # Only construct tally if both lists are non-empty
181+ if rr_nuclides and rr_reactions :
157182 rr_tally = openmc .Tally (name = 'MicroXS RR' )
158- rr_tally .filters = [domain_filter , energy_filter ]
159- rr_tally .nuclides = nuclides
183+ # Use 1-group energy filter for RR in flux mode
184+ if reaction_rate_mode == 'flux' :
185+ rr_energy_filter = openmc .EnergyFilter (
186+ [energy_filter .values [0 ], energy_filter .values [- 1 ]])
187+ else :
188+ rr_energy_filter = energy_filter
189+ rr_tally .filters = [domain_filter , rr_energy_filter ]
190+ rr_tally .nuclides = rr_nuclides
160191 rr_tally .multiply_density = False
161- rr_tally .scores = reactions
192+ rr_tally .scores = rr_reactions
162193 model .tallies .append (rr_tally )
163194
164195 if openmc .lib .is_initialized :
@@ -196,7 +227,7 @@ def get_microxs_and_flux(
196227
197228 # Read in tally results (on all ranks)
198229 with StatePoint (statepoint_path ) as sp :
199- if reaction_rate_mode == 'direct' :
230+ if rr_tally is not None :
200231 rr_tally = sp .tallies [rr_tally .id ]
201232 rr_tally ._read_results ()
202233 flux_tally = sp .tallies [flux_tally .id ]
@@ -209,31 +240,46 @@ def get_microxs_and_flux(
209240 # Create list where each item corresponds to one domain
210241 fluxes = list (flux .squeeze ((1 , 2 )))
211242
212- if reaction_rate_mode == 'direct' :
243+ # If we built a reaction-rate tally, compute microscopic cross sections
244+ if rr_tally is not None :
213245 # Get reaction rates
214246 reaction_rates = rr_tally .get_reshaped_data () # (domains, groups, nuclides, reactions)
215247
216248 # Make energy groups last dimension
217249 reaction_rates = np .moveaxis (reaction_rates , 1 , - 1 ) # (domains, nuclides, reactions, groups)
218250
251+ # If RR is 1-group, sum flux over groups
252+ if reaction_rate_mode == "flux" :
253+ flux = flux .sum (axis = - 1 , keepdims = True ) # (domains, 1, 1, 1)
254+
219255 # Divide RR by flux to get microscopic cross sections. The indexing
220256 # ensures that only non-zero flux values are used, and broadcasting is
221257 # applied to align the shapes of reaction_rates and flux for division.
222- xs = np .empty_like (reaction_rates ) # (domains, nuclides, reactions, groups)
258+ xs = np .zeros_like (reaction_rates ) # (domains, nuclides, reactions, groups)
223259 d , _ , _ , g = np .nonzero (flux )
224260 xs [d , ..., g ] = reaction_rates [d , ..., g ] / flux [d , :, :, g ]
225261
226262 # Create lists where each item corresponds to one domain
227- micros = [MicroXS (xs_i , nuclides , reactions ) for xs_i in xs ]
228- else :
229- micros = [MicroXS .from_multigroup_flux (
263+ direct_micros = [MicroXS (xs_i , rr_nuclides , rr_reactions ) for xs_i in xs ]
264+
265+ # If using flux mode, compute flux-collapsed microscopic XS
266+ if reaction_rate_mode == 'flux' :
267+ flux_micros = [MicroXS .from_multigroup_flux (
230268 energies = energies ,
231269 multigroup_flux = flux_i ,
232270 chain_file = chain_file ,
233271 nuclides = nuclides ,
234272 reactions = reactions
235273 ) for flux_i in fluxes ]
236274
275+ # Decide which micros to use and merge if needed
276+ if reaction_rate_mode == 'flux' and rr_tally is not None :
277+ micros = [m1 .merge (m2 ) for m1 , m2 in zip (flux_micros , direct_micros )]
278+ elif rr_tally is not None :
279+ micros = direct_micros
280+ else :
281+ micros = flux_micros
282+
237283 # Reset tallies
238284 model .tallies = original_tallies
239285
@@ -484,6 +530,79 @@ def from_hdf5(cls, group_or_filename: h5py.Group | PathLike) -> Self:
484530
485531 return cls (data , nuclides , reactions )
486532
533+ def merge (self , other : Self , prefer : str = 'other' ) -> Self :
534+ """Merge two MicroXS objects by taking the union of nuclides/reactions.
535+
536+ If the two objects contain overlapping nuclide/reaction entries, values
537+ from `other` will overwrite values from `self` when `prefer='other'`.
538+ When `prefer='self'`, values from `self` are retained for overlapping
539+ entries, and values from `other` are used only for non-overlapping
540+ entries.
541+
542+ Parameters
543+ ----------
544+ other : MicroXS
545+ Other MicroXS instance to merge with this one.
546+ prefer : {"other", "self"}
547+ Which instance's data should take precedence on overlap.
548+
549+ Returns
550+ -------
551+ MicroXS
552+ New instance containing the merged data.
553+ """
554+ check_value ('prefer' , prefer , {'other' , 'self' })
555+
556+ # Require same number of energy groups
557+ if self .data .shape [2 ] != other .data .shape [2 ]:
558+ raise ValueError (
559+ 'Cannot merge MicroXS with different number of energy groups: '
560+ f"{ self .data .shape [2 ]} vs { other .data .shape [2 ]} . Ensure that "
561+ 'both were generated with consistent group structures and '
562+ 'treatments (e.g., both multigroup or both collapsed).'
563+ )
564+
565+ # Build unified axes preserving order (self first, then other's new)
566+ new_nuclides = list (self .nuclides )
567+ for nuc in other .nuclides :
568+ if nuc not in self ._index_nuc :
569+ new_nuclides .append (nuc )
570+ new_reactions = list (self .reactions )
571+ for rx in other .reactions :
572+ if rx not in self ._index_rx :
573+ new_reactions .append (rx )
574+
575+ # Allocate and fill from self (self's nuclides/reactions map to the
576+ # first indices of new_nuclides/new_reactions by construction)
577+ groups = self .data .shape [2 ]
578+ data = np .zeros ((len (new_nuclides ), len (new_reactions ), groups ))
579+ idx_n = {nuc : i for i , nuc in enumerate (new_nuclides )}
580+ idx_r = {rx : i for i , rx in enumerate (new_reactions )}
581+
582+ n_self = len (self .nuclides )
583+ r_self = len (self .reactions )
584+ data [:n_self , :r_self ] = self .data
585+
586+ # Build destination index arrays for other's nuclides/reactions
587+ dst_n = np .array ([idx_n [nuc ] for nuc in other .nuclides ])
588+ dst_r = np .array ([idx_r [rx ] for rx in other .reactions ])
589+
590+ # Copy from other, respecting precedence
591+ if prefer == 'other' :
592+ data [np .ix_ (dst_n , dst_r )] = other .data
593+ else :
594+ # Copy only entries where nuc or rx is absent from self
595+ nuc_is_new = np .array (
596+ [nuc not in self ._index_nuc for nuc in other .nuclides ])
597+ rx_is_new = np .array (
598+ [rx not in self ._index_rx for rx in other .reactions ])
599+ mask = nuc_is_new [:, np .newaxis ] | rx_is_new [np .newaxis , :]
600+ src_i , src_j = np .where (mask )
601+ if src_i .size :
602+ data [dst_n [src_i ], dst_r [src_j ]] = other .data [src_i , src_j ]
603+
604+ return MicroXS (data , new_nuclides , new_reactions )
605+
487606
488607def write_microxs_hdf5 (
489608 micros : Sequence [MicroXS ],
0 commit comments