Module PHITS_tools
This module contains a variety of tools used for parsing PHITS output files.
Specifically, it seeks to be a (nearly) universal PHITS output parser, supporting output from
all tallies, both normal "standard" output as well as dump file outputs (in ASCII and binary formats).
It is also capable of automatically parsing all such PHITS output files in a directory or listed in
active tallies inside of a PHITS input file.
If a DCHAIN input file (output from the [T-Dchain] tally) or DCHAIN output *.act
file is provided
to the main tally output processing function, an attempt will be made to import the DCHAIN Tools module
and process the found DCHAIN output files too.
The functions contained in this module and brief descriptions of their functions are included below. However, provided first is a description of the three different ways one can use and interface with this module.
How to use the PHITS_tools.py module
There are three main ways one can use this Python module:
- As an imported Python module
- In your own Python scripts, you can import this module as
from PHITS_tools import *
and call its main functions, which are listed in the next section below, or any of its other functions documented here.
- In your own Python scripts, you can import this module as
- As a command line interface (CLI)
- This module can be ran on the command line with the individual PHITS output file to be parsed (or a directory
or PHITS input/phits.out file containing multiple files to be parsed) as the required argument.
Execute
python PHITS_tools.py --help
to see all of the different options that can be used with this module to parse standard or dump PHITS output files (individually and directories containing them) via the CLI. This functionality can be used to automatically process all PHITS output whenever PHITS is ran; see the "Automatic processing at PHITS runtime" section further below. How the CLI options explicitly translate to the functions documented here is also covered further below.
- This module can be ran on the command line with the individual PHITS output file to be parsed (or a directory
or PHITS input/phits.out file containing multiple files to be parsed) as the required argument.
Execute
- As a graphical user interface (GUI)
- When the module is executed without any additional arguments,
python PHITS_tools.py
, (or with the--GUI
or-g
flag in the CLI) a GUI will be launched to step you through selecting what "mode" you would like to run PHITS Tools in (STANDARD
,DUMP
,DIRECTORY
, orINPUT_FILE
), selecting a file to be parsed (or a directory containing multiple files to be parsed), and the various options for each mode.
- When the module is executed without any additional arguments,
The CLI and GUI options result in the parsed file's contents being saved to a pickle file, which can be reopened and used later in a Python script. When using the main functions below within a Python script which has imported the PHITS_tools module, you can optionally choose not to save the pickle files (if desired) and only have the tally output/dump parsing functions return the data objects they produce for your own further analyses.
Main PHITS Output Parsing Functions
parse_tally_output_file()
: general parser for standard output files for all PHITS talliesparse_tally_dump_file()
: parser for dump files from "dump" flag in PHITS [T-Cross], [T-Time], and [T-Track] talliesparse_all_tally_output_in_dir()
: runparse_tally_output_file()
over all standard output files in a directory (and, optionally,parse_tally_dump_file()
over all dump files too)parse_phitsout_file()
: creates a metadata dictionary of a PHITS run from its "phits.out" file
General Purpose Functions
tally()
: tally/histogram values (and their indices) falling within a desired binning structure (useful with "dump" files)rebinner()
: rebin a set of y-data to a new x-binning structure (edges need not necessarily be preserved)autoplot_tally_results()
: make plot(s), saved as PDFs, of tally results from tally output Pandas DataFrame(s)fetch_MC_material()
: returns a string of a formatted material for PHITS or MCNP (mostly those in PNNL-15870 Rev. 1)ICRP116_effective_dose_coeff()
: returns effective dose conversion coefficient of a mono-energetic particle of some species and some geometry; does coefficients are those in ICRP 116merge_dump_file_pickles()
: merge multiple dump file outputs into a single file (useful for dumps in MPI runs)ZZZAAAM_to_nuclide_plain_str()
: returns a nuclide plaintext string for a given "ZZZAAAM" number (1000Z+10A+M)nuclide_plain_str_to_latex_str()
: convert a plaintext string for a nuclide to a LaTeX formatted raw stringElement_Z_to_Sym()
: return an elemental symbol string given its proton number ZElement_Sym_to_Z()
: returns an atomic number Z provided the elemental symbolkfcode_to_common_name()
: converts a particle kf-code to a plaintext stringis_number()
: returns Boolean denoting whether provided string is that of a numberfind()
: return index of the first instance of a value in a list
Subfunctions for PHITS output parsing
(These are meant as dependencies more so than for standalone usage.)
determine_PHITS_output_file_type()
: determine if a file is standard tally output or ASCII/binary dump filesearch_for_dump_parameters()
: attempt to auto find "dump" parameters via possible standard tally output filesplit_into_header_and_content()
: initial reading of PHITS tally output, dividing it into header and "content" sectionsparse_tally_header()
: extract metadata from tally output header sectionparse_tally_content()
: extract tally results/values from tally content sectionextract_data_from_header_line()
: extract metadata key/value pairs from tally output header linessplit_str_of_equalities()
: split a string containing equalities (e.g.,reg = 100
) into a list of themparse_group_string()
: split a string containing "groups" (e.g., regions) into a list of theminitialize_tally_array()
: initialize NumPy array for storing tally resultsdata_row_to_num_list()
: extract numeric values from a line in the tally content sectioncalculate_tally_absolute_errors()
: calculate absolute uncertainties from read values and relative errorsbuild_tally_Pandas_dataframe()
: make Pandas dataframe from the main results NumPy array and the metadataextract_tally_outputs_from_phits_input()
: extract a dictionary of files produced by a PHITS run
CLI options
Essentially, the CLI serves to interface with the core three functions of PHITS Tools: parse_tally_output_file()
,
parse_tally_dump_file()
, and parse_all_tally_output_in_dir()
.
The required file
argument is checked to see if it is a directory or a file, and, if the latter, whether the -d
option is used denoting a dump output file, otherwise defaulting to assuming it is a PHITS standard tally output file;
then file
and the relevant settings are sent to the corresponding main function.
Explicitly, inclusion of the various CLI options have the following effects on the main functions' arguments and settings:
- Affecting all functions
file
is passed totally_output_filepath
,path_to_dump_file
, ortally_output_dirpath
-skip
setsprefer_reading_existing_pickle = True
(False
if excluded)
parse_tally_output_file()
(and passed to it viaparse_all_tally_output_in_dir()
)-np
setsmake_PandasDF = False
(True
if excluded)-na
setscalculate_absolute_errors = False
(True
if excluded)-lzma
setscompress_pickle_with_lzma = True
(False
if excluded)-po
setsinclude_phitsout_in_metadata = True
(False
if excluded)-p
setsautoplot_tally_output = True
(False
if excluded)
parse_tally_dump_file()
(and passed to it viaparse_all_tally_output_in_dir()
)-d
tells the CLI thatfile
should be processed as a dump file (if it's not a directory)-dvals
passes the provided sequence of values todump_data_sequence
(None
if excluded)-dbin
specifies that the file is binary (thatdump_data_number=len(dump_data_sequence)
and is positive)-dnmax
passes its value tomax_entries_read
(None
if excluded)-ddir
setsreturn_directional_info = True
(False
if excluded)-ddeg
setsuse_degrees = True
(False
if excluded)-dnsl
setssave_namedtuple_list = False
(True
if excluded)-dnsp
setssave_Pandas_dataframe = False
(True
if excluded)-dmaxGB
passes its value tosplit_binary_dumps_over_X_GB
(20
GB if excluded)-dsplit
passes its value tomerge_split_dump_handling
(0
if excluded)
parse_all_tally_output_in_dir()
exclusively-r
setsinclude_subdirectories = True
(False
if excluded)-fpre
passes its value tooutput_file_prefix
(''
if excluded)-fsuf
passes its value tooutput_file_suffix
('.out'
if excluded)-fstr
passes its value tooutput_file_required_string
(''
if excluded)-d
setsinclude_dump_files = True
(False
if excluded)-dnmmpi
setsdump_merge_MPI_subdumps = False
(True
if excluded)-dndmpi
setsdump_delete_MPI_subdumps_post_merge = False
(True
if excluded)-m
setsmerge_tally_outputs = True
(False
if excluded)-smo
setsmerge_tally_outputs = True
(False
if excluded),save_output_pickle = False
(True
if excluded), andsave_pickle_of_merged_tally_outputs = True
(None
if excluded)-pa
setsautoplot_all_tally_output_in_dir = True
(False
if excluded)
Below is a picture of all of these options available for use within the CLI and their descriptions.
Automatic processing at PHITS runtime
PHITS Tools can be used to automatically process the output of every PHITS run executed with the "phits.bat" and "phits.sh"
scripts found in the "phits/bin/" directory of your PHITS installation.
To do this, first you must identify the location
of your "PHITS_tools.py" file.
If using the file directly downloaded from GitHub, this should be in a location of your choosing.
If you installed PHITS Tools via pip install PHITS-Tools
, you can find its location with pip show PHITS-Tools -f
.
Once you have identified the location of PHITS_tools.py, for example "/path/locating/PHITS_Tools/PHITS_tools.py", you can
add the following line to your PHITS execution script:
On Windows, using "phits/bin/phits.bat":
- Scroll down toward the bottom of the script, to the section with the line
rem - Your file processing starts here.
- After the if statement (right before the
rem - Your file processing ends here
line), insert a new line with the following command: python "C:\path\locating\PHITS_Tools\PHITS_tools.py" "%%~nxF" -po -m -d -ddir -ddeg -lzma -p -pa
On Linux/Mac, using "phits/bin/phits.sh":
- Scroll down toward the bottom of the script, to the section titled
# Run PHITS
- On the line after the end of the if statement
fi
, add the following command: python "/path/locating/PHITS_Tools/PHITS_tools.py" $1 -po -m -d -ddir -ddeg -lzma -p -pa
(Of course, if necessary, replace "python
" with however you typically call python in your environment, e.g. py
, python3
, etc.)
Adding this line causes the following to happen:
- After PHITS finishes running normally, the PHITS input file is passed to PHITS Tools.
- Since it is a PHITS input file, the CLI will have
parse_all_tally_output_in_dir()
handle it, in [INPUT_FILE mode] - The input file and its produced "phits.out"-type file are scanned for output files from active tallies, including those inputted via the PHITS insert file function
infl:{}
too (usingextract_tally_outputs_from_phits_input()
andparse_phitsout_file()
).- This will include any dump files (
-d
) if present. - When the "phits.out" file (
file(6)
in the PHITS input [Parameters]) is parsed, its metadata—including the PHITS input echo—will be saved to a .pickle file, compressed with LZMA (-lzma
) and with the extra ".xz" extension.
- This will include any dump files (
- Then, the standard tally outputs are processed.
For each standard tally output:
- The
parse_tally_output_file()
function processes the tally output, producing atally_output
dictionary with keys for the produced NumPy array, Pandas DataFrame, and metadata dictionary. - The
tally_output['tally_metadata']
dictionary will have the phits.out metadata dictionary added to it (-po
) under the'phitsout'
key. - This
tally_output
dictionary object is saved to a .pickle file, compressed with LZMA (-lzma
) and with the extra ".xz" extension. - A plot, saved in PDF and PNG formats, of the tally's output is generated (
-p
) byautoplot_tally_results()
. - These files will share the same name as the tally output file, just with different extensions.
- The
- Then, any encountered tally dump files are processed (
-d
). For each tally dump output file:- The
parse_tally_dump_file()
function processes the dump file, automatically extracting the dump parameters from its parent tally's output file. - Provided direction vector information (u,v,w) is present, extra directional information is calculated (
-ddir
), using degrees for the unit of calculated angles (-ddeg
) - The produced named tuple list and Pandas DataFrame are saved as two separate LZMA-compressed pickle files.
- The
- Then, a merged (
-m
) dictionary object containing all of thetally_output
dictionaries for each standard tally output processed is produced.- The dictionary is keyed with the
file
parameter of each tally in the PHITS input, with the values being the correspondingtally_output
dictionaries. - This merged dictionary object is saved to a pickle file sharing the same name as the PHITS input file but ending in "_ALL_TALLY_OUTPUTS.pickle", compressed with LZMA (
-lzma
) and with the extra ".xz" extension.
- The dictionary is keyed with the
- Then, a PDF containing plots from all standard tally outputs (
-pa
) is generated withautoplot_tally_results()
.- This PDF of plots is saved to a file sharing the same name as the PHITS input file but ending in "_ALL_TALLY_OUTPUTS_PLOTTED.pdf"
You can edit the flags provided to the CLI for your desired default behavior.
For instance, to only save the pickle file of
the merged output (rather than for every tally output too), replace -m
with -smo
. And to not bother with creating a merged output
and only save the outputs for each individual tally, just omit -m
.
Given that the plotting is easily the slowest part of
the execution of PHITS Tools in most cases, it may be desirable to omit the -p
and/or -pa
flags to not save plots of the tally
outputs individually or all together in a single PDF, respectively.
Since dump files can be very large and sometimes solely
created for reusage by PHITS (e.g., with a s-type=17
[Source] section), it may also be desirable to exclude dump files from
automatic processing by omitting -d
.
As an example, a more "minimal" automatic processing would result from:
- Windows:
python "C:\path\locating\PHITS_Tools\PHITS_tools.py" "%%~nxF" -po -smo -lzma -pa
- Linux/Mac:
python "/path/locating/PHITS_Tools/PHITS_tools.py" $1 -po -smo -lzma -pa
This would only create the ".pickle.xz" file of the merged standard tally outputs and the PDF containing all of their plots together, skipping any processing of dump files.
Expand source code
'''
This module contains a variety of tools used for parsing PHITS output files.
Specifically, it seeks to be a (nearly) universal PHITS output parser, supporting output from
all tallies, both normal "standard" output as well as dump file outputs (in ASCII and binary formats).
It is also capable of automatically parsing all such PHITS output files in a directory or listed in
active tallies inside of a PHITS input file.
If a DCHAIN input file (output from the [T-Dchain] tally) or DCHAIN output `*.act` file is provided
to the main tally output processing function, an attempt will be made to import the [DCHAIN Tools module](https://github.com/Lindt8/DCHAIN-Tools)
and process the found DCHAIN output files too.
The functions contained in this module and brief descriptions of their functions are included below.
However, provided first is a description of the three different ways one can use and interface with this module.
### **How to use the PHITS_tools.py module**
There are three main ways one can use this Python module:
1. As an **imported Python module**
- In your own Python scripts, you can import this module as `from PHITS_tools import *` and call its main functions,
which are listed in the next section below, or any of its other functions documented here.
2. As a **command line interface (CLI)**
- This module can be ran on the command line with the individual PHITS output file to be parsed (or a directory
or PHITS input/phits.out file containing multiple files to be parsed) as the required argument.
Execute `python PHITS_tools.py --help` to see all of the different options that can be used with this module
to parse standard or dump PHITS output files (individually and directories containing them) via the CLI.
This functionality can be used to automatically process all PHITS output whenever PHITS is ran;
see the **"Automatic processing at PHITS runtime"** section further below.
How the CLI options explicitly translate to the functions documented here is also covered further below.
3. As a **graphical user interface (GUI)**
- When the module is executed without any additional arguments, `python PHITS_tools.py`, (or with the `--GUI` or `-g` flag in the CLI)
a GUI will be launched to step you through selecting what "mode" you would like to run PHITS Tools in (`STANDARD`, `DUMP`, `DIRECTORY`, or `INPUT_FILE`),
selecting a file to be parsed (or a directory containing multiple files to be parsed), and the various options for each mode.
The CLI and GUI options result in the parsed file's contents being saved to a pickle file, which can be reopened
and used later in a Python script. When using the main functions below within a Python script which has imported the PHITS_tools
module, you can optionally choose not to save the pickle files (if desired) and only have the tally output/dump parsing
functions return the data objects they produce for your own further analyses.
### **Main PHITS Output Parsing Functions**
- `parse_tally_output_file` : general parser for standard output files for all PHITS tallies
- `parse_tally_dump_file` : parser for dump files from "dump" flag in PHITS [T-Cross], [T-Time], and [T-Track] tallies
- `parse_all_tally_output_in_dir` : run `parse_tally_output_file()` over all standard output files in a directory (and, optionally, `parse_tally_dump_file()` over all dump files too)
- `parse_phitsout_file` : creates a metadata dictionary of a PHITS run from its "phits.out" file
### General Purpose Functions
- `tally` : tally/histogram values (and their indices) falling within a desired binning structure (useful with "dump" files)
- `rebinner` : rebin a set of y-data to a new x-binning structure (edges need not necessarily be preserved)
- `autoplot_tally_results` : make plot(s), saved as PDFs, of tally results from tally output Pandas DataFrame(s)
- `fetch_MC_material` : returns a string of a formatted material for PHITS or MCNP (mostly those in [PNNL-15870 Rev. 1](https://www.osti.gov/biblio/1023125))
- `ICRP116_effective_dose_coeff` : returns effective dose conversion coefficient of a mono-energetic particle of some species and some geometry; does coefficients are those in [ICRP 116](https://doi.org/10.1016/j.icrp.2011.10.001)
- `merge_dump_file_pickles` : merge multiple dump file outputs into a single file (useful for dumps in MPI runs)
- `ZZZAAAM_to_nuclide_plain_str` : returns a nuclide plaintext string for a given "ZZZAAAM" number (1000Z+10A+M)
- `nuclide_plain_str_to_latex_str` : convert a plaintext string for a nuclide to a LaTeX formatted raw string
- `Element_Z_to_Sym` : return an elemental symbol string given its proton number Z
- `Element_Sym_to_Z` : returns an atomic number Z provided the elemental symbol
- `kfcode_to_common_name` : converts a particle kf-code to a plaintext string
- `is_number` : returns Boolean denoting whether provided string is that of a number
- `find` : return index of the first instance of a value in a list
### Subfunctions for PHITS output parsing
(These are meant as dependencies more so than for standalone usage.)
- `determine_PHITS_output_file_type` : determine if a file is standard tally output or ASCII/binary dump file
- `search_for_dump_parameters` : attempt to auto find "dump" parameters via possible standard tally output file
- `split_into_header_and_content` : initial reading of PHITS tally output, dividing it into header and "content" sections
- `parse_tally_header` : extract metadata from tally output header section
- `parse_tally_content` : extract tally results/values from tally content section
- `extract_data_from_header_line` : extract metadata key/value pairs from tally output header lines
- `split_str_of_equalities` : split a string containing equalities (e.g., `reg = 100`) into a list of them
- `parse_group_string` : split a string containing "groups" (e.g., regions) into a list of them
- `initialize_tally_array` : initialize NumPy array for storing tally results
- `data_row_to_num_list` : extract numeric values from a line in the tally content section
- `calculate_tally_absolute_errors` : calculate absolute uncertainties from read values and relative errors
- `build_tally_Pandas_dataframe` : make Pandas dataframe from the main results NumPy array and the metadata
- `extract_tally_outputs_from_phits_input` : extract a dictionary of files produced by a PHITS run
### **CLI options**
Essentially, the CLI serves to interface with the core three functions of PHITS Tools: `parse_tally_output_file`,
`parse_tally_dump_file`, and `parse_all_tally_output_in_dir`.
The required `file` argument is checked to see if it is a directory or a file, and, if the latter, whether the `-d`
option is used denoting a dump output file, otherwise defaulting to assuming it is a PHITS standard tally output file;
then `file` and the relevant settings are sent to the corresponding main function.
Explicitly, inclusion of the various CLI options have the following effects on the main functions' arguments and settings:
- Affecting all functions
- `file` is passed to `tally_output_filepath`, `path_to_dump_file`, or `tally_output_dirpath`
- `-skip` sets `prefer_reading_existing_pickle = True` (`False` if excluded)
- `parse_tally_output_file` (and passed to it via `parse_all_tally_output_in_dir`)
- `-np` sets `make_PandasDF = False` (`True` if excluded)
- `-na` sets `calculate_absolute_errors = False` (`True` if excluded)
- `-lzma` sets `compress_pickle_with_lzma = True` (`False` if excluded)
- `-po` sets `include_phitsout_in_metadata = True` (`False` if excluded)
- `-p` sets `autoplot_tally_output = True` (`False` if excluded)
- `parse_tally_dump_file` (and passed to it via `parse_all_tally_output_in_dir`)
- `-d` tells the CLI that `file` should be processed as a dump file (if it's not a directory)
- `-dvals` passes the provided sequence of values to `dump_data_sequence` (`None` if excluded)
- `-dbin` specifies that the file is binary (that `dump_data_number=len(dump_data_sequence)` and *is positive*)
- `-dnmax` passes its value to `max_entries_read` (`None` if excluded)
- `-ddir` sets `return_directional_info = True` (`False` if excluded)
- `-ddeg` sets `use_degrees = True` (`False` if excluded)
- `-dnsl` sets `save_namedtuple_list = False` (`True` if excluded)
- `-dnsp` sets `save_Pandas_dataframe = False` (`True` if excluded)
- `-dmaxGB` passes its value to `split_binary_dumps_over_X_GB` (`20` GB if excluded)
- `-dsplit` passes its value to `merge_split_dump_handling` (`0` if excluded)
- `parse_all_tally_output_in_dir` exclusively
- `-r` sets `include_subdirectories = True` (`False` if excluded)
- `-fpre` passes its value to `output_file_prefix` (`''` if excluded)
- `-fsuf` passes its value to `output_file_suffix` (`'.out'` if excluded)
- `-fstr` passes its value to `output_file_required_string` (`''` if excluded)
- `-d` sets `include_dump_files = True` (`False` if excluded)
- `-dnmmpi` sets `dump_merge_MPI_subdumps = False` (`True` if excluded)
- `-dndmpi` sets `dump_delete_MPI_subdumps_post_merge = False` (`True` if excluded)
- `-m` sets `merge_tally_outputs = True` (`False` if excluded)
- `-smo` sets `merge_tally_outputs = True` (`False` if excluded), `save_output_pickle = False` (`True` if excluded), and `save_pickle_of_merged_tally_outputs = True` (`None` if excluded)
- `-pa` sets `autoplot_all_tally_output_in_dir = True` (`False` if excluded)
Below is a picture of all of these options available for use within the CLI and their descriptions.

### **Automatic processing at PHITS runtime**
PHITS Tools can be used to automatically process the output of every PHITS run executed with the "phits.bat" and "phits.sh"
scripts found in the "phits/bin/" directory of your PHITS installation. To do this, first you must identify the location
of your "PHITS_tools.py" file. If using the file directly downloaded from GitHub, this should be in a location of your choosing.
If you installed PHITS Tools via `pip install PHITS-Tools`, you can find its location with `pip show PHITS-Tools -f`.
Once you have identified the location of PHITS_tools.py, for example "/path/locating/PHITS_Tools/PHITS_tools.py", you can
add the following line to your PHITS execution script:
On Windows, using "phits/bin/phits.bat":
- Scroll down toward the bottom of the script, to the section with the line `rem - Your file processing starts here.`
- After the if statement (right before the `rem - Your file processing ends here` line), insert a new line with the following command:
- `python "C:\path\locating\PHITS_Tools\PHITS_tools.py" "%%~nxF" -po -m -d -ddir -ddeg -lzma -p -pa`
On Linux/Mac, using "phits/bin/phits.sh":
- Scroll down toward the bottom of the script, to the section titled `# Run PHITS`
- On the line after the end of the if statement `fi`, add the following command:
- `python "/path/locating/PHITS_Tools/PHITS_tools.py" $1 -po -m -d -ddir -ddeg -lzma -p -pa`
(Of course, if necessary, replace "`python`" with however you typically call python in your environment, e.g. `py`, `python3`, etc.)
Adding this line causes the following to happen:
- After PHITS finishes running normally, the PHITS input file is passed to PHITS Tools.
- Since it is a PHITS input file, the CLI will have `parse_all_tally_output_in_dir()` handle it, in *[INPUT_FILE mode]*
- The input file and its produced "phits.out"-type file are scanned for output files from active tallies, including those inputted via the PHITS insert file function `infl:{}` too (using `extract_tally_outputs_from_phits_input()` and `parse_phitsout_file()`).
- This will include any dump files (**`-d`**) if present.
- When the "phits.out" file (`file(6)` in the PHITS input [Parameters]) is parsed, its metadata—including the PHITS input echo—will be saved to a .pickle file, compressed with LZMA (**`-lzma`**) and with the extra ".xz" extension.
- Then, the standard tally outputs are processed. For each standard tally output:
- The `parse_tally_output_file()` function processes the tally output, producing a `tally_output` dictionary with keys for the produced NumPy array, Pandas DataFrame, and metadata dictionary.
- The `tally_output['tally_metadata']` dictionary will have the phits.out metadata dictionary added to it (**`-po`**) under the `'phitsout'` key.
- This `tally_output` dictionary object is saved to a .pickle file, compressed with LZMA (**`-lzma`**) and with the extra ".xz" extension.
- A plot, saved in PDF and PNG formats, of the tally's output is generated (**`-p`**) by `autoplot_tally_results()`.
- These files will share the same name as the tally output file, just with different extensions.
- Then, any encountered tally dump files are processed (**`-d`**). For each tally dump output file:
- The `parse_tally_dump_file()` function processes the dump file, automatically extracting the dump parameters from its parent tally's output file.
- Provided direction vector information (u,v,w) is present, extra directional information is calculated (**`-ddir`**), using degrees for the unit of calculated angles (**`-ddeg`**)
- The produced named tuple list and Pandas DataFrame are saved as two separate LZMA-compressed pickle files.
- Then, a merged (**`-m`**) dictionary object containing all of the `tally_output` dictionaries for each standard tally output processed is produced.
- The dictionary is keyed with the `file` parameter of each tally in the PHITS input, with the values being the corresponding `tally_output` dictionaries.
- This merged dictionary object is saved to a pickle file sharing the same name as the PHITS input file but ending in "_ALL_TALLY_OUTPUTS.pickle", compressed with LZMA (**`-lzma`**) and with the extra ".xz" extension.
- Then, a PDF containing plots from all standard tally outputs (**`-pa`**) is generated with `autoplot_tally_results()`.
- This PDF of plots is saved to a file sharing the same name as the PHITS input file but ending in "_ALL_TALLY_OUTPUTS_PLOTTED.pdf"
You can edit the flags provided to the CLI for your desired default behavior. For instance, to only save the pickle file of
the merged output (rather than for every tally output too), replace `-m` with `-smo`. And to not bother with creating a merged output
and only save the outputs for each individual tally, just omit `-m`. Given that the plotting is easily the slowest part of
the execution of PHITS Tools in most cases, it may be desirable to omit the `-p` and/or `-pa` flags to not save plots of the tally
outputs individually or all together in a single PDF, respectively. Since dump files can be very large and sometimes solely
created for reusage by PHITS (e.g., with a `s-type=17` [Source] section), it may also be desirable to exclude dump files from
automatic processing by omitting `-d`. As an example, a more "minimal" automatic processing would result from:
- Windows: `python "C:\path\locating\PHITS_Tools\PHITS_tools.py" "%%~nxF" -po -smo -lzma -pa`
- Linux/Mac: `python "/path/locating/PHITS_Tools/PHITS_tools.py" $1 -po -smo -lzma -pa`
This would only create the ".pickle.xz" file of the merged standard tally outputs and the PDF containing all of their
plots together, skipping any processing of dump files.
'''
'''
Each function beings with a comment block containing the following sections:
Description:
Dependencies:
Inputs:
Outputs:
("Dependencies:" is omitted when there are none.)
'''
import sys
import os
import numpy as np
from pathlib import Path
# default program settings
launch_GUI = False
run_with_CLI_inputs = False
in_debug_mode = False # toggles printing of debug messages throughout the code
#in_debug_mode = True # toggles printing of debug messages throughout the code
test_explicit_files_dirs = False # used for testing specific files at the bottom of this file
#test_explicit_files_dirs = True
if __name__ == "__main__":
#in_debug_mode = True
if test_explicit_files_dirs:
in_debug_mode = True
pass
elif len(sys.argv) == 1:
launch_GUI = True
else:
if '-g' in sys.argv or '--GUI' in sys.argv:
launch_GUI = True
else:
run_with_CLI_inputs = True
# CLI for PHITS Tools
import argparse
if in_debug_mode:
import pprint
import time
# Timer start
start = time.time()
# use Path, get extension, check for existence of filename_err.extension
def parse_tally_output_file(tally_output_filepath, make_PandasDF = True, calculate_absolute_errors = True,
save_output_pickle=True, include_phitsout_in_metadata=False,
prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False,
autoplot_tally_output=False):
'''
Description:
Parse any PHITS tally output file, returning tally metadata and an array of its values (and optionally
this data inside of a Pandas dataframe too). Note the separate `parse_tally_dump_file` function for
parsing PHITS dump files. If a DCHAIN input file (output from the [T-Dchain] tally) or DCHAIN output
`*.act` file is provided, an attempt will be made to import the [DCHAIN Tools module](https://github.com/Lindt8/DCHAIN-Tools) and process the found
DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file.
Dependencies:
- `import numpy as np`
- `import pandas as pd` (if `make_PandasDF = True`)
- `import seaborn as sns` (if `autoplot_tally_output = True`)
- `from munch import Munch` (will still run if package not found)
Inputs:
(required)
- **`tally_output_filepath`** = string or Path object denoting the path to the tally output file to be parsed
Inputs:
(optional)
- **`make_PandasDF`** = (D=`True`) A Boolean denoting whether a Pandas dataframe of the tally data array will be made
- **`calculate_absolute_errors`** = (D=`True`) A Boolean determining whether the absolute uncertainty of each tally output value
is to be calculated (simply as the product of the value and relative error); if `False`, the final
dimension of `tally_data`, `ierr`, will be of length-2 rather than length-3
- **`save_output_pickle`** = (D=`True`) A Boolean determining whether the `tally_output` dictionary object is saved as a pickle file;
if `True`, the file will be saved with the same path and name as the provided PHITS tally output file
but with the .pickle extension.
- **`include_phitsout_in_metadata`** = (D=`False`) A Boolean determining whether the "phits.out" file
(`file(6)` in the [Parameters] section of a PHITS input file) in the same directory as `tally_output_filepath`,
if found, should be processed via `parse_phitsout_file()` and have its informational dictionary
about the PHITS run added to the `'tally_metadata'` dictionary under the key `'phitsout'`.
If `True`, this function assumes `file(6) = phits.out` (the default setting) in the PHITS input.
- If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided
dictionary will be taken as that to be added as the `'phitsout'` key in the `'tally_metadata'` dictionary.
- Otherwise, if a string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object
is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its
informational dictionary added to the `'tally_metadata'` dictionary.
- Setting `include_phitsout_in_metadata = None` functions the same as `include_phitsout_in_metadata = True`
but sets `include_input_echo = False` in the `parse_phitsout_file()` call.
- **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the pickle file this function
seeks to generate already exists. If `False` (default behavior), this function will parse the PHITS
output files as usual and overwrite the existing pickle file. If `True`, this function will instead
simply just read the existing found pickle file and return its stored `tally_output` contents.
- **`compress_pickle_with_lzma`** = (D=`False`; requires `save_output_pickle=True`) Boolean designating
whether the pickle file to be saved will be compressed with
[LZMA compression](https://docs.python.org/3/library/lzma.html) (included in
the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the
pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`.
A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as:
`with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`.
For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant
compression, but LZMA compression can reduce the file size by several orders of magnitude,
great for output from massive tallies.
- **`autoplot_tally_output`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting whether
the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path as
`tally_output_filepath` but ending in `.pdf`/`.png`) using the `autoplot_tally_results()` function.
Output:
- **`tally_output`** = a dictionary object with the below keys and values:
- `'tally_data'` = a 10-dimensional NumPy array containing all tally results, explained in more detail below
- `'tally_metadata'` = a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units
- `'tally_dataframe'` = a Pandas dataframe version of `tally_data` (`None` if `make_PandasDF = False`)
†_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `tally_metadata` dictionary
will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access
(e.g., `tally_metadata.mesh` instead of only `tally_metadata['mesh']`). If you do not have Munch, then `tally_metadata`
will just be a standard dictionary object. If, for whatever reason, you have Munch installed but do not wish for
`tally_metadata` to be a Munch object, then in the first line of code for the `parse_tally_header()` function,
set `prefer_to_munch_meta_dict = False`._
-----
-----
Notes:
**Quick code for loading pickled tally output**
import pickle, lzma; from pathlib import Path
p = Path('path/to/pickle/file.pickle[.xz]')
tally_output = pickle.load(lzma.open(p, 'rb') if p.name[-3:]=='.xz' else open(p, 'rb'))
tally_data, tally_metadata, tally_dataframe = [tally_output[k] for k in tally_output.keys()]
-----
**The `'tally_data'` 10-dimensional NumPy array**
Many quantities can be scored across the various tallies in the PHITS code. This function outputs a "universal"
array `tally_data` (`tally_data = tally_output['tally_data']`) that can accomodate all of the different
scoring geometry meshes, physical quantities with assigned meshes, and output axes provided within PHITS.
This is achieved with a 10-dimensional array accessible as
`tally_data[ ir, iy, iz, ie, it, ia, il, ip, ic, ierr ]`, with indices explained below:
Tally data indices and corresponding mesh/axis:
- `0` | `ir`, Geometry mesh: `reg` / `x` / `r` / `tet` ([T-Cross] `ir surf` if `mesh=r-z` with `enclos=0`)
- `1` | `iy`, Geometry mesh: `1` / `y` / `1`
- `2` | `iz`, Geometry mesh: `1` / `z` / `z` ([T-Cross] `iz surf` if `mesh=xyz` or `mesh=r-z` with `enclos=0`)
- `3` | `ie`, Energy mesh: `eng` ([T-Deposit2] `eng1`)
- `4` | `it`, Time mesh
- `5` | `ia`, Angle mesh
- `6` | `il`, LET mesh
- `7` | `ip`, Particle type (`part = `)
- `8` | `ic`, Special: [T-Deposit2] `eng2`; [T-Yield] `mass`, `charge`, `chart`; [T-Interact] `act`
- `9` | `ierr = 0/1/2`, Value / relative uncertainty / absolute uncertainty (expanded to `3/4/5`, or `2/3` if
`calculate_absolute_errors = False`, for [T-Cross] `mesh=r-z` with `enclos=0` case; see notes further below)
By default, all array dimensions are length-1 (except `ierr`, which is length-3). These dimensions are set/corrected
automatically when parsing the tally output file. Thus, for very simple tallies, most of these indices will be
set to 0 when accessing tally results, e.g. `tally_data[2,0,0,:,0,0,0,:,0,:]` to access the full energy spectrum
in the third region for all scored particles / particle groups with the values and uncertainties.
-----
**The `'tally_metadata'` dictionary**
The output `tally_metadata` dictionary contains all information needed to identify every bin along every
dimension: region numbers/groups, particle names/groups, bin edges and midpoints for all mesh types
(x, y, z, r, energy, angle, time, and LET) used in the tally.
It typically contains quite a bit of information. To conveniently view its contents, one can import the built-in
pprint library `import pprint` and then use `pprint.pp(dict(tally_output['tally_metadata']))`.
At a basic level, the "header" of the PHITS output file (everything before the first instance of `newpage:`) is
parsed and, in effect, a key+value pair is created for every line containing an equals sign `=`.
The following keys exist denoting the lengths of the first 9 axes/dimensions of the `tally_data` NumPy array:
`'nreg'`/`'nx'`/`'nr'`/`'ntet'`, `'ny'`, `'nz'`, `'ne'`, `'nt'`, `'na'`, `'nl'`, `'npart'`, and `'nc'`.
These keys are all initialized as `None` and assigned a value if found in the PHITS output file. Thus, if one of
these retains its value of `None`, the corresponding array axis length should be 1. The `'found_mesh_kinds'` key
lists which of these meshes are identified.
For these values with a defined numerical mesh, taking energy `'e'` for example here, a number of different keys
and values will be read/generated for the metadata dictionary, such as: `'e-type'`, `'emin'`, `'emax'`, `'edel'`,
`'e-mesh_bin_edges'`, `'e-mesh_bin_mids'`, and `'e-mesh_bin_mids_log'`.
Complications arise in PHITS allowing the flexibility of grouping regions and particles together for scoring.
For example, something like `(1 2 5)` can be provided to `reg = ` or something like `(neutron photon)` can be
provided to `part = ` in a tally. To accomodate this, some additional key+value pairs are introduced for the
metadata dictionary:
For regions:
- `'reg'` = the string following `reg = ` in the PHITS output
- `'reg_groups'` = a list of strings of each "group" of region(s) provided to `reg = ` in the PHITS output
- `'num_reg_groups'` = an integer of the length of `'reg_groups'`, which is also the length of the first dimension of the `tally_data` array and should equal `'nreg'`
- `'nreg'` = an integer of the number of scoring regions, should equal `'num_reg_groups'`
- `'reg_serial_num'` = a list of integers counting the region groups, starting at 1
- `'reg_num'` = a list of unique strings of the region numbers found (for individual regions) or assigned by PHITS (for combined regions)
For particles:
- `'part'` = the string following `part = ` in the PHITS output
- `'npart'` = the integer number of particle scoring groups provided to `part = ` in the PHITS output
- `'part_groups'` = a list of strings of each "group" of particle(s) provided to `part = ` in the PHITS output
- `'kf_groups'` = a list of strings like `'part_groups'` but containing the kf-code particle identification numbers (see PHITS manual Table 4.4) for each particle/group; `0` is assigned to `part = all`
- `'part_serial_groups'` = a list of strings with the serialized name of each particle scoring group (`'p1-group'`, `'p2-group'`, etc.)
Perhaps of use for quickly making plots using PHITS Tools, the following keys also exist:
- `'title'` = the title string of the plots used in the .eps files
- `'axis_dimensions'` = `1` or `2` denoting whether the value provided to `axis = ` indicated one or two dimensional plots
- `'axis_index_of_tally_array'` = integer specifying what axis of `tally_data` should be accessed for the quantity provided to `axis = `
- `'axis1_label'` = the string of the horizontal axis label of the plots used in the .eps files
- `'value_label'` = the string of the vertical axis label of the 1D plots used in the .eps files
- `'axis2_label'` = the string of the vertical axis label of the 2D plots used in the .eps files
Note that for some tallies there may be additional special entries (like for [T-Point]) or that some of the key
names may differ slightly from those stated here (such as for [T-Deposit2]).
If the `include_phitsout_in_metadata` setting is enabled, `parse_phitsout_file()` will be called to provide a
dictionary containing metadata about the PHITS run itself—by default also including the PHITS run's input
echo—added to this metadata dictionary under the `'phitsout'` key.
See the `parse_phitsout_file()` function's description for more details about the structure of this dictionary.
-----
**The `'tally_dataframe'` Pandas DataFrame**
The `tally_dataframe` Pandas dataframe output functions as normal. The dataframe contains a number of rows equal
to the number of Values in the NumPy array (i.e., the product of the lengths of the first nine dimensions of the
ten-dimensional array) and extra columns for each array dimension of length greater than one (e.g., if the tally
includes an energy and/or angle mesh, columns will be present stating the energy/angle bin of each row).
A dictionary containing supplemental information that is common to all rows of the dataframe is included and
can be accessed with `tally_dataframe.attrs`.
-----
-----
Exceptions:
**Unsupported tallies and DCHAIN**
The following tallies are NOT supported by this function:
- [T-WWG], [T-WWBG], and [T-Volume] (due to being "helper" tallies for generating text sections meant for reinsertion into a PHITS input file)
- [T-Gshow], [T-Rshow], [T-3Dshow], and [T-4Dtrack] (due to being visualization tallies meant for ANGEL/PHIG-3D)
- [T-Userdefined] (due to having no standard format)
- [T-Dchain]‡
‡If provided with the output file of [T-Dchain] (the input file for the DCHAIN code) or the `*.act` main
output file produced by the DCHAIN code, this function will attempt to import the [DCHAIN Tools module](https://github.com/Lindt8/DCHAIN-Tools)
and process the found DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file.
Otherwise, for [T-Dchain] or [T-Yield] with `axis = dchain`, you can manually use the separate suite of parsing
functions included in the [DCHAIN Tools](https://github.com/Lindt8/DCHAIN-Tools) module.
-----
**[T-Cross] special case**
The [T-Cross] tally is unique (scoring across region boundaries rather than within regions), creating some
additional challenges.
In the `mesh = reg` case, much is the same except each region number is composed of the `r-from` and `r-to` values, e.g. `'100 - 101'`.
For `xyz` and `r-z` meshes, an additional parameter is at play: `enclos`.
By default, `enclos=0`.
In the event `enclos=1` is set, the total number of geometric regions is still either `nx*ny*nz` or `nr*nz` for
`xyz` and `r-z` meshes, respectively.
For `enclos=0` in the `mesh = xyz` case, the length of the z dimension (`iz` index) is instead equal to `nzsurf`,
which is simply one greater than `nz` (# regions = `nx*ny*(nz+1)`).
For `enclos=0` in the `mesh = r-z` case, this is much more complicated as PHITS will output every combination of
`nr*nzsurf` AND `nrsurf*nz`, noting `nzsurf=nz+1` and `nrsurf=nr+1` (or `nrsurf=nr` if the first radius bin edge
is `r=0.0`).
The solution implemented here is to, for only this circumstance (in only the `enclos=0 mesh=r-z` case),
set the length of the `ir` and `iz` dimensions to `nrsurf` and `nzsurf`, respectively, and also
to expand the length of the final dimension of `tally_data` from 3 to 6 (or from 2 to 4 if `calculate_absolute_errors=False`), where:
- `ierr = 0/1/2` refer to the combinations of `nr` and `nzsurf` (or `0/1` if `calculate_absolute_errors=False`)
- `ierr = 3/4/5` refer to the combinations of `nrsurf` and `nz` (or `2/3` if `calculate_absolute_errors=False`)
In this case, the Pandas dataframe, if enabled, will contain 3 (or 2) extra columns `value2` and `rel.err.2` [and `abs.err.2`],
which correspond to the combinations of `nrsurf` and `nz` (while the original columns without the "2" refer to
values for combinations of and `nr` and `nzsurf`).
-----
**[T-Yield] special case**
[T-Yield] is also a bit exceptional. When setting the `axis` parameter equal to `charge`, `mass`, or `chart`,
the `ic` dimension of `tally_data` is used for each entry of charge (proton number, Z), mass (A), or
isotope/isomer, respectively.
In the case of `axis = charge` or `axis = mass`, the value of `ic` refers to the actual charge/proton number Z
or mass number A when accessing `tally_data`; for instance, `tally_data[:,:,:,:,:,:,:,:,28,:]`
references results from nuclei with Z=28 if `axis = charge` or A=28 if `axis = mass`. The length of the `ic`
dimension is initialized as 130 or 320 but is later reduced to only just include the highest charge or mass value.
In the case of `axis = chart`, the length of the `ic` dimension is initially set equal to the `mxnuclei` parameter
in the [T-Yield] tally. If `mxnuclei = 0` is set, then the length of the `ic` dimension is initially set to 10,000.
This `ic` dimension length is later reduced to the total number of unique nuclides found in the output.
Owing to the huge number of possible nuclides, a list of found nuclides with nonzero yield is assembled and
added to `tally_metadata` under the keys `nuclide_ZZZAAAM_list` and `nuclide_isomer_list`, i.e.
`tally_metadata['nuclide_ZZZAAAM_list']` and `tally_metadata['nuclide_isomer_list']`.
These lists should be referenced to see what nuclide each of index `ic` refers to.
The entries of the ZZZAAAM list are intergers calculated with the formula 10000*Z + 10*A + M, where M is the
metastable state of the isomer (0 = ground state, 1 = 1st metastable/isomeric state, etc.). The entries
of the isomer list are these same nuclides in the same order but written as plaintext strings, e.g. `'Al-28'` and `'Xe-133m1'`.
The lists are ordered in the same order nuclides are encountered while parsing the output file.
Thus, to sensibly access the yield of a specific nuclide, one must first find its index `ic` in one of the two
metadata lists of ZZZAAAM values or isomer names and then use that to access `tally_data`. For example, to get
the yield results of production of carbon-14 (C-14), one would use the following code:
`ic = tally_metadata['nuclide_ZZZAAAM_list'].index(60140)`
OR
`ic = tally_metadata['nuclide_isomer_list'].index('C-14')`
then
`my_yield_values = tally_data[:,:,:,:,:,:,:,:,ic,:]`
'''
'''
The old [T-Cross] mesh=r-z enclos=0 solution is written below:
The solution implemented here uses `ir` to iterate `nr`, `iy` to iterate `nrsurf`, `iz` to
iterate `nz`, and `ic` to iterate `nzsurf`. Since only `rsurf*z [iy,iz]` and `r*zsurf [ir,ic]` pairs exist,
when one pair is being written, the other will be `[-1,-1]`, thus the lengths of these dimensions for the array
are increased by an extra 1 to prevent an overlap in the data written.
'''
tally_output_filepath = Path(tally_output_filepath)
pickle_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle')
picklexz_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle.xz')
if prefer_reading_existing_pickle and (os.path.isfile(pickle_filepath) or os.path.isfile(picklexz_filepath)):
import pickle
if os.path.isfile(pickle_filepath):
print('Reading found pickle file: ', pickle_filepath)
with open(pickle_filepath, 'rb') as handle:
tally_output = pickle.load(handle)
else:
print('Reading found pickle file: ', picklexz_filepath)
import lzma
with lzma.open(picklexz_filepath, 'rb') as handle:
tally_output = pickle.load(handle)
if autoplot_tally_output:
if tally_output['tally_dataframe'] is not None:
plot_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pdf')
if not plot_filepath.is_file(): # only make plot if file doesn't already exist
from inspect import signature
max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7
tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1])
if tot_num_values > max_num_values_to_plot:
print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values,
' elements), deemed too large for automatic plotting (default max of', max_num_values_to_plot, 'elements).')
else:
autoplot_tally_results(tally_output, output_filename=plot_filepath,
plot_errorbars=calculate_absolute_errors,
additional_save_extensions=['.png'])
return tally_output
# main toggled settings
#calculate_absolute_errors = True
construct_Pandas_frame_from_array = make_PandasDF
#process_all_tally_out_files_in_directory = False
save_pickle_files_of_output = save_output_pickle # save metadata, array, and Pandas frame in a pickled dictionary object
if construct_Pandas_frame_from_array: import pandas as pd
# Check if is _err or _dmp file (or normal value file)
is_val_file = False
is_err_file = False
is_dmp_file = False
if tally_output_filepath.stem[-4:] == '_err':
is_err_file = True
elif tally_output_filepath.stem[-4:] == '_dmp':
is_dmp_file = True
else:
is_val_file = True
if is_dmp_file:
print('ERROR: The provided file is a "dump" output file. Use the function titled "parse_tally_dump_file" to process it instead.')
return None
if is_err_file:
print('WARNING: Provided file contains just relative uncertainties.',str(tally_output_filepath))
potential_val_file = Path(tally_output_filepath.parent, tally_output_filepath.stem.replace('_err','') + tally_output_filepath.suffix)
if potential_val_file.is_file():
print('\t Instead, both it and the file with tally values will be parsed.')
potential_err_file = tally_output_filepath
tally_output_filepath = potential_val_file
is_val_file = True
is_err_file = False
else:
print('\t The corresponding file with tally values could not be found, so only these uncertainties will be parsed.')
# Split content of output file into header and content
if in_debug_mode: print("\nSplitting output into header and content... ({:0.2f} seconds elapsed)".format(time.time() - start))
tally_header, tally_content = split_into_header_and_content(tally_output_filepath)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
# print(len(tally_content))
# Check if *_err file exists
potential_err_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '_err' + tally_output_filepath.suffix)
is_err_in_separate_file = potential_err_file.is_file() # for some tallies/meshes, uncertainties are stored in a separate identically-formatted file
# Extract tally metadata
if in_debug_mode: print("\nExtracting tally metadata... ({:0.2f} seconds elapsed)".format(time.time() - start))
tally_metadata = parse_tally_header(tally_header, tally_content)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
if in_debug_mode: pprint.pp(dict(tally_metadata))
# Check if tally_type is among those supported.
unsupported_tally_types = ['[T-WWG]', '[T-WWBG]', '[T-Volume]', '[T-Userdefined]', '[T-Gshow]', '[T-Rshow]',
'[T-3Dshow]', '[T-4Dtrack]', '[T-Dchain]', 'UNKNOWN']
if tally_metadata['tally_type'] in unsupported_tally_types:
print('ERROR! tally type',tally_metadata['tally_type'],'is not supported by this function!')
if tally_metadata['tally_type'] == '[T-Dchain]' or tally_output_filepath.suffix == '.act':
print('Instead, the DCHAIN Tools module is used to process the DCHAIN output files with the same basename of the provided file.')
dchain_tools_url = 'github.com/Lindt8/DCHAIN-Tools'
dchain_tools_go_to_github_str = 'The DCHAIN Tools module ( '+dchain_tools_url+' ) is capable of parsing all DCHAIN-related output.'
if tally_output_filepath.suffix != '.act':
act_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.act')
if not act_filepath.is_file():
# DCHAIN output is not present in directory
print('Failed to find the main DCHAIN *.act output file:',act_filepath)
print('Aborting this process...')
return None
try:
from dchain_tools import process_dchain_simulation_output
except:
try:
from PHITS_tools.dchain_tools import process_dchain_simulation_output
except:
print('Failed to import the DCHAIN Tools module; to parse DCHAIN output via PHITS Tools, please install DCHAIN Tools and configure it in your Python environment')
return None
simulation_folder_path = str(Path(tally_output_filepath.parent)) + '\\'
simulation_basename = str(tally_output_filepath.stem)
dchain_output = process_dchain_simulation_output(simulation_folder_path,simulation_basename,process_DCS_file=True)
if save_output_pickle:
import pickle
path_to_pickle_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle')
if in_debug_mode: print("\nWriting output to pickle file... ({:0.2f} seconds elapsed)".format(time.time() - start))
with open(path_to_pickle_file, 'wb') as handle:
pickle.dump(dchain_output, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', path_to_pickle_file, '\n')
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
return dchain_output
else:
return None
if (tally_metadata['tally_type'] == '[T-Yield]' or tally_metadata['tally_type'] == '[T-Track]') and tally_metadata['axis'] == 'dchain':
dchain_tools_url = 'github.com/Lindt8/DCHAIN-Tools'
print('This function does not support [T-Yield]/[T-Track] with setting "axis = dchain".')
print('However, the DCHAIN Tools module (', dchain_tools_url, ') is capable of parsing all DCHAIN-related output.')
return None
# Initialize tally data array with zeros
tally_data = initialize_tally_array(tally_metadata, include_abs_err=calculate_absolute_errors)
# Parse tally data
if is_val_file:
err_mode = False
else: # if is_err_file
err_mode = True
if in_debug_mode: print("\nParsing tally data... ({:0.2f} seconds elapsed)".format(time.time() - start))
if tally_metadata['tally_type']=='[T-Yield]' and tally_metadata['axis'] in ['chart','charge','mass']: # need to update metadata too
tally_data, tally_metadata = parse_tally_content(tally_data, tally_metadata, tally_content, is_err_in_separate_file, err_mode=err_mode)
else:
tally_data = parse_tally_content(tally_data, tally_metadata, tally_content, is_err_in_separate_file, err_mode=err_mode)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
err_data_found = True
if tally_metadata['axis_dimensions'] == 2 and tally_metadata['2D-type'] != 4:
if is_err_file:
err_data_found = False
elif is_err_in_separate_file:
err_tally_header, err_tally_content = split_into_header_and_content(potential_err_file)
if in_debug_mode: print("\nParsing tally error... ({:0.2f} seconds elapsed)".format(time.time() - start))
if tally_metadata['tally_type'] == '[T-Yield]' and tally_metadata['axis'] in ['chart','charge','mass']: # need to update metadata too
tally_data, tally_metadata = parse_tally_content(tally_data, tally_metadata, err_tally_content, is_err_in_separate_file,err_mode=True)
else:
tally_data = parse_tally_content(tally_data, tally_metadata, err_tally_content, is_err_in_separate_file, err_mode=True)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
else:
print('WARNING: A separate file ending in "_err" containing uncertainties should exist but was not found.')
err_data_found = False
if calculate_absolute_errors:
if err_data_found:
if in_debug_mode: print("\nCalculating absolute errors... ({:0.2f} seconds elapsed)".format(time.time() - start))
tally_data = calculate_tally_absolute_errors(tally_data)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
elif is_err_file:
print('WARNING: Absolute errors not calculated since the main tally values file was not found.')
else:
print('WARNING: Absolute errors not calculated since the _err file was not found.')
# Generate Pandas dataframe of tally results
if construct_Pandas_frame_from_array:
if in_debug_mode: print("\nConstructing Pandas dataframe... ({:0.2f} seconds elapsed)".format(time.time() - start))
tally_Pandas_df = build_tally_Pandas_dataframe(tally_data, tally_metadata)
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
else:
tally_Pandas_df = None
phitsout_dict = {}
include_input_echo = True
if include_phitsout_in_metadata is None:
include_input_echo = False
include_phitsout_in_metadata = True
if type(include_phitsout_in_metadata) == bool: # True or False
if include_phitsout_in_metadata: # True provided, need to search for phits.out
phitsout_file = Path(tally_output_filepath.parent, 'phits.out')
if phitsout_file.exists():
phitsout_dict = parse_phitsout_file(phitsout_file, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo)
elif isinstance(include_phitsout_in_metadata, dict): # dictionary provided for phitsout
phitsout_dict = include_phitsout_in_metadata
else: # assume path string/object pointing to phits.out is provided
phitsout_dict = parse_phitsout_file(include_phitsout_in_metadata, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma)
if phitsout_dict != {}:
tally_metadata['phitsout'] = phitsout_dict
tally_output = {
'tally_data': tally_data,
'tally_metadata': tally_metadata,
'tally_dataframe': tally_Pandas_df,
}
if save_output_pickle:
import pickle
if compress_pickle_with_lzma:
import lzma
compression_file_extension = '.xz'
else:
compression_file_extension = ''
path_to_pickle_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle' + compression_file_extension)
if in_debug_mode: print("\nWriting output to pickle file... ({:0.2f} seconds elapsed)".format(time.time() - start))
if compress_pickle_with_lzma:
with lzma.open(path_to_pickle_file, 'wb') as handle:
pickle.dump(tally_output, handle, protocol=4) # protocol=4 needed to pickle an object w/ a Pandas DF
else:
with open(path_to_pickle_file, 'wb') as handle:
pickle.dump(tally_output, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', path_to_pickle_file, '\n')
if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start))
if autoplot_tally_output:
if not make_PandasDF:
print('Plotting via "autoplot_tally_output=True" requires also setting "make_PandasDF=True".')
else:
from inspect import signature
max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7
tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1])
if tot_num_values > max_num_values_to_plot:
print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values,
' elements), deemed too large for automatic plotting (default max of',max_num_values_to_plot,'elements).')
else:
plot_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pdf')
autoplot_tally_results(tally_output, output_filename=plot_filepath,
plot_errorbars=calculate_absolute_errors,
additional_save_extensions=['.png'])
return tally_output
def parse_tally_dump_file(path_to_dump_file, dump_data_number=None , dump_data_sequence=None, return_directional_info=False,
use_degrees=False,max_entries_read=None,return_namedtuple_list=True,
return_Pandas_dataframe=True, save_namedtuple_list=False, save_Pandas_dataframe=False,
compress_pickles_with_lzma=True, prefer_reading_existing_pickle=False,
split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0):
'''
Description:
Parses the dump file of a [T-Cross], [T-Product], or [T-Time] tally generated by PHITS, in ASCII or binary format.
Dependencies:
- `import numpy as np`
- `from collections import namedtuple`
- `from scipy.io import FortranFile`
- `import pandas as pd` (if `return_Pandas_dataframe = True`)
Inputs:
(required)
- **`path_to_dump_file`** = string or Path object denoting the path to the dump tally output file to be parsed
- **`dump_data_number`** = integer number of data per row in dump file, binary if >0 and ASCII if <0.
This should match the value following `dump=` in the tally creating the dump file. (D=`None`)
If not specified, the `search_for_dump_parameters()` function will attempt to find it automatically.
- **`dump_data_sequence`** = string or list of integers with the same number of entries as `dump_data_number`,
mapping each column in the dump file to their physical quantities. (D=`None`)
This should match the line following the `dump=` line in the tally creating the dump file.
See PHITS manual section "6.7.22 dump parameter" for further explanations of these values.
If not specified, the `search_for_dump_parameters()` function will attempt to find it automatically.
Inputs:
(optional)
- **`return_directional_info`** = (D=`False`) Boolean designating whether extra directional information
should be calculated and returned; these include: radial distance `r` from the origin in cm,
radial distance `rho` from the z-axis in cm,
polar angle `theta` between the direction vector and z-axis in radians [0,pi] (or degrees), and
azimuthal angle `phi` of the direction vector in radians [-pi,pi] (or degrees).
Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file.
- **`use_degrees`** = (D=`False`) Boolean designating whether angles `theta` and `phi` are returned
in units of degrees. Default setting is to return angles in radians.
- **`max_entries_read`** = (D=`None`) integer number specifying the maximum number of entries/records
of the dump file to be read. By default, all records in the dump file are read.
- **`return_namedtuple_list`** = (D=`True`) Boolean designating whether `dump_data_list` is returned.
- **`return_Pandas_dataframe`** = (D=`True`) Boolean designating whether `dump_data_frame` is returned.
- **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the
pickle file(s) this function seeks to generate already exist. If `False` (default behavior),
this function will parse the PHITS dump files as usual and overwrite the existing pickle file(s).
If `True`, this function will instead simply just read the existing found pickle file(s) and return the stored contents.
Note that it can also be used for creating LZMA compressed/decompressed versions of the pickle file(s)
if the file is set to be saved and the found existing file's compression (or lack thereof) contrasts
with the current setting of `compress_pickles_with_lzma`.
- **`compress_pickles_with_lzma`** = (D=`True`) Boolean designating whether the pickle files to be saved of
the namedtuple list (if `save_namedtuple_list = True`) and/or the Pandas DataFrame (if `save_Pandas_dataframe = True`)
will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included within
the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the file
extension of the saved pickle file(s) will be `'.pickle.xz'` instead of just `'.pickle'`.
A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as:
`with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)`.
While compression will notably slow down the file-saving process, owing to the often large size of
PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred.
- **`save_namedtuple_list`** = (D=`False`) Boolean designating whether `dump_data_list` is saved to a pickle file,
which will by default be compressed with LZMA (built-in with Python; see the `compress_pickles_with_lzma` option).
For complicated reasons†, the namedtuple list is converted to a [NumPy recarray](https://numpy.org/doc/stable/reference/generated/numpy.recarray.html) before being saved,
which is nearly functionally identical to the list of namedtuples. When viewing individual entries,
the field names are not printed (but can be accessed with `dump_data_list.dtype.names`), but you can access
the fields in exactly the same atribute access way as namedtuples (e.g., `dump_data_list[0].time` to
view the time column value for the first entry in the list of events) as well as in a dictionary-like
way (e.g., `dump_data_list[0]['time']` for that same time value).
†_Pickle could not save the list of namedtuples without error; the dill library could but
adding the extra external dependency was undesirable. Furthermore, the time performance of saving the
NumPy recarray was substantially superior, approximately a factor of 2, relative to just using dill to
save the list of namedtuples, also using the LZMA compression in both cases._
- **`save_Pandas_dataframe`** = (D=`False`) Boolean designating whether `dump_data_frame` is saved to a pickle
file (via the Pandas `.to_pickle()` method), which will by default
be compressed with LZMA (built-in with Python; see the `compress_pickles_with_lzma` option). This pickle
can then be accessed via the [`pandas.read_pickle(path_to_DF_pickle)`](https://pandas.pydata.org/docs/reference/api/pandas.read_pickle.html) function.
- **`split_binary_dumps_over_X_GB`** = (D=`20` GB) an integer/float specifying the number of gigabytes over
which a _binary_ dump file will be split into chunks of this size for processing‡.
- **`merge_split_dump_handling`** = (D=`0`) For instances where binary dump files are to be split,
described above for `split_binary_dumps_over_X_GB` and the the note below‡, input one of the following
numbers to determine what will happen to the intermediate pickle files created from each split chunk:
- `0` = Merge the intermediate pickle files (via `merge_dump_file_pickles()`), then, if successful, delete them.
- `1` = Do not merge the intermediate pickle files.
- `2` = Merge the intermediate pickle files but do not delete them afterward.
Outputs:
- **`dump_data_list`** = List of length equal to the number of records contained in the file. Each entry in the list
is a namedtuple containing all of the physical information in the dump file for a given particle event,
in the same order as specified in `dump_data_sequence` and using the same naming conventions for keys as
described in the PHITS manual section "6.7.22 dump parameter"
(`kf`, `x`, `y`, `z`, `u`, `v`, `w`, `e`, `wt`, `time`, `c1`, `c2`, `c3`, `sx`, `sy`, `sz`, `name`, `nocas`, `nobch`, `no`).
If `return_directional_info = True`, `r`, `rho`, `theta`, and `phi` are appended to the end of this namedtuple, in that order.
- **`dump_data_frame`** = A Pandas dataframe created from `dump_data_list` with columns for each physical quantity
and rows for each record included in the dump file.
Notes:
‡For extremely large dump files, having sufficient available memory (RAM) can become an issue.
This function has a built-in feature for automatically splitting _binary_ dump files over a certain size
(20 GB by default) into smaller chunks to be processed individually and then merged at the end.
You can control when/if this splitting is triggered and how the resulting files are merged/handled
with the `split_binary_dumps_over_X_GB` and `merge_split_dump_handling` inputs to this function.
If running PHITS simulations yielding absolutely massive ASCII dump files, consider setting the tally's `dump` parameter to
a positive number to write _binary_ (much more space efficient) dump files instead, unless you have a good
reason for needing them as ASCII.
For more information on converting an existing ASCII dump file to a binary one,
see the documentation within the following directory of your PHITS installation: /phits/utility/dump-a/
'''
# The below variables handle if/when absolutely massive binary dump files are forcibly split to not exceed RAM limits.
# This only applies to dump files stored in binary.
force_split_handling_of_massive_dumps = True # If False, this function will attempt to ingest the whole dump file, regardless of how big.
max_bytes_to_handle_per_dump_part = split_binary_dumps_over_X_GB * 1073741824 # There are 1073741824 bytes in 1 GB.
# max_bytes_to_handle_per_dump_part sets how much of the dump file is processed at a time, the default above is 20 GB.
merge_split_parts_at_end = True # determines if split pickles will be merged at the end
delete_merged_parts_at_end = True # if merging, determines if component parts will be deleted or not.
if merge_split_dump_handling == 1: merge_split_parts_at_end = False
if merge_split_dump_handling == 2: delete_merged_parts_at_end = False
# Initializing this as False
split_dump = False # This will be set to True if the dump file's size is found to exceed max_bytes_to_handle_per_dump_part
from collections import namedtuple
#from typing import NamedTuple
from scipy.io import FortranFile
if return_Pandas_dataframe or save_Pandas_dataframe:
import pandas as pd
if save_Pandas_dataframe or save_namedtuple_list or prefer_reading_existing_pickle:
import pickle
import lzma
if not return_namedtuple_list and not return_Pandas_dataframe and not save_namedtuple_list and not save_Pandas_dataframe:
raise ValueError('ERROR: All "return_namedtuple_list", "return_Pandas_dataframe", "save_namedtuple_list", and "save_Pandas_dataframe" are False. Enable at least one to use this function.')
if dump_data_number == None or dump_data_sequence == None:
dump_data_number, dump_data_sequence = search_for_dump_parameters(path_to_dump_file)
if dump_data_number == None or dump_data_sequence == None:
raise ValueError("Please manually specify 'dump_data_number' and 'dump_data_sequence'; these were not inputted and could not be automatically found from an origin tally standard output file.")
#return None
if isinstance(dump_data_sequence, str):
dump_data_sequence = dump_data_sequence.split()
dump_data_sequence = [int(i) for i in dump_data_sequence]
dump_file_is_binary = True if (dump_data_number > 0) else False # if not binary, file will be ASCII
data_values_per_line = abs(dump_data_number)
if data_values_per_line != len(dump_data_sequence):
raise ValueError('ERROR: Number of values in "dump_data_sequence" is not equal to "dump_data_number"')
path_to_dump_file = Path(path_to_dump_file)
dump_file_suffixes = path_to_dump_file.suffixes
MPI_subdump_num_str = ''
if len(dump_file_suffixes) > 1 and is_number(dump_file_suffixes[-1][1:]) and '_dmp' in path_to_dump_file.stem: # MPI dump found
MPI_subdump_num_str = dump_file_suffixes[-1] # [1:]
if compress_pickles_with_lzma:
compression_file_extension = '.xz'
else:
compression_file_extension = ''
pandas_df_pickle_filepath = Path(path_to_dump_file.parent, path_to_dump_file.stem + MPI_subdump_num_str + '_Pandas_df.pickle' + compression_file_extension)
recarray_pickle_filepath = Path(path_to_dump_file.parent, path_to_dump_file.stem + MPI_subdump_num_str + '_namedtuple_list.pickle' + compression_file_extension)
if prefer_reading_existing_pickle:
df_exists = pandas_df_pickle_filepath.is_file()
read_df_path = pandas_df_pickle_filepath
if not df_exists:
if compress_pickles_with_lzma:
tmp_path = Path(pandas_df_pickle_filepath.parent, pandas_df_pickle_filepath.name.replace('.xz',''))
else:
tmp_path = Path(pandas_df_pickle_filepath.parent, pandas_df_pickle_filepath.name + '.xz')
if tmp_path.is_file():
df_exists = True
read_df_path = tmp_path
ra_exists = recarray_pickle_filepath.is_file()
read_ra_path = recarray_pickle_filepath
read_ra_uses_lzma = compress_pickles_with_lzma
if not ra_exists:
if compress_pickles_with_lzma:
tmp_path = Path(recarray_pickle_filepath.parent, recarray_pickle_filepath.name.replace('.xz', ''))
else:
tmp_path = Path(recarray_pickle_filepath.parent, recarray_pickle_filepath.name + '.xz')
if tmp_path.is_file():
ra_exists = True
read_ra_path = tmp_path
read_ra_uses_lzma = not compress_pickles_with_lzma
# Perform any desired compression/decompression of pickles
if ra_exists:
if compress_pickles_with_lzma and read_ra_path.name[-3:]!='.xz' and save_namedtuple_list: # save a compressed version
with open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
with lzma.open(recarray_pickle_filepath, 'wb') as handle: pickle.dump(records_list, handle, protocol=pickle.HIGHEST_PROTOCOL)
if not compress_pickles_with_lzma and read_ra_path.name[-3:]=='.xz' and save_namedtuple_list: # save an uncompressed version
with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
with open(recarray_pickle_filepath, 'wb') as handle: pickle.dump(records_list, handle, protocol=pickle.HIGHEST_PROTOCOL)
save_namedtuple_list = False
if df_exists:
if (compress_pickles_with_lzma and read_df_path.name[-3:]!='.xz' and save_Pandas_dataframe) or \
(not compress_pickles_with_lzma and read_df_path.name[-3:]=='.xz' and save_Pandas_dataframe): # save a compressed or uncompressed version
records_df = pd.read_pickle(read_df_path)
records_df.to_pickle(pandas_df_pickle_filepath)
save_Pandas_dataframe = False
# Determine if function can end here or if the dump file needs to be read and reprocessed
if (ra_exists and return_namedtuple_list and not save_namedtuple_list) and (df_exists and return_Pandas_dataframe and not save_Pandas_dataframe):
if read_ra_uses_lzma:
with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
else:
with open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
records_df = pd.read_pickle(read_df_path)
return records_list, records_df
elif (ra_exists and return_namedtuple_list and not save_namedtuple_list):
if read_ra_uses_lzma:
with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
else:
with open(read_ra_path, 'rb') as file: records_list = pickle.load(file)
return records_list
elif (df_exists and return_Pandas_dataframe and not save_Pandas_dataframe):
records_df = pd.read_pickle(read_df_path)
return records_df
elif not return_namedtuple_list and not return_Pandas_dataframe and not save_namedtuple_list and not save_Pandas_dataframe:
# nothing more to be done in the function, everything to be saved already exists
return None
# Generate NamedTuple for storing record information
# See PHITS manual section "6.7.22 dump parameter" for descriptions of these values
dump_quantities = ['kf', 'x', 'y', 'z', 'u', 'v', 'w', 'e', 'wt', 'time', 'c1', 'c2', 'c3', 'sx', 'sy', 'sz',
'name', 'nocas', 'nobch', 'no']
dump_col_dtypes = [int, float, float, float, float, float, float, float, float, float, int, int, int, float, float, float,
int, int, int, int]
ordered_record_entries_list = [dump_quantities[i - 1] for i in dump_data_sequence]
rawRecord = namedtuple('rawRecord', ordered_record_entries_list)
if return_directional_info:
ordered_record_entries_list += ['r', 'rho', 'theta', 'phi']
angle_units_mult = 1
if use_degrees: angle_units_mult = 180 / np.pi
Record = namedtuple('Record', ordered_record_entries_list)
records_list = []
if dump_file_is_binary:
# Read binary dump file; extract each record (particle)
file_size_bytes = os.path.getsize(path_to_dump_file)
record_size_bytes = (data_values_per_line + 1) * 8 # each record has 8 bytes per data value plus an 8-byte record end
num_records = int(file_size_bytes / record_size_bytes)
if max_entries_read != None:
if max_entries_read < num_records:
num_records = max_entries_read
bytes_to_read = num_records*record_size_bytes
current_record_count = 0
if force_split_handling_of_massive_dumps and bytes_to_read > max_bytes_to_handle_per_dump_part:
#num_total_parts = np.ceil(bytes_to_read/max_bytes_to_handle_per_dump_part)
max_records_per_part = np.floor(max_bytes_to_handle_per_dump_part/record_size_bytes)
num_total_parts = np.ceil(num_records/max_records_per_part)
remaining_total_records_to_read = num_records
remaining_records_to_read_in_this_part = max_records_per_part
num_current_part = 1
current_bytes_read = 0
split_dump = True
paths_to_split_recarrays = []
paths_to_split_pandas_dfs = []
paths_to_split_fake_dump_files = []
if not save_namedtuple_list and not save_Pandas_dataframe:
raise ValueError('ERROR: "save_namedtuple_list" and "save_Pandas_dataframe" are False. Enable at least one to parse a dump file to be split.')
print('WARNING: Dump file will be split and then remerged to not exceed memory limits.')
#if return_namedtuple_list or return_Pandas_dataframe:
# print('WARNING: When splitting a huge dump, "return_namedtuple_list" and "return_Pandas_dataframe" are set to False.')
# return_namedtuple_list, return_Pandas_dataframe = False, False
with FortranFile(path_to_dump_file, 'r') as f:
while current_record_count < num_records:
current_record_count += 1
remaining_records_to_read_in_this_part -= 1
raw_values = f.read_reals(float)
rawrecord = rawRecord(*raw_values)
if return_directional_info:
# calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis)
r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2)
rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2)
dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w]
theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult
phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult
record = Record(*raw_values, r, rho, theta, phi)
records_list.append(record)
else:
records_list.append(rawrecord)
if remaining_records_to_read_in_this_part<=0: # time to write dump part
paths_to_split_fake_dump_files.append(Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part, num_total_parts) + '.out'))
num_records = num_records - current_record_count
remaining_total_records_to_read -= current_record_count
remaining_records_to_read_in_this_part = min(remaining_total_records_to_read,max_records_per_part)
current_record_count = 0
if save_namedtuple_list:
pickle_path = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part,num_total_parts) + '_namedtuple_list.pickle' + compression_file_extension)
paths_to_split_recarrays.append(pickle_path)
record_type = [(ordered_record_entries_list[i], dump_col_dtypes[i]) for i in range(len(ordered_record_entries_list))]
records_np_array = np.array(records_list, dtype=record_type)
records_np_array = records_np_array.view(np.recarray)
if compress_pickles_with_lzma:
with lzma.open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
else:
with open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
del records_np_array
if save_Pandas_dataframe:
records_df = pd.DataFrame(records_list, columns=Record._fields)
path_to_dump_file = Path(path_to_dump_file)
pickle_path = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part, num_total_parts) + '_Pandas_df.pickle' + compression_file_extension)
paths_to_split_pandas_dfs.append(pickle_path)
records_df.to_pickle(pickle_path)
del records_df
num_current_part += 1
records_list = []
else:
# Normal flow, if not split dump
if return_directional_info:
with FortranFile(path_to_dump_file, 'r') as f:
while current_record_count < num_records:
current_record_count += 1
raw_values = f.read_reals(float)
rawrecord = rawRecord(*raw_values)
# calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis)
r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2)
rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2)
dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w]
theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult
phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult
record = Record(*raw_values, r, rho, theta, phi)
records_list.append(record)
else: # just return data in dump file
with FortranFile(path_to_dump_file, 'r') as f:
while current_record_count < num_records:
current_record_count += 1
raw_values = f.read_reals(float)
record = Record(*raw_values)
records_list.append(record)
else: # file is ASCII
if max_entries_read == None:
max_entries_read = np.inf
if return_directional_info:
with open(path_to_dump_file, 'r') as f:
current_record_count = 0
for line in f:
current_record_count += 1
if current_record_count > max_entries_read: break
line_str_values = line.replace('D', 'E').split()
raw_values = [float(i) for i in line_str_values]
rawrecord = rawRecord(*raw_values)
# calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis)
r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2)
rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2)
dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w]
theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult
phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult
record = Record(*raw_values, r, rho, theta, phi)
records_list.append(record)
else: # just return data in dump file
with open(path_to_dump_file, 'r') as f:
current_record_count = 0
for line in f:
current_record_count += 1
if current_record_count > max_entries_read: break
line_str_values = line.replace('D', 'E').split()
raw_values = [float(i) for i in line_str_values]
record = Record(*raw_values)
records_list.append(record)
#print(record)
if split_dump: # Now attempt to merge dumps back together
if merge_split_parts_at_end:
merged_dump_base_filepath = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str)
merge_dump_file_pickles(paths_to_split_fake_dump_files, merged_dump_base_filepath=merged_dump_base_filepath,
delete_pre_merge_pickles=delete_merged_parts_at_end, compress_pickles_with_lzma=compress_pickles_with_lzma)
if return_namedtuple_list:
path_to_recarray_pickle_file = recarray_pickle_filepath
if compress_pickles_with_lzma:
with lzma.open(path_to_recarray_pickle_file, 'rb') as file: records_list = pickle.load(file)
else:
with open(path_to_recarray_pickle_file, 'rb') as file: records_list = pickle.load(file)
if return_Pandas_dataframe:
records_df = pd.read_pickle(Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_Pandas_df.pickle'))
else: # Normal (non-split) dump saving
if save_namedtuple_list:
path_to_dump_file = Path(path_to_dump_file)
pickle_path = recarray_pickle_filepath
#num_records = len(records_list)
record_type = [(ordered_record_entries_list[i], dump_col_dtypes[i]) for i in range(len(ordered_record_entries_list))]
records_np_array = np.array(records_list, dtype=record_type)
records_np_array = records_np_array.view(np.recarray)
if compress_pickles_with_lzma:
with lzma.open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
else:
with open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', pickle_path, '\n')
del records_np_array # In the event this is huge in memory, best to delete it here before the Pandas DF is made
if return_Pandas_dataframe or save_Pandas_dataframe:
# Make Pandas dataframe from list of records
records_df = pd.DataFrame(records_list, columns=Record._fields)
if save_Pandas_dataframe:
pickle_path = pandas_df_pickle_filepath
records_df.to_pickle(pandas_df_pickle_filepath)
#with open(pickle_path, 'wb') as handle:
# pickle.dump(records_df, handle, protocol=pickle.HIGHEST_PROTOCOL)
# print('Pickle file written:', pickle_path, '\n')
print('Pickle file written:', pandas_df_pickle_filepath, '\n')
if return_namedtuple_list and return_Pandas_dataframe:
return records_list, records_df
elif return_namedtuple_list:
return records_list
elif return_Pandas_dataframe:
return records_df
else:
return None
def parse_all_tally_output_in_dir(tally_output_dirpath, output_file_suffix = None, output_file_prefix = '',
output_file_required_string='', include_subdirectories=False, return_tally_output=False,
make_PandasDF=True, calculate_absolute_errors=True,
save_output_pickle=True, include_phitsout_in_metadata=False,
prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False,
merge_tally_outputs=False, save_pickle_of_merged_tally_outputs=None,
include_dump_files=False,
dump_data_number=None , dump_data_sequence=None,
dump_return_directional_info=False, dump_use_degrees=False,
dump_max_entries_read=None,
dump_save_namedtuple_list=True, dump_save_Pandas_dataframe=True,
dump_merge_MPI_subdumps=True, dump_delete_MPI_subdumps_post_merge=True,
split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0,
autoplot_tally_output=False, autoplot_all_tally_output_in_dir=False
):
'''
Description:
Parse all standard PHITS tally output files in a directory *[DIRECTORY mode]*, returning either a list of dictionaries containing
tally metadata and an array of values from each tally output (and optionally this data inside of a Pandas dataframe too)
or a list of filepaths to pickle files containing these dictionaries, as created with the `parse_tally_output_file()` function.
Alternatively, if provided a PHITS input file or its produced "phits.out" file instead of a directory *[INPUT_FILE mode]*,
this function will instead scan
the provided file for all tally output that should have been produced by the PHITS run and parse all of those files instead.
This function allows selective processing of files in the directory by specification of strings which must
appear at the start, end, and/or anywhere within each filename.
Even if a file satisfies all of these naming criteria, the function will also use `determine_PHITS_output_file_type()`
to check if it is a valid tally output file or phits.out file (meaning, it will skip files like batch.out).
It will also skip over "_err" uncertainty files as these are automatically found by the `parse_tally_output_file()`
function after it processes that tally's main output file.
This function will mainly process standard tally output files, but it can optionally process tally "dump" files too,
though it can only save the dump outputs to its pickle files and not return the (quite large) dump data objects.
The filenames of saved dump data will not be included in the returned list.
Dependencies:
- `import numpy as np`
- `import pandas as pd` (if `make_PandasDF = True`)
- `import seaborn as sns` (if `autoplot_tally_output=True` or `autoplot_all_tally_output_in_dir=True`)
- `from munch import Munch` (will still run if package not found)
- `from collections import namedtuple` (if processing tally dump files)
- `from scipy.io import FortranFile` (if processing tally dump files)
Inputs:
(required)
- **`tally_output_dirpath`** = Path (string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object) to the tally output directory to be searched and parsed
or to a PHITS input file or its "phits.out" file (`file(6)` in the [Parameters] section of a PHITS input file)
whose produced outputs are to be parsed. If provided a directory, this function will operate in *[DIRECTORY mode]*;
otherwise, if provided a PHITS input or phits.out file, it will operate in *[INPUT_FILE mode]*.
Both of these modes are nearly identical in their function, but their differences are highlighted
here in this function's documentation.
Inputs:
(optional)
- **`output_file_suffix`** = (*[DIRECTORY mode]* D=`'.out'` / *[INPUT_FILE mode]* D=`''`)
A string specifying what characters processed filenames (including the file extension)
must end in to be included. This condition is not enforced if set to an empty string `''`.
Note that if the filename contains "_dmp" and has a final extension of the form ".###" (where "###"
is an arbitrarily long numeric string of digits 0-9), as is the case for dump files from PHITS
ran in MPI parallelization, this ".###" final extension is pretended not to exist when enforcing
this `output_file_suffix` parameter, allowing these dump files to be found and processed the same
regardless of whether MPI was used or not when running PHITS.
- **`output_file_prefix`** = (D=`''`) A string specifying what characters processed filenames (including the file extension)
must begin with to be included. This condition is not enforced if set to an empty string `''`.
- **`output_file_required_string`** = (D=`''`) A string which must be present anywhere within processed filenames (including the
file extension) to be included. This condition is not enforced if set to an empty string `''`.
- **`include_subdirectories`** = (D=`False`) A Boolean determining whether this function searches and processes all included
tally output files in this directory AND deeper subdirectories if set to `True`
or only the files directly within the provided directory `tally_output_dirpath` if set to `False`
- **`return_tally_output`** = (D=`False`) A Boolean determining whether this function returns (if `True`) a list of `tally_output` dictionaries
or (if `False`) just a list of filepaths to the pickle files containing these dictionaries
- **`include_dump_files`** = (D=`False`) A Boolean determining whether dump files will be processed too or skipped.
Settings to be applied to all encountered dump files can be specified per the optional inputs
detailed below which are simply passed to the `parse_tally_dump_file()` function. Note that parameters
`return_namedtuple_list` and `return_Pandas_dataframe` will always be `False` when dump files are
processed in a directory with this function; instead, `save_namedtuple_list` and `save_Pandas_dataframe`
are by default set to `True` when parsing dump files in a directory with this function. (Be warned,
if the dump file is large, the produced files from parsing them will be too.)
- **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the pickle file this function
seeks to generate already exists. If `False` (default behavior), this function will parse the PHITS
output files as usual and overwrite the existing pickle file. If `True`, this function will instead
simply just read the existing found pickle file, extracting its stored `tally_output` contents
(and, if for dump file pickles, they'll simply be skipped and not reprocessed).
- **`merge_tally_outputs`** = (D=`False`) A Boolean determining whether this function will merge all
of the produced `tally_output` dictionaries (and "phitsout" dictionaries if `include_phitsout_in_metadata=True`)
into a single larger dictionary object. The output filenames are used as the keys in this dictionary.
In *[DIRECTORY mode]*, this is the relative filepath (from the provided dirctory) of each output file, converted to a string;
in *[INPUT_FILE mode]*, this is the string provided to each tally's `file` parameter in the PHITS input.
This dictionary will respect the `save_pickle_of_merged_tally_outputs` and `compress_pickle_with_lzma` input arguments
for saving and compression, with path/name "`tally_output_dirpath`/ALL_TALLY_OUTPUTS.pickle[.xz]" by default.
If in *[INPUT_FILE mode]*, the default filename of this file will be prefixed by either the PHITS
input filename or "phitsout_", depending on which type of file is provided to this function.
If a string is provided instead of a Boolean, this setting will be set to `True` and the string
will be used as the saved pickle file's filename, with
the file name and path as "`tally_output_dirpath`/`merge_tally_outputs`.pickle[.xz]".
If `return_tally_output` is set to `True`,
this single dictionary will be returned instead of a list of produced `tally_output` dictionaries.
- **`save_pickle_of_merged_tally_outputs`** = (D=`None`; requires `merge_tally_outputs=True`) Boolean denoting whether
the merged dictionary containing all PHITS tally output created when `merge_tally_outputs=True` should
be saved as a pickle file, allowing independent control of saving this pickle file versus those of the
constituent tallies. If left at its default value of `None`, this option will be set to match
the `save_output_pickle` (D=`True`) option. If set to `True` and `return_tally_output=False` and
`save_output_pickle=False`, this function will return the path to this saved pickle file.
- **`autoplot_all_tally_output_in_dir`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting,
for all tally outputs included in this function's call, whether the tallies' outputs will be automatically
plotted and saved to a single PDF, with path/name "`tally_output_dirpath`/ALL_TALLY_OUTPUTS_PLOTTED.pdf" by default,
using the `autoplot_tally_results()` function. If a string is provided instead of a Boolean,
this setting will be set to `True` and the string will be used as the plot PDF's filename, with
the file name and path as "`tally_output_dirpath`/`autoplot_all_tally_output_in_dir`.pdf".
If in *[INPUT_FILE mode]*, the default filename of this PDF will be prefixed by either the PHITS
input filename or "phitsout_", depending on which is provided to this function.
Inputs:
(optional, the same as in and directly passed to the `parse_tally_output_file()` function)
- **`make_PandasDF`** = (D=`True`) A Boolean determining whether a Pandas dataframe of the tally data array will be made
- **`calculate_absolute_errors`** = (D=`True`) A Boolean determining whether the absolute uncertainty of each tally output value
is to be calculated (simply as the product of the value and relative error); if `False`, the final
dimension of `tally_data`, `ierr`, will be of length-2 rather than length-3
- **`save_output_pickle`** = (D=`True`) A Boolean determining whether the `tally_output` dictionary object is saved as a pickle file;
if `True`, the file will be saved with the same path and name as the provided PHITS tally output file
but with the .pickle extension.
- **`include_phitsout_in_metadata`** = (D=`False`) A Boolean determining whether the "phits.out" file
(`file(6)` in the [Parameters] section of a PHITS input file) in the same directory as `tally_output_dirpath`,
if found, should be processed via `parse_phitsout_file()` and have its informational dictionary
about the PHITS run added to the `'tally_metadata'` dictionary under the key `'phitsout'`.
- This only works if a single "phits.out"-type file is found; if multiple are encountered (as determined by `determine_PHITS_output_file_type()`),
they will still be processed with `parse_phitsout_file()` (if `save_output_pickle=True` or `merge_tally_outputs=True`) but not added to
any tally output metadata dictionaries. In this circumstance and if `merge_tally_outputs=True`,
the multiple phits.out dictionaries will be added to the merged dictionary.
- If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided
dictionary will be taken as that to be added as the `'phitsout'` key in the `'tally_metadata'` dictionary.
- Otherwise, if a string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object
is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its
informational dictionary added to the `'tally_metadata'` dictionary.
- Note that this is automatically set to `True` if in *[INPUT_FILE mode]* (if `tally_output_dirpath`
is a file, rather than a directory, and deemed to be a PHITS input file or "phits.out"-type file,
as determined by `determine_PHITS_output_file_type()`);
to override this automatic behavior (i.e., to behave as if set to `False` even when provided a PHITS
input or phits.out file), set `include_phitsout_in_metadata = {}`.
- Setting `include_phitsout_in_metadata = None` functions the same as `include_phitsout_in_metadata = True`
but sets `include_input_echo = False` in the `parse_phitsout_file()` call.
- **`compress_pickle_with_lzma`** = (D=`False`; requires `save_output_pickle=True`) Boolean designating
whether the pickle file to be saved will be compressed with
[LZMA compression](https://docs.python.org/3/library/lzma.html) (included in
the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the
pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`.
A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as:
`with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`.
For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant
compression, but LZMA compression can reduce the file size by several orders of magnitude,
great for output from massive tallies.
- **`autoplot_tally_output`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting whether, for each tally,
the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path as
`tally_output_filepath` but ending in `.pdf`/`.png`) using the `autoplot_tally_results()` function.
Inputs:
(optional, the same as in and directly passed to the `parse_tally_dump_file()` function)
- **`dump_data_number`** = (D=`None`) integer number of data per row in dump file, binary if >0 and ASCII if <0.
This should match the value following `dump=` in the tally creating the dump file.
If not specified, the search_for_dump_parameters() function will attempt to find it automatically.
- **`dump_data_sequence`** = (D=`None`) string or list of integers with the same number of entries as `dump_data_number`,
mapping each column in the dump file to their physical quantities.
This should match the line following the `dump=` line in the tally creating the dump file.
See PHITS manual section "6.7.22 dump parameter" for further explanations of these values.
If not specified, the search_for_dump_parameters() function will attempt to find it automatically.
- **`dump_return_directional_info`** = (D=`False`) Boolean designating whether extra directional information
should be calculated and returned; these include: radial distance `r` from the origin in cm,
radial distance `rho` from the z-axis in cm,
polar angle `theta` between the direction vector and z-axis in radians [0,pi] (or degrees), and
azimuthal angle `phi` of the direction vector in radians [-pi,pi] (or degrees).
Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file.
- **`dump_use_degrees`** = (D=`False`) Boolean designating whether angles `theta` and `phi` are returned
in units of degrees. Default setting is to return angles in radians.
- **`dump_max_entries_read`** = (D=`None`) integer number specifying the maximum number of entries/records
of the dump file to be read. By default, all records in the dump file are read.
- **`dump_save_namedtuple_list`** = (D=`True`) Boolean designating whether `dump_data_list` is saved to a pickle file
(for complicated reasons, the namedtuple list is converted to a [NumPy recarray](https://numpy.org/doc/stable/reference/generated/numpy.recarray.html) before being saved,
which is nearly functionally identical to the list of namedtuples.
For more info and the full explanation, see the documentation for the `parse_tally_dump_file()` function's
`save_namedtuple_list` argument, which is what this argument is passed to anyways).
- **`dump_save_Pandas_dataframe`** = (D=`True`) Boolean designating whether `dump_data_frame` is saved to a pickle
file (via Pandas .to_pickle()).
- **`split_binary_dumps_over_X_GB`** = (D=`20` GB) an integer/float specifying the number of gigabytes over
which a _binary_ dump file will be split into chunks of this size for processing.
- **`merge_split_dump_handling`** = (D=`0`) For instances where binary dump files are to be split,
as determined by the `split_binary_dumps_over_X_GB` setting, input one of the following
numbers to determine what will happen to the intermediate pickle files created from each split chunk:
- `0` = Merge the intermediate pickle files (via `merge_dump_file_pickles()`), then, if successful, delete them.
- `1` = Do not merge the intermediate pickle files.
- `2` = Merge the intermediate pickle files but do not delete them afterward.
- **`dump_merge_MPI_subdumps`** = (D=`True`) Boolean designating whether the pickled namedtuple lists and/or
Pandas DataFrames for all "sub-dumps" from an MPI run of PHITS should be merged into single namedtuple
list and/or Pandas DataFrame pickle file(s) (with `merge_dump_file_pickles()`). When a dump file is written in an MPI execution of PHITS,
it is split into N "sub dump" files, one per MPI process, and given an extra final extension of the
form ".###" (where "###" is an arbitrarily long numeric string of digits 0-9 designating the MPI process).
This option is only active when such files are encountered.
- **`dump_delete_MPI_subdumps_post_merge`** = (D=`True`) Requires `dump_merge_MPI_subdumps=True`, Boolean
designating whether the numerous "sub dump" pickle files merged when `dump_merge_MPI_subdumps=True`
(and MPI dump files are found) are deleted after the merged version combining all of them is saved.
Output:
- **`tally_output_list`** = a list of `tally_output` dictionary objects (if `return_tally_output=True`) with the
below keys and values / a list of file paths to pickle files containing `tally_output` dictionary objects:
- `'tally_data'` = a 10-dimensional NumPy array containing all tally results, explained in more detail below
- `'tally_metadata'` = a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units
- `'tally_dataframe'` = (optionally included if setting `make_PandasDF = True`) a Pandas dataframe version of `tally_data`
- This list of `tally_output` dictionary objects will be returned as a larger merged dictionary/Munch† object,
keyed using the tally output file names, instead if `merge_tally_outputs=True` and `return_tally_output=True`.
If `merge_tally_outputs=True`, `return_tally_output=False`, `save_output_pickle=False`, and `save_pickle_of_merged_tally_outputs=True`,
the path to this saved merged tally output dictionary pickle file will be returned instead.
†_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `tally_metadata` dictionary
and merged tally output dictionary will instead be Munch objects, which are identical to a dictionary but additionally
allows attribute-style access (e.g., `tally_metadata.mesh` instead of only `tally_metadata['mesh']`).
If you do not have Munch, then these will just be standard dictionary objects.
If, for whatever reason, you have Munch installed but do not wish for these to be Munch objects,
then in the first line of code for both this and the `parse_tally_header()` function, set
`prefer_to_munch_merged_dict = False` and `prefer_to_munch_meta_dict = False`, respectively._
'''
prefer_to_munch_merged_dict = True
import os
from inspect import signature
if prefer_to_munch_merged_dict:
try:
from munch import Munch
merged_outputs_dict = Munch({})
except:
merged_outputs_dict = {}
else:
merged_outputs_dict = {}
if save_pickle_of_merged_tally_outputs is None:
save_pickle_of_merged_tally_outputs = save_output_pickle
operating_in_directory_mode = True # otherwise, if False, assume INPUT_FILE mode
is_file6_phitsout_file, is_PHITS_input_file = False, False
include_THIS_phitsout_dict_in_metadata = False
include_input_echo = True
if include_phitsout_in_metadata is None: include_input_echo = False
if not os.path.isdir(tally_output_dirpath):
#print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath)
PHITS_file_type = determine_PHITS_output_file_type(tally_output_dirpath)
if PHITS_file_type['is_file6_phitsout_file'] or PHITS_file_type['is_PHITS_input_file']:
is_file6_phitsout_file, is_PHITS_input_file = PHITS_file_type['is_file6_phitsout_file'], PHITS_file_type['is_PHITS_input_file']
file_type_str = 'PHITS input' if is_PHITS_input_file else 'phits.out'
#print('However, it is a valid '+file_type_str+' file, thus the tally outputs from this PHITS run will be processed.')
operating_in_directory_mode = False
original_input_path = tally_output_dirpath
head, tail = os.path.split(tally_output_dirpath)
tally_output_dirpath = head
elif os.path.isfile(tally_output_dirpath):
print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath)
head, tail = os.path.split(tally_output_dirpath)
tally_output_dirpath = head
print('However, it is a valid path to a file; thus, its parent directory will be used:',tally_output_dirpath)
else:
print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath)
print('Nor is it a valid path to a file. ERROR! Aborting...')
return None
if output_file_suffix is None:
if operating_in_directory_mode:
output_file_suffix = '.out'
else:
output_file_suffix = ''
if is_file6_phitsout_file or is_PHITS_input_file:
# Only list out files produced by this PHITS run
if is_PHITS_input_file:
files_dict = extract_tally_outputs_from_phits_input(original_input_path)
phitsout_dict = parse_phitsout_file(files_dict['phitsout'], save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo)
if not (isinstance(include_phitsout_in_metadata, dict) and include_phitsout_in_metadata=={}): include_THIS_phitsout_dict_in_metadata = True
if files_dict['active_infl_found']:
print('An active "infl:{}" section was found in the PHITS input; therefore its phits.out file will be used for obtaining the complete list of generated output files.')
files_dict = phitsout_dict['produced_files']
else:
phitsout_file = Path(original_input_path)
phitsout_dict = parse_phitsout_file(phitsout_file, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo)
if not (isinstance(include_phitsout_in_metadata, dict) and include_phitsout_in_metadata=={}): include_THIS_phitsout_dict_in_metadata = True
files_dict = phitsout_dict['produced_files']
files_in_dir = files_dict['standard_output']
if include_dump_files: files_in_dir += files_dict['dump_output']
elif include_subdirectories:
# Get paths to all files in this dir and subdirs
files_in_dir = []
for path, subdirs, files in os.walk(tally_output_dirpath):
for name in files:
files_in_dir.append(os.path.join(path, name))
else:
# Just get paths to files in this dir
files_in_dir = [os.path.join(tally_output_dirpath, f) for f in os.listdir(tally_output_dirpath) if os.path.isfile(os.path.join(tally_output_dirpath, f))]
if is_PHITS_input_file:
autoplot_dir_filename = Path(original_input_path).stem + "_ALL_TALLY_OUTPUTS_PLOTTED.pdf"
merged_output_pickle_filename = Path(original_input_path).stem + "_ALL_TALLY_OUTPUTS.pickle"
elif is_file6_phitsout_file:
autoplot_dir_filename = "phitsout_ALL_TALLY_OUTPUTS_PLOTTED.pdf"
merged_output_pickle_filename = "phitsout_ALL_TALLY_OUTPUTS.pickle"
else:
autoplot_dir_filename = "ALL_TALLY_OUTPUTS_PLOTTED.pdf"
merged_output_pickle_filename = "ALL_TALLY_OUTPUTS.pickle"
if isinstance(autoplot_all_tally_output_in_dir, str):
autoplot_dir_filename = autoplot_all_tally_output_in_dir + '.pdf'
autoplot_all_tally_output_in_dir = True
if isinstance(merge_tally_outputs, str):
merged_output_pickle_filename = merge_tally_outputs + '.pickle'
merge_tally_outputs = True
# Determine which files should be parsed
filepaths_to_process = []
dump_filepaths_to_process = []
phitsout_files_to_process = []
len_suffix = len(output_file_suffix)
len_prefix = len(output_file_prefix)
len_reqstr = len(output_file_required_string)
MPI_subdump_files_found = []
MPI_subdump_filestems_found = []
for f in files_in_dir:
head, tail = os.path.split(f)
file_suffixes = tail.split('.')[1:]
if len(file_suffixes) >= 1 and is_number(file_suffixes[-1][1:]) and '_dmp' in tail: # MPI dump found
MPI_subdump_files_found.append(f)
tail = tail[:-1 * (1+len(file_suffixes[-1]))]
if Path(head,tail) not in MPI_subdump_filestems_found: MPI_subdump_filestems_found.append(Path(head,tail))
if len_suffix > 0 and tail[-len_suffix:] != output_file_suffix: continue
if len_prefix > 0 and tail[:len_prefix] != output_file_prefix: continue
if len_reqstr > 0 and output_file_required_string not in tail: continue
if tail[(-4-len_suffix):] == '_err' + output_file_suffix: continue
this_file_type = determine_PHITS_output_file_type(f)
if this_file_type['is_standard_tally_output']:
filepaths_to_process.append(f)
elif include_dump_files and tail[(-4 - len_suffix):] == '_dmp' + output_file_suffix and (
this_file_type['is_binary_tally_dump'] or this_file_type['is_binary_tally_dump']):
dump_filepaths_to_process.append(f)
elif this_file_type['is_file6_phitsout_file']:
phitsout_files_to_process.append(f)
else:
continue
# with open(f) as ff:
# try:
# first_line = ff.readline().strip()
# except: # triggered if encountering binary / non ASCII or UTF-8 file
# if include_dump_files and tail[(-4-len_suffix):] == '_dmp' + output_file_suffix:
# dump_filepaths_to_process.append(f)
# continue
# if len(first_line) == 0: continue
# if first_line[0] != '[' :
# if '_________________________________________________________' in first_line: # phits.out file
# phitsout_files_to_process.append(f)
# if include_dump_files and tail[(-4-len_suffix):] == '_dmp' + output_file_suffix:
# dump_filepaths_to_process.append(f)
# continue
# filepaths_to_process.append(f)
# If there is only one phits.out file, we can safely assume it belongs to all other processed output
if len(phitsout_files_to_process) == 1 and (save_output_pickle or (include_phitsout_in_metadata is None or (type(include_phitsout_in_metadata) == bool and include_phitsout_in_metadata))):
phitsout_dict = parse_phitsout_file(phitsout_files_to_process[0], include_input_echo=include_input_echo, save_phitsout_pickle=save_output_pickle)
if include_phitsout_in_metadata is None or (type(include_phitsout_in_metadata) == bool and include_phitsout_in_metadata):
include_THIS_phitsout_dict_in_metadata = True
# if merge_tally_outputs:
# key = str(f.relative_to(tally_output_dirpath))
# merged_outputs_dict[key] = phitsout_dict
elif len(phitsout_files_to_process) > 1 and (save_output_pickle or merge_tally_outputs): # Otherwise, if more, only process if pickles are to be saved
for f in phitsout_files_to_process:
temp_phitsout_dict = parse_phitsout_file(f, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma)
if merge_tally_outputs:
key = str(Path(f).relative_to(tally_output_dirpath))
merged_outputs_dict[key] = temp_phitsout_dict
if include_THIS_phitsout_dict_in_metadata: # provide a phitsout dictionary created in THIS function to parse_tally_output_file, otherwise pass provided include_phitsout_in_metadata argument
include_phitsout_in_metadata = phitsout_dict
tally_output_pickle_path_list = []
tally_output_list = []
tally_include_in_plotting_list = []
max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7
for f in filepaths_to_process:
f = Path(f)
xz_text = ''
if compress_pickle_with_lzma: xz_text = '.xz'
path_to_pickle_file = Path(f.parent, f.stem + '.pickle' + xz_text)
tally_output = parse_tally_output_file(f, make_PandasDF=make_PandasDF,
calculate_absolute_errors=calculate_absolute_errors,
save_output_pickle=save_output_pickle,
include_phitsout_in_metadata=include_phitsout_in_metadata,
prefer_reading_existing_pickle=prefer_reading_existing_pickle,
compress_pickle_with_lzma=compress_pickle_with_lzma,
autoplot_tally_output=autoplot_tally_output)
if tally_output is not None:
tally_output_pickle_path_list.append(path_to_pickle_file)
tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1])
if tot_num_values > max_num_values_to_plot:
tally_include_in_plotting_list.append(False)
if autoplot_all_tally_output_in_dir and not autoplot_tally_output: # only print this message if not already printed
print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values,
' elements), deemed too large for automatic plotting (default max of', max_num_values_to_plot, 'elements).')
else:
tally_include_in_plotting_list.append(True)
if return_tally_output or (merge_tally_outputs and not save_output_pickle and autoplot_all_tally_output_in_dir):
tally_output_list.append(tally_output)
if merge_tally_outputs:
if operating_in_directory_mode:
key = str(Path(f).relative_to(tally_output_dirpath))
else:
key = tally_output['tally_metadata']['file']
merged_outputs_dict[key] = tally_output
if include_dump_files:
for f in dump_filepaths_to_process:
f = Path(f)
parse_tally_dump_file(f, dump_data_number=dump_data_number, dump_data_sequence=dump_data_number,
return_directional_info=dump_return_directional_info, use_degrees=dump_use_degrees,
max_entries_read=dump_max_entries_read,
return_namedtuple_list=False, return_Pandas_dataframe=False,
save_namedtuple_list=dump_save_namedtuple_list,
save_Pandas_dataframe=dump_save_Pandas_dataframe,
prefer_reading_existing_pickle=prefer_reading_existing_pickle,
split_binary_dumps_over_X_GB=split_binary_dumps_over_X_GB,
merge_split_dump_handling=merge_split_dump_handling)
if dump_merge_MPI_subdumps:
for id in MPI_subdump_filestems_found: # identified independent tallies with dumps
dumps_to_merge = []
id_head, id_tail = os.path.split(id)
for f in MPI_subdump_files_found:
head, tail = os.path.split(f)
tail = tail[:-1 * (1 + len(tail.split('.')[-1]))]
if tail == id_tail:
dumps_to_merge.append(f)
merged_fp = Path(Path(id_head),id_tail)
merge_succeeded = merge_dump_file_pickles(dumps_to_merge, merged_dump_base_filepath=merged_fp,
delete_pre_merge_pickles=dump_delete_MPI_subdumps_post_merge)
if merge_tally_outputs and save_pickle_of_merged_tally_outputs:
# created dictionary of merged tally outputs
import pickle, lzma
merged_pickle_path = Path(tally_output_dirpath, merged_output_pickle_filename)
if compress_pickle_with_lzma:
merged_pickle_path = Path(merged_pickle_path.parent, merged_pickle_path.name + '.xz')
with lzma.open(merged_pickle_path, 'wb') as handle:
pickle.dump(merged_outputs_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
else:
with open(merged_pickle_path, 'wb') as handle:
pickle.dump(merged_outputs_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', merged_pickle_path, '\n')
if not return_tally_output:
del merged_outputs_dict # In the event this is huge in memory, best to delete it here before the Pandas DF is made
if autoplot_all_tally_output_in_dir:
if not make_PandasDF:
print('Plotting via "autoplot_tally_output=True" or "autoplot_all_tally_output_in_dir=True" requires also setting "make_PandasDF=True".')
else:
print('Compiling PDF of plots...')
plot_filepath = Path(tally_output_dirpath, autoplot_dir_filename)
plot_list = []
for i in range(len(tally_include_in_plotting_list)):
if tally_include_in_plotting_list[i]:
if return_tally_output or (merge_tally_outputs and not save_output_pickle):
plot_list.append(tally_output_list[i])
else:
plot_list.append(tally_output_pickle_path_list[i])
autoplot_tally_results(plot_list, plot_errorbars=calculate_absolute_errors, output_filename=plot_filepath)
if return_tally_output:
if merge_tally_outputs:
return merged_outputs_dict
else:
return tally_output_list
elif merge_tally_outputs and save_pickle_of_merged_tally_outputs and not save_output_pickle:
return merged_pickle_path
else:
return tally_output_pickle_path_list
def parse_phitsout_file(phitsout_filepath, include_input_echo=True, save_phitsout_pickle=False, compress_pickle_with_lzma=False):
'''
Description:
Extracts useful information from the "phits.out" file (`file(6)` in the PHITS [Parameters] section)
into a dictionary object, including the PHITS version number, time data (start/stop time, CPU time, MPI info, etc.),
event and particle details, papers to cite, and more. Sections taking the approximate form of a table are
converted to Pandas DataFrames before being stored in the dictionary object.
Dependencies:
- `import pandas as pd`
- `from munch import Munch` (will still run if package not found)
Inputs:
- `phitsout_filepath` = string or Path object denoting the path to the "phits.out" file to be parsed
- `include_input_echo` = (optional, D=`True`) Boolean indicating whether the "Input Echo" block of text
should be included in the output dictionary.
- `save_phitsout_pickle` = (optional, D=`False`) Boolean indicating whether a pickle file of the returned `phitsout_dict`
dictionary should be saved, named "phits_out.pickle[.xz]" and in the same directory as `phitsout_filepath`.
More specifically, the filename will be that of the "phits.out"-type file (`file(6)`) with "_out.pickle[.xz]" appended to it.
- `compress_pickle_with_lzma` = (optional, D=`False`; requires `save_phitsout_pickle=True`) Boolean designating
whether the pickle file to be saved will be compressed with
[LZMA compression](https://docs.python.org/3/library/lzma.html) (included in
the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the
pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`.
A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as:
`with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`.
For most "normal" PHITS runs, the pickle file sizes are likely to be small enough to not warrant
compression, but LZMA compression can reduce the file size by several orders of magnitude,
great for instances with very large geometry, materials, tally, etc. sections.
Outputs:
- `phitsout_dict` = a dictionary/Munch† object with various information/data extracted from the "phits.out" file.
It is organized with the following keys:
- `'job'` : a dictionary of information about the run (PHITS version, start/end time, CPU time, MPI info, etc.)
- `'summary'` : a dictionary of the various text tables in the "Summary for the end of job" section of the
phits.out file. The sections clearly formatted as tables are generally converted into Pandas DataFrames.
- `'memory'` : a string of the section of text in phits.out containing memory usage information
- `'batch'` : a string of the section of text in phits.out containing information on each batch
- `'input_echo'` : a string of the "Input Echo" text section of phits.out
- `'produced_files'` : a dictionary of the output files produced by this PHITS run's tallies, catergorized
with keys `'standard_output'` (list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects),
`'dump_output'` (list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects), and
`'phitsout'` (a single [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object);
this is the `files_dict` output returned by the `extract_tally_outputs_from_phits_input()` function.
- `'citation_request'` : a string of the "Citation Request" text section of phits.out listing additional papers
beyond the main PHITS reference paper that should be cited in any publications using results from
this simulation owing to using dependent models, codes, etc.
- `'extra_lines'` : a list of strings of any lines parsed by this function not classified as belonging to
any other section (and clearly containing information; i.e., not just blank or delimiter lines)
To conveniently view the contents of `phitsout_dict`, one can import the built-in
pprint library `import pprint` and then use `pprint.pp(dict(phitsout_dict))`.
†_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `phitsout_dict` dictionary
will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access.
If you do not have Munch, then `phitsout_dict` will just be a standard dictionary object.
If, for whatever reason, you have Munch installed but do not wish for
`phitsout_dict` to be a Munch object, then in the first line of code for the `parse_phitsout_file()` function,
set `prefer_to_munch_meta_dict = False`._
'''
prefer_to_munch_meta_dict = True
import datetime
import io
import pandas as pd
phitsout_filepath = Path(phitsout_filepath)
starting_dict = {'job':{},'summary':{},'memory':{},'batch':{}}
if prefer_to_munch_meta_dict:
try:
from munch import Munch
meta = Munch(starting_dict)
except:
meta = starting_dict
else:
meta = starting_dict
in_header = True
in_input_echo = False
in_footer = False
header_lines = []
input_echo = ''
footer_lines = []
# First, extract text from phits.out by section
with open(phitsout_filepath, mode='r', encoding="utf-8") as f:
for line in f:
if ">>> Input Echo >>>=============================================================" in line:
in_header = False
in_input_echo = True
continue
if "[END] of Input Echo <<<========================================================" in line:
in_input_echo = False
in_footer = True
continue
if in_header:
header_lines.append(line)
elif in_input_echo:
input_echo += line
elif in_footer:
footer_lines.append(line)
# Extract info from header
for li, line in enumerate(header_lines):
if 'Version =' in line:
key, value = extract_data_from_header_line(line.replace('|','').strip())
meta['job']['PHITS_version'] = value
elif '[ Job Title ]' in line:
meta['job']['title'] = header_lines[li+2].replace('|','').strip()
elif 'Starting Date' in line:
key, date = extract_data_from_header_line(line.replace('|', '').strip())
year, month, day = [int(i) for i in date.split('-')]
key, time = extract_data_from_header_line(header_lines[li+1].replace('|', '').strip())
hour, minute, second = [int(i) for i in time.replace('h ',':').replace('m ',':').split(':')]
meta['job']['datetime_start'] = datetime.datetime(year, month, day, hour, minute, second)
meta['job']['datetime_start_ISO_str'] = meta['job']['datetime_start'].isoformat()
elif 'Total PE =' in line:
key, pe = extract_data_from_header_line(line.replace('|', '').strip())
meta['job']['MPI_total_num_PE'] = pe
meta['job']['MPI_executable_num_PE'] = pe - 1
# input echo
if include_input_echo:
meta['input_echo'] = input_echo
# produced files
meta['produced_files'] = extract_tally_outputs_from_phits_input({'sim_base_dir_path':phitsout_filepath.parent,
'input_echo':input_echo},
use_path_and_string_mode=True)
# Extract info from footer
extra_lines = [] # extra footer lines not added to any specific dictionary entry
skip_lines = 0
memory_report = ''
batch_report = ''
citation_request = ''
line_of_dashes = '-------------------------------------------------------------------------------'
for li, line in enumerate(footer_lines):
if skip_lines>0:
skip_lines -= 1
continue
if '<<< Report of' in line and 'memory' in line:
for eli in range(5):
memory_report += footer_lines[li + eli]
skip_lines = 4
elif line[:4] == 'bat[':
for eli in range(5):
batch_report += footer_lines[li + eli]
skip_lines = 4
elif line[:-1] == line_of_dashes and footer_lines[li + 2][:-1] == line_of_dashes: # encountered table
in_special_events_table, in_special_MPI_table = False, False
if "CPU time and number of event called in PHITS" in footer_lines[li + 1][:-1]:
in_special_events_table = True
# get CPU time
total_cpu_time_sec = float(footer_lines[li + 5][:-1].split('=')[1].strip())
meta['job']['CPU_time_sec_int'] = total_cpu_time_sec
meta['job']['CPU_time_datetime_object'] = datetime.timedelta(seconds=total_cpu_time_sec)
meta['job']['CPU_time_str'] = str(meta['job']['CPU_time_datetime_object'])
elif "Final Parallel Status:" in footer_lines[li + 1][:-1]:
in_special_MPI_table = True
table_text = ''
table_title = footer_lines[li + 1][:-1]
column_header_line = table_title
nsl = 0 # number of lines to skip in table content
table_keyname = table_title.strip().replace(' ','_')
if "number of analyz call vs ncol" in table_title:
column_header_line = footer_lines[li + 3][:-1] # + ' description'
nsl = 1
table_keyname = 'analyz_calls_per_ncol'
elif "List of transport particles" in table_title:
column_header_line = footer_lines[li + 3][:-1]
nsl = 1
table_keyname = 'transport_particles'
elif "CPU time and number of event called in PHITS" in table_title:
column_header_line = ' event count'
nsl = 6
table_keyname = 'events_and_models'
reassign_key_to_first_col_name = False
if column_header_line == table_title:
reassign_key_to_first_col_name = True
column_header_line = column_header_line.replace('. p','._p')
column_header_line = column_header_line.replace('source:','')
col_headers_with_spaces = ['weight per source', 'average weight', 'total source']
for x in col_headers_with_spaces:
column_header_line = column_header_line.replace(x,x.replace(' ','_'))
line_len = len(line)
tesli = 3 + nsl
eli = 0
next_line = footer_lines[li + tesli + eli + 1]
while len(next_line)>1 or eli==0:
this_line = footer_lines[li + tesli + eli]
next_line = footer_lines[li + tesli + eli + 1]
if len(this_line)>1:
if in_special_events_table:
this_line = this_line.replace('=',' ')
this_line = this_line.replace('photonucl lib', 'photonucl_lib')
this_line = this_line.replace('frag data', 'frag_data')
table_text += this_line #.rstrip() + '\n' #.replace(':','')
eli += 1
if in_special_events_table:
if len(next_line)<2 and '>>>' not in next_line and '---' not in next_line and '===' not in next_line:
this_line = footer_lines[li + tesli + eli]
next_line = footer_lines[li + tesli + eli + 1]
eli += 1
if '>>>' in next_line or '---' in next_line or '===' in next_line:
eli -= 1
break
if in_special_MPI_table:
# also grab final MPI stats
eli += 1
while len(footer_lines[li + tesli + eli]) > 1:
key, value = extract_data_from_header_line(footer_lines[li + tesli + eli])
key = 'MPI_' + key.strip().replace(' ','_').replace('time','time_sec')
meta['job'][key] = value
eli += 1
skip_lines = tesli + eli
if '=' not in table_text:
table_text = column_header_line + '\n' + table_text
table_df = pd.read_csv(io.StringIO(table_text.replace(',','.')), comment=':', sep='\s+', on_bad_lines='skip')
if ':' in table_text:
descriptions = [x.split(':', 1)[-1] for x in table_text.split('\n')[1:-1]]
table_df['description'] = descriptions
if reassign_key_to_first_col_name:
if 'source: maxcas' in table_title:
table_keyname = 'source'
else:
table_keyname = table_df.columns[0]
#print(table_text)
#print(table_df.to_string())
meta['summary'][table_keyname + '_df'] = table_df
meta['summary'][table_keyname + '_text'] = table_text
else:
table_text = column_header_line + '\n' + table_text
meta['summary'][table_keyname + '_text'] = table_text
else:
if 'job termination date' in line:
date = line.split(':')[1].strip()
year, month, day = [int(i) for i in date.split('/')]
time = footer_lines[li + 1].split(':',1)[1].strip()
hour, minute, second = [int(i) for i in time.split(':')]
meta['job']['datetime_end'] = datetime.datetime(year, month, day, hour, minute, second)
meta['job']['datetime_end_ISO_str'] = meta['job']['datetime_start'].isoformat()
skip_lines = 1
elif '>>> Citation Request >>>' in line:
citation_request += line
in_citation_request = True
eli = 1
while in_citation_request:
this_line = footer_lines[li + eli]
if this_line.strip() == line_of_dashes or this_line.strip() == 'END':
break
citation_request += this_line
eli += 1
skip_lines = eli - 1
meta['citation_request'] = citation_request
elif 'initial random seed:' in line:
meta['job']['initial_random_seed_bitrseed'] = footer_lines[li + 1].replace('bitrseed =','').strip()
meta['job']['next_initial_random_seed_bitrseed'] = footer_lines[li + 3].replace('bitrseed =', '').strip()
skip_lines = 4
else:
extra_lines.append(line)
meta['memory']['report_text'] = memory_report
meta['batch']['report_text'] = batch_report
# Clean up extra lines
extra_lines_cleaned = []
for line in extra_lines:
if len(line.strip()) == 0: continue
if line.strip() == line_of_dashes: continue
if 'Summary for the end of job' in line: continue
if line.strip() == 'END': continue
extra_lines_cleaned.append(line)
meta['extra_lines'] = extra_lines_cleaned
phitsout_dict = meta
if save_phitsout_pickle:
import pickle, lzma
pickle_path = Path(phitsout_filepath.parent, phitsout_filepath.stem + '_out.pickle')
if compress_pickle_with_lzma:
pickle_path = Path(pickle_path.parent, pickle_path.name + '.xz')
with lzma.open(pickle_path, 'wb') as handle:
pickle.dump(phitsout_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
else:
with open(pickle_path, 'wb') as handle:
pickle.dump(phitsout_dict, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', pickle_path, '\n')
return phitsout_dict
def tally(data, bin_edges=[], min_bin_left_edge=None, max_bin_right_edge=None, nbins=None, bin_width=None, divide_by_bin_width=False, normalization=None, scaling_factor=1, place_overflow_at_ends=True, return_uncertainties=False, return_event_indices_histogram=False):
'''
Description:
Tally number of incidences of values falling within a desired binning structure
Inputs:
- `data` = list of values to be tallied/histogrammed
- `bin_edges` = list of N+1 bin edge values for a tally of N bins
- `min_bin_left_edge` = left/minimum edge value of the first bin
- `max_bin_right_edge` = right/maximum edge value of the last bin
- `nbins` = number of equally-sized bins to be created from `min_bin_left_edge` to `max_bin_right_edge`
- `bin_width` = constant width of bins to be created from `min_bin_left_edge` to `max_bin_right_edge`
- `divide_by_bin_width` = Boolean denoting whether final bin values are divided by their bin widths (D=`False`)
- `normalization` = determine how the resulting histogram is normalized (D=`None`), options are:
`[None, 'unity-sum', 'unity-max-val']`. If `None`, no additional normalization is done.
If `unity-sum`, the data is normalized such that its sum will be 1. If `unity-max-val`, the
data is normalized such that the maximum value is 1. The operation occurs after any bin
width normalization from `divide_by_bin_width` but before any scaling from `scaling_factor`.
- `scaling_factor` = value which all final bins are multiplied/scaled by (D=`1`)
- `place_overflow_at_ends` = handling of values outside of binning range (D=`True`); if `True` extreme
values are tallied in the first/last bin, if `False` extreme values are discarded
- `return_uncertainties` = Boolean denoting if should return an extra N-length list whose elements
are the statistical uncertainties (square root w/ normalizations) of the tally bins (D=`False`)
- `return_event_indices_histogram` = Boolean denoting if should return an extra N-length list whose elements
are each a list of the event indices corresponding to each bin (D=`False`)
Notes:
Regarding the binning structure, this function only needs to be provided `bin_edges` directly (takes priority)
or the information needed to calculate `bin_edges`, that is: `min_bin_left_edge` and `max_bin_right_edge` and
either `nbins` or `bin_width`. (Priority is given to `nbins` if both are provided.)
Outputs:
- `tallied_hist` = N-length list of tallied data
- `bin_edges` = list of N+1 bin edge values for a tally of N bins
- `tallied_hist_err` = (optional) N-length list of statistical uncertainties of tallied data
- `tallied_event_indicies` = (optional) N-length list of, for each bin, a list of the event indices populating it
'''
normalization_valid_entries = [None, 'unity-sum', 'unity-max-val']
if normalization not in normalization_valid_entries:
print("Entered normalization option of ",normalization," is not a valid option; please select from the following: [None, 'unity-sum', 'unity-max-val']".format())
if len(bin_edges)!=0:
bin_edges = np.array(bin_edges)
else:
if nbins != None:
bin_edges = np.linspace(min_bin_left_edge,max_bin_right_edge,num=nbins+1)
else:
bin_edges = np.arange(min_bin_left_edge,max_bin_right_edge+bin_width,step=bin_width)
nbins = len(bin_edges) - 1
if return_event_indices_histogram:
tallied_event_indicies = []
tallied_hist = np.zeros(nbins)
for i in range(nbins):
tallied_event_indicies.append([])
# events must be histogrammed manually
for i, val in enumerate(data):
if val < bin_edges[0]:
if place_overflow_at_ends:
tallied_hist[0] += 1
tallied_event_indicies[0].append(i)
continue
if val > bin_edges[-1]:
if place_overflow_at_ends:
tallied_hist[-1] += 1
tallied_event_indicies[-1].append(i)
continue
for j, be in enumerate(bin_edges):
if be > val: # found right edge of bin containing val
tallied_hist[j-1] += 1
tallied_event_indicies[j-1].append(i)
break
else:
tallied_hist, bins = np.histogram(data,bins=bin_edges)
if return_uncertainties:
tallied_hist_err = np.sqrt(tallied_hist)
if divide_by_bin_width: tallied_hist_err = tallied_hist_err/(bin_edges[1:]-bin_edges[:-1])
if normalization=='unity-sum': tallied_hist_err = tallied_hist_err/np.sum(tallied_hist)
if normalization=='unity-max-val': tallied_hist_err = tallied_hist_err/np.max(tallied_hist)
if scaling_factor != 1: tallied_hist_err = tallied_hist_err*scaling_factor
if divide_by_bin_width: tallied_hist = tallied_hist/(bin_edges[1:]-bin_edges[:-1])
if normalization=='unity-sum': tallied_hist = tallied_hist/np.sum(tallied_hist)
if normalization=='unity-max-val': tallied_hist = tallied_hist/np.max(tallied_hist)
if scaling_factor != 1: tallied_hist = tallied_hist*scaling_factor
if return_event_indices_histogram:
if return_uncertainties:
return tallied_hist,bin_edges,tallied_hist_err,tallied_event_indicies
else:
return tallied_hist,bin_edges,tallied_event_indicies
else:
if return_uncertainties:
return tallied_hist,bin_edges,tallied_hist_err
else:
return tallied_hist,bin_edges
def rebinner(output_xbins,input_xbins,input_ybins):
"""
Description:
The purpose of this function is to rebin a set of y values corresponding to a set of x bins to a new set of x bins.
The function seeks to be as generalized as possible, meaning bin sizes do not need to be consistent nor do the
new bin edges necessarily need to line up exactly with the old bin edges.
Dependencies:
`import numpy as np`
Inputs:
- `output_xbins` = output array containing bounds of x bins of length N; first entry is leftmost bin boundary
- `input_xbins` = input array containing bounds of x bins of length M; first entry is leftmost bin boundary
- `input_ybins` = input array containing y values of length M-1
Outputs:
- `output_ybins` = output array containing y values of length N-1
"""
N = len(output_xbins)
M = len(input_xbins)
output_ybins = np.zeros(N-1)
for i in range(0,N-1):
# For each output bin
lxo = output_xbins[i] # lower x value of output bin
uxo = output_xbins[i+1] # upper x value of output bin
dxo = uxo - lxo # width of current x output bin
# Scan input x bins to see if any fit in this output bin
for j in range(0,M-1):
lxi = input_xbins[j] # lower x value of input bin
uxi = input_xbins[j+1] # upper x value of input bin
dxi = uxi - lxi # width of current x input bin
if uxi<lxo or lxi>uxo:
# no bins are aligned
continue
elif lxi >= lxo and lxi < uxo:
# start of an input bin occurs in this output bin
if lxi >= lxo and uxi <= uxo:
# input bin completely encompassed by output bin
output_ybins[i] = output_ybins[i] + input_ybins[j]
else:
# input bin spans over at least one output bin
# count fraction in current output x bin
f_in_dxo = (uxo-lxi)/dxi
output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j]
elif lxi < lxo and uxi > uxo:
# output bin is completely encompassed by input bin
f_in_dxo = (uxo-lxo)/dxi
output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j]
elif lxi < lxo and uxi > lxo and uxi <= uxo:
# tail of input bin is located in this output bin
f_in_dxo = (uxi-lxo)/dxi
output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j]
return output_ybins
def autoplot_tally_results(tally_output_list,plot_errorbars=True,output_filename='results.pdf',
additional_save_extensions=[],show_plots=False,return_fg_list=False,
max_num_values_to_plot=1e7,rasterizesize_threshold=5e4,rasterize_dpi=300):
'''
Description:
Generates visualizations/plots of the data in the output Pandas DataFrames from the `parse_tally_output_file()`
function in an automated fashion. Note that this function only seeks to accomplish exactly this.
It is not a function for generating customized plots; it exists to automate creating visualizations of PHITS
output using sets of predetermined rules and settings, principally for initial checking of results.
Generally, it does not respect plotting-relevant settings provided to PHITS tallies (e.g., `samepage`, `axis`, `angel`, etc.),
though it will use the `title` parameter and the plot axis titles for ANGEL included in the .out files, which are
influenced by some tally parameters, such as `unit` and `y-txt`.
This function seeks to compile plots of results from one or multiple tallies into a single PDF file (and individual files in other image formats).
The [seaborn](https://seaborn.pydata.org/) package's [relplot](https://seaborn.pydata.org/generated/seaborn.relplot.html) function is used for generating these plots.
This function is primarily intended to be called by `parse_tally_output_file()` and `parse_all_tally_output_in_dir()`.
However, if you wish to make modifications to the automatically generated figures, you can use the `return_fg_list=True` setting
and apply your desired modifications to the returned FacetGrid objects. (This is demonstrated in the
[example](https://github.com/Lindt8/PHITS-Tools/tree/main/example) distributed with PHITS Tools.)
Dependencies:
- `import seaborn as sns`
- `import pandas as pd`
- `import matplotlib.pyplot as plt`
- `from matplotlib.colors import LogNorm, SymLogNorm, Normalize`
- `from matplotlib.backends.backend_pdf import PdfPages`
Inputs:
- `tally_output_list` = the `tally_output` output from the `parse_tally_output_file()` function, a string/Path
object pointing to the pickle file of such output, or a list of such outputs or pickle filepaths.
- `plot_errorbars` = (optional, D=`True`, requires `calculate_absolute_errors=True` to have been set in the
`parse_tally_output_file()` call producing the `tally_output`) Boolean determining if errorbars will be
displayed in plots. Note that owing to shortcomings in the [seaborn](https://seaborn.pydata.org/) package's
handling of error bars (i.e., not supporting externally calculated error bars) that a workaround has
instead been implemented but is only functional for "line"-type and "2D"-type plots.
- `output_filename` = (optional, D=`results.pdf`) String or Path object designating the name/path where the
PDF of plots will be saved.
- `additional_save_extensions` = (optional, D=`[]`) a list of strings of file extensions, e.g.,
`['.png', '.svg']` compatible with [matplotlib.savefig](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html)
to also save the plots as.
(options include '.eps', '.jpg', '.jpeg', '.pdf', '.pgf', '.png', '.ps', '.raw', '.rgba', '.svg', '.svgz', '.tif', '.tiff', and '.webp';
see `plt.gcf().canvas.get_supported_filetypes()`)
- `show_plots` = (optional, D=`False`) Boolean denoting whether this function will make a `plt.show()` call
immediately before the `return` statement.
- `return_fg_list` = (optional, D=`False`) Boolean denoting whether a list of the generated seaborn FacetGrid
objects returned by the sns.relplot() calls should be returned by this function, allowing modification
of the automatically generated plots. (Strictly speaking, the objects are copies of the FacetGrid objects
made using the built-in [copy.deepcopy()](https://docs.python.org/3/library/copy.html#copy.deepcopy) function.) If `False`, this function returns `None`.
- `max_num_values_to_plot` = (optional, D=`1e7`) integer denoting the maximum number of data points to be
plotted by this function for a single tally output, which, when exceeded, will cause the function to
skip creating the plot†.
The number of data points to be plotted is calculated as the product of the axis lengths of the
`tally_output['tally_data']` Numpy array (excluding the final axis for values/errors).
- `rasterizesize_threshold` = (optional, D=`5e4`) integer denoting the maximum number of data points to be
plotted by this function for a single tally output before setting `rasterized=True` to the `sns.relplot()`
calls generating plots‡.
The number of data points to be plotted is calculated as the product of the axis lengths of the
`tally_output['tally_data']` Numpy array (excluding the final axis for values/errors).
- `rasterize_dpi` = (optional, D=`300`) integer denoting the DPI to be used in the `savefig()` calls when
the `rasterizesize_threshold` is exceeded or when `additional_save_extensions` is used to save a plot
in a non-vectorized format.
Outputs:
- `None` if `return_fg_list=False` (default) or `fg_list` if `return_fg_list=True`; see description of the `return_fg_list` input argument
- (and the saved file(s) of plot(s) specified by `output_filename`)
--------
Notes:
All plots generated by this function will be one of three types:
- 'scatter' = a scatterplot (error bars not available)
- 'line' = a line plot (error bars available)
- 'pseudo 2D' = a 2D color image plot (relative errors displayed as an additional 2D plot)
How the plot type is chosen and structured is dependent on the number of plotting axes found and their types.
The number of plotting axes is taken as the count of axes in the `tally_output['tally_data']` Numpy array of
length greater than 1 (excluding the final axis for values/errors). Each axis is categorized as being either
'numerical' (energy, time, angle, radius, x, etc.) or 'categorical' (particle, region number, etc.) in nature.
If all plotting axes are categorical in nature, then the plot type will be of 'scatter' type. Otherwise, the
longest numerical axis is taken to be the horizontal plotting axis on either the 'line' or 'pseudo 2D' plot.
If the next longest numerical axis has length >6 (or, in the event the total number of plot axes is 4 or greater,
catergorical axes are also considered in this step), the plot type will be 'pseudo 2D' with that
axis taken as the vertical axis; otherwise, the plot type will be 'line' with `'value'` as the vertical axis.
The seaborn relplot `hue`, `style`, `row`, `col`, and `size` variables, loosely in that order, are assigned to
the next longest axes in order of descending length (except for the 'pseudo 2D' plots, where `hue` is used for
`'value'` and `style` and `size` are unused).
For the 'scatter' and 'line' plots, `hue` and `style` are typically assigned to the same axis for better visual distinction.
The 'pseudo 2D' plots are called "pseudo" here as they are still made with the seaborn replot function, which
actually does not support making this type of plot. The 2D plots are achieved by using the [matplotlib.markers "verts"](https://matplotlib.org/stable/api/markers_api.html)
functionality to make markers of the correct aspect ratio to tile together nicely when scaled up (or down)
sufficiently in size `s` to form the illusion of a 2D colormap plot, hence the "pseudo" in the name.
†This function also calculates the total number of values (bins) in the tally output to be plotted, the product
of the axis lengths of the `tally_output['tally_data']` Numpy array (excluding the final axis for values/errors).
If this value exceeds 10 million (controlled by the `max_num_values_to_plot` input in this function),
this function will skip attempting to make the plot. This limit is set to avoid potential crashes from
insufficient memory in scenarios where these plots are unlikely to actually be desired anyways.
‡Furthermore, if the total number of values exceeds 50 thousand (controlled by the `rasterizesize_threshold` input
in this function), rasterization will be employed in the plotting area (passing `rasterized = True` to
the `sns.relplot()` call) using a default DPI of 300 (controlled by the `rasterize_dpi` input in this function).
'''
'''
Behavior of this function within PHITS Tools:
- The option to execute this function on each produced output should be available for both `parse_tally_output_file()`
and `parse_all_tally_output_in_dir()` (one PDF file per tally output file)
- Also for `parse_all_tally_output_in_dir()` should be an option to execute this function on ALL tally outputs
handled by the dir parsing function (combining all plots into a single PDF)
- By default, both of these options should be disabled (set False) by default when PHITS Tools is used as an imported module.
- For GUI/CLI usage, checkboxes/flags for these three options should appear
- For GUI/CLI usage of `parse_tally_output_file()`, use of this function should be enabled/disabled by default.
- For GUI/CLI usage of `parse_all_tally_output_in_dir()`, use of this function should be enabled/disabled by default
only for combining all output in a directory; making individual PDFs of each handled tally output should
be disabled/disabled by default.
'''
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm, SymLogNorm, Normalize
from matplotlib.backends.backend_pdf import PdfPages
import pickle, lzma
import datetime
import copy
#max_num_values_to_plot = 1e7 # if the number of data points to plot exceeds this, plotting is skipped for that tally
#rasterizesize_threshold = 5e4 # use "rasterize" option if the number of data points to be plotted exceeds this
#rasterize_dpi = 300 # if using rasterization, use this DPI
fig_aspect_ratio = 1.414 # width / height
fig_height = 4 # inches
figi = 10000 # figure number
fg_list = [] # list of generated Seaborn FacetGrid objects returned by the sns.relplot() calls
output_filename = Path(output_filename)
# Convert provided input to a list of `tally_output` dictionary objects
if isinstance(tally_output_list, dict): # single tally output, tally output object
tally_output_list = [tally_output_list]
elif not isinstance(tally_output_list, list): # single tally output, pickle file of tally output object
p = Path(tally_output_list)
tally_output = pickle.load(lzma.open(p, 'rb') if p.name[-3:] == '.xz' else open(p, 'rb'))
tally_output_list = [tally_output]
else: # list of tally output objects and/or pickles
for i, to in enumerate(tally_output_list): # open any pickle files provided
if not isinstance(to, dict):
if to is None:
print("WARNING: 'None' tally output encountered in tally_output_list in autoplot_tally_results()!")
continue
p = Path(to)
tally_output_list[i] = pickle.load(lzma.open(p, 'rb') if p.name[-3:] == '.xz' else open(p, 'rb'))
plotting_multiple_tallies = len(tally_output_list) > 1
def are_bins_linearly_spaced(bin_mids):
'''
Return True/False designating whether bin centers appear to be linearly spaced or not
'''
diff = np.diff(bin_mids).round(decimals=8)
vals, counts = np.unique(diff, return_counts=True)
counts, vals = zip(*sorted(zip(counts, vals), reverse=True))
if 0 in vals:
izero = vals.index(0)
counts, vals = list(counts), list(vals)
counts.pop(izero)
vals.pop(izero)
if len(vals) <= 1: # all diff values are the same
bins_are_lin_spaced = True
else:
if counts[0]/sum(counts) > 0.5: # most frequent difference is more than half of all differences
bins_are_lin_spaced = True
else:
bins_are_lin_spaced = False
return bins_are_lin_spaced
tex_replace_strs = {'^2':'$^2$', '^3':'$^3$', r'\sigma':r'$\sigma$', r'\Omega':r'$\Omega$'}
with PdfPages(output_filename) as pdf:
for toi, tally_output in enumerate(tally_output_list):
tally_data, tally_metadata, tally_dataframe = [tally_output[k] for k in tally_output.keys()]
# determine number of plots necessary
ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tally_data)
if ierr_max==2 or ierr_max==4: plot_errorbars = False
special_tcross_case = False
if ierr_max>3: special_tcross_case = True
if special_tcross_case:
df_cols = tally_dataframe.columns.values.tolist()
#print(df_cols)
df1 = tally_dataframe.copy().drop(['r_surf', 'z_mid', 'value2', 'rel.err.2', 'abs.err.2'], axis=1)
df2 = tally_dataframe.copy().drop(['r_mid', 'z_surf', 'value', 'rel.err.', 'abs.err.'], axis=1).rename(columns={'value2':'value', 'rel.err.2':'rel.err.', 'abs.err.2':'abs.err.'})
#print(df1.columns.values.tolist())
#print(df2.columns.values.tolist())
tally_df_list = [df1, df2]
extra_df_fname_text = ['_rsurf-zmid', '_rmid-zsurf']
else:
tally_df_list = [tally_dataframe]
extra_df_fname_text = ['']
for tdfi, tally_df in enumerate(tally_df_list):
df_cols = tally_df.columns.values.tolist()
array_axes_lens = [ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max]
tot_plot_axes = sum(1 for i in array_axes_lens if i > 1)
tot_num_values = np.prod(array_axes_lens)
if tot_num_values > max_num_values_to_plot:
print('WARNING: Tally output for ',tally_metadata['file'],' is VERY LARGE (',tot_num_values,' elements), deemed too large for automatic plotting.')
if return_fg_list: fg_list.append(None)
continue
if tot_num_values > rasterizesize_threshold:
use_rasterization = True
else:
use_rasterization = False
if tot_plot_axes==0: # case where the tally is only scoring a single value
fig = plt.figure(figi,(fig_aspect_ratio*fig_height,fig_height))
plt.errorbar(1,tally_data[0,0,0,0,0,0,0,0,0,0],yerr=tally_data[0,0,0,0,0,0,0,0,0,2],marker='o')
plt.title(tally_metadata['tally_type'] + ', ' + tally_metadata['file'] + '\n' + tally_metadata['title'])
plt.ylabel('value')
plt.tight_layout()
# add PHITS Tools info
fontdict = {'color': '#666666', 'weight': 'normal', 'size': 8, 'style': 'italic'}
fig.text(0.005, 0.005, 'Figure generated by PHITS Tools, github.com/Lindt8/PHITS-Tools',
fontdict=fontdict, ha='left', va='bottom', url='https://github.com/Lindt8/PHITS-Tools')
pdf.savefig()
if return_fg_list: fg_list.append(copy.deepcopy(fig))
if not show_plots: plt.close()
figi += 1
continue
# We can divide axes into two categories:
# - categorical: 'reg', 'tet', 'point#', 'ring#', 'particle','nuclide', 'ZZZAAAM'
# - numerical: 'r_mid', 'r_surf', 'x_mid', 'y_mid', 'z_surf', 'z_mid', 'e_mid', 't_mid', 'a_mid', 'LET_mid', 'ic/Z/charge', 'ic/A/mass'
plot_axes = []
plot_axes_lens = []
plot_axes_ivars = []
num_axes = []
cat_axes = []
# assign all possible dimensions a "priority" score to compare for tiebreakers (dimensions of equal length)
ax_priority_vals = {'reg': 100, 'tet': 100, 'point#': 101, 'ring#': 101,
'r_mid': 100, 'r_surf': 101, 'x_mid': 100,
'y_mid': 90, 'z_mid': 80, 'z_surf': 81,
'e_mid': 110, 'e1_mid': 111,
't_mid': 70,
'a_mid': 60,
'LET_mid': 109,
'particle': 95,
'nuclide': 20, 'ZZZAAAM': 19,
'ic/Z/charge': 30, 'ic/A/mass': 30,
'#Interactions': 10, 'e2_mid': 109
}
cat_ax_to_index = {'reg': 'ir', 'tet': 'ir', 'point#': 'ir', 'ring#': 'ir',
'particle': 'ip',
'nuclide': 'ic', 'ZZZAAAM': 'ic'
}
# For the sake of making the plot labels nicer, also determine appropriate label for each axis
a_units = 'Angle [degrees]'
if 'a-type' in tally_metadata:
if tally_metadata['a-type']>0:
a_units = 'cos(Angle)'
elif tally_metadata['axis'] == 'rad':
a_units = 'Angle [radians]'
value_label_from_phits = 'value'
if tally_metadata['axis_dimensions'] == 1:
if tally_metadata['value_label'] != '':
value_label_from_phits = tally_metadata['value_label']
elif tally_metadata['axis2_label'] != '':
value_label_from_phits = tally_metadata['axis2_label']
else:
if tally_metadata['value_label'] != '':
value_label_from_phits = tally_metadata['value_label']
if value_label_from_phits != 'value':
for itx in tex_replace_strs:
value_label_from_phits = r'{}'.format(value_label_from_phits.replace(itx,tex_replace_strs[itx]))
ax_labels = {'reg':'Region #', 'tet':'Tet #', 'point#':'Point det #', 'ring#':'Ring det #',
'r_mid':'r [cm]', 'r_surf':'r_surface [cm]', 'x_mid':'x [cm]',
'y_mid':'y [cm]', 'z_mid':'z [cm]', 'z_surf':'z_surface [cm]',
'e_mid':'Energy [MeV]', 'e1_mid':'Energy 1 [MeV]',
't_mid':'Time [ns]',
'a_mid':a_units,
'LET_mid':r'LET [keV/$\mu$m]',
'particle':'Particle',
'nuclide':'Nuclide', 'ZZZAAAM':'ZZZAAAM',
'ic/Z/charge':'Z (proton #)', 'ic/A/mass':'A (mass #)',
'#Interactions':'Number of interactions', 'e2_mid':'Energy 2 [MeV]',
'value':value_label_from_phits, 'value2':value_label_from_phits,
'rel.err.':'relative error', 'rel.err.2':'relative error'
}
# now determine what axes are getting plotted
for i, leni in enumerate(array_axes_lens):
if leni==1: continue
if i==0: # 'reg', 'tet', 'point#', 'ring#', 'r_mid', 'r_surf', 'x_mid'
if any(a in ['reg', 'point#', 'ring#'] for a in df_cols): # categorical
col = [a for a in ['reg', 'point#', 'ring#'] if a in df_cols][0]
cat_axes.append(col)
else: # numerical
col = [a for a in ['r_mid', 'r_surf', 'x_mid', 'tet'] if a in df_cols][0]
num_axes.append(col)
if col=='tet': # convert tet number from string to float
tally_df['tet'] = tally_df['tet'].astype(float)
elif i==1: # 'y_mid'
col = 'y_mid'
num_axes.append(col)
elif i==2: # 'z_mid', 'z_surf'
col = [a for a in ['z_mid', 'z_surf'] if a in df_cols][0]
num_axes.append(col)
elif i==3: # 'e_mid'
col = [a for a in ['e_mid', 'e1_mid'] if a in df_cols][0]
num_axes.append(col)
elif i==4: # 't_mid'
col = 't_mid'
num_axes.append(col)
elif i==5: # 'a_mid'
col = 'a_mid'
num_axes.append(col)
elif i==6: # 'LET_mid'
col = 'LET_mid'
num_axes.append(col)
elif i==7: # 'particle'
col = 'particle'
cat_axes.append(col)
elif i==8: # 'nuclide', 'ZZZAAAM', 'ic/Z/charge', 'ic/A/mass', 'act'
if any(a in ['nuclide', 'ZZZAAAM'] for a in df_cols): # categorical
col = [a for a in ['nuclide', 'ZZZAAAM'] if a in df_cols][0]
cat_axes.append(col)
else: # numerical
col = [a for a in ['ic/Z/charge', 'ic/A/mass', '#Interactions', 'e2_mid'] if a in df_cols][0]
num_axes.append(col)
plot_axes.append(col)
plot_axes_lens.append(leni)
# sort the lists in order of descending lengths, with "higher priority" axes coming first when equal in length
plot_axes_sorted = []
plot_axes_lens_sorted = []
num_axes_sorted = []
num_lens_sorted = []
cat_axes_sorted = []
cat_lens_sorted = []
ax_props = [(plot_axes[i],plot_axes_lens[i],ax_priority_vals[plot_axes[i]]) for i in range(len(plot_axes))]
ax_props.sort(key=lambda x: (-x[1], -x[2])) # sort first by length then priority, both in descending order
for i,tup in enumerate(ax_props):
plot_axes_sorted.append(tup[0])
plot_axes_lens_sorted.append(tup[1])
if tup[0] in cat_axes:
cat_axes_sorted.append(tup[0])
cat_lens_sorted.append(tup[1])
else:
num_axes_sorted.append(tup[0])
num_lens_sorted.append(tup[1])
cat = cat_axes_sorted
num = num_axes_sorted
# Now determine how each axis will be represented in the plot
'''
How we represent the data will depend on:
- how many axes there are to represent
- the specific combination of A cat and B num
- whether the longest axes are cat or num
For the total number axes variables (r, y, z, e, t, a, l, p, and c) with length > 1:
[unless stated otherwise, y='value' for all options]
[charts become difficult to read if length of axis passed to hue and (especially) style are too long;
therefore, maxlen = 6 is set, meaning if this length is exceeded for an axis bound for hue/style,
instead it will be split in row/col and/or cause a shift from a 1D line plot to a 2D scatter plot.]
- 0 : set x to 1, y to 'value'
- 1 : 1 cat + 0 num : scatter(line?) : x = i_cat1, hue=style=cat1
- 1 : 0 cat + 1 num : line : x = num1
- 2 : 2 cat + 0 num : scatter(line?) : x = i_cat1, hue=cat1, style=cat2
- 2 : 1 cat + 1 num : line : x = num1, hue=style=cat1
- 2 : 0 cat + 2 num : line : x = num1, hue=style=num2 (suitable if len(num2) <= maxlen)
OR : scatter : x = num1, y = num2, hue='value' (increase marker size for pseudo 2D plot)
- 3 : 3 cat + 0 num : scatter(line?) : x = i_cat1, hue=cat1, style=cat2, row=cat3 (note: this scenario is extremely unlikely to happen)
- 3 : 2 cat + 1 num : line : x = num1, hue=cat1, style=cat2
- 3 : 1 cat + 2 num : line : x = num1, hue=num2, row=cat1 (suitable if len(num2) <= maxlen)
OR : scatter : x = num1, y = num2, hue='value', row=cat1
- 3 : 0 cat + 3 num : line : x = num1, hue=num2, row=num3 (suitable if len(num2) <= maxlen)
OR : scatter : x = num1, y = num2, hue='value', row=num3
- 4 : 3 cat + 1 num : line : x = num1, hue=cat1, style=cat2, row=cat3
- 4 : 2 cat + 2 num : line : x = num1, hue=num2, style=cat1, row=cat2 (suitable if len(num2) <= maxlen and len(cat1) <= maxlen)
OR : line : x = num1, hue=num2, row=cat1, col=cat2 (if len(num2) <= maxlen but len(cat1) > maxlen)
OR : scatter : x = num1, y = num2, hue='value', row=cat1, col=cat2
- 4 : 1 cat + 3 num : line : x = num1, hue=num2, style=cat1, row=num3 (suitable if len(num2) <= maxlen and len(cat1) <= maxlen)
OR : line : x = num1, hue=num2, row=num3, col=cat1 (if len(num2) <= maxlen but len(cat1) > maxlen)
OR : scatter : x = num1, y = num2, hue='value', row=num3, col=cat1
- 4 : 0 cat + 4 num : line : x = num1, hue=num2, style=num3, row=num4 (suitable if len(num2) <= maxlen and len(num3) <= maxlen)
OR : line : x = num1, hue=num2, row=num3, col=num4 (if len(num2) <= maxlen but len(num3) > maxlen)
OR : scatter : x = num1, y = num2, hue='value', row=num3, col=num4
- 5 : any 5 cat/num : line : x = num1, hue=num2, style=num5, row=num3, col=num4
- 6 : any 6 cat/num : line : x = num1, hue=num2, style=num5, size=num6, row=num3, col=num4
- 7+ : no plot will be generated, output will be skipped. If you have a tally this complex, you already have usage ideas.
'''
maxlen = 6 # if an axis length exceeds this, it may need to be reassigned to row/col or the plot type changed
maxlines = 16 # will modify plotting strategy if combining two axes on a line plot results in more lines than this
plot_kind = 'line' # 'line' or 'scatter'
y_var = 'value'
x_var = None
hue_var = None
style_var = None
size_var = None
row_var = None
col_var = None
pseudo_2d_plot = False
xvar_original_label = None
yvar_original_label = 'value'
if tot_plot_axes==0:
pass
elif tot_plot_axes==1:
if len(cat)==1: # 1 cat + 0 num
plot_kind = 'scatter'
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
hue_var = cat[0]
style_var = cat[0]
else: # 0 cat + 1 num
plot_kind = 'line'
x_var = num[0]
elif tot_plot_axes==2:
if len(cat)==2: # 2 cat + 0 num
plot_kind = 'scatter'
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
hue_var = cat[0]
style_var = cat[1]
elif len(cat)==1: # 1 cat + 1 num
plot_kind = 'line'
x_var = num[0]
hue_var = cat[0]
style_var = cat[0]
elif len(num)==2: # 0 cat + 2 num
if num_lens_sorted[1] <= maxlen:
plot_kind = 'line'
x_var = num[0]
hue_var = num[1]
style_var = num[1]
else:
pseudo_2d_plot = True
plot_kind = 'scatter'
x_var = num[0]
y_var = num[1]
hue_var = 'value'
elif tot_plot_axes==3:
if len(cat) == 3: # 3 cat + 0 num
plot_kind = 'scatter'
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
hue_var = cat[0]
style_var = cat[1]
row_var = cat[2]
elif len(cat) == 2: # 2 cat + 1 num
plot_kind = 'line'
x_var = num[0]
hue_var = cat[0]
style_var = cat[1]
elif len(cat) == 1: # 1 cat + 2 num
if num_lens_sorted[1] <= maxlen:
plot_kind = 'line'
x_var = num[0]
hue_var = num[1]
style_var = num[1]
row_var = cat[0]
else:
pseudo_2d_plot = True
plot_kind = 'scatter'
x_var = num[0]
y_var = num[1]
hue_var = 'value'
row_var = cat[0]
elif len(num) == 3: # 0 cat + 3 num
if num_lens_sorted[1] <= maxlen:
plot_kind = 'line'
x_var = num[0]
hue_var = num[1]
style_var = num[1]
row_var = num[2]
else:
pseudo_2d_plot = True
plot_kind = 'scatter'
x_var = num[0]
y_var = num[1]
hue_var = 'value'
row_var = num[2]
'''
if plot_axes_sorted[0] in num:
x_var = plot_axes_sorted[0]
else:
x_var = plot_axes_sorted[0]
#x_var = cat_ax_to_index[cat_axes_sorted[0]]
#cat.append(x_var)
style_var = plot_axes_sorted[0]
if plot_axes_lens_sorted[1] <= maxlen:
plot_kind = 'line'
hue_var = plot_axes_sorted[1]
if plot_axes_lens_sorted[1]*plot_axes_lens_sorted[2] <= maxlines:
style_var = plot_axes_sorted[2]
else:
if style_var==None: style_var = plot_axes_sorted[1]
row_var = plot_axes_sorted[2]
else:
pseudo_2d_plot = True
plot_kind = 'scatter'
if plot_axes_sorted[1] in num:
y_var = plot_axes_sorted[1]
else:
y_var = cat_ax_to_index[cat_axes_sorted[1]]
cat.append(y_var)
hue_var = 'value'
row_var = plot_axes_sorted[2]
'''
elif tot_plot_axes==4:
if plot_axes_sorted[0] in num:
x_var = plot_axes_sorted[0]
else:
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
style_var = plot_axes_sorted[0]
if plot_axes_lens_sorted[1] <= maxlen:
plot_kind = 'line'
hue_var = plot_axes_sorted[1]
if style_var==None: style_var = plot_axes_sorted[1]
row_var = plot_axes_sorted[2]
col_var = plot_axes_sorted[3]
else:
pseudo_2d_plot = True
plot_kind = 'scatter'
if plot_axes_sorted[1] in num:
y_var = plot_axes_sorted[1]
else:
y_var = cat_ax_to_index[cat_axes_sorted[1]]
yvar_original_label = cat_axes_sorted[1]
cat.append(y_var)
hue_var = 'value'
row_var = plot_axes_sorted[2]
col_var = plot_axes_sorted[3]
elif tot_plot_axes==5:
plot_kind = 'line'
if plot_axes_sorted[0] in num:
x_var = plot_axes_sorted[0]
else:
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
hue_var = plot_axes_sorted[1]
row_var = plot_axes_sorted[2]
col_var = plot_axes_sorted[3]
style_var = plot_axes_sorted[4]
elif tot_plot_axes==6:
plot_kind = 'line'
if plot_axes_sorted[0] in num:
x_var = plot_axes_sorted[0]
else:
x_var = cat_ax_to_index[cat_axes_sorted[0]]
xvar_original_label = cat_axes_sorted[0]
cat.append(x_var)
hue_var = plot_axes_sorted[1]
row_var = plot_axes_sorted[2]
col_var = plot_axes_sorted[3]
style_var = plot_axes_sorted[4]
size_var = plot_axes_sorted[5]
else:
print('Cannot create plot with 7+ variables...')
continue
'''
listed below are all possible column headers in the dataframe
'ir','reg','reg#', 'tet', 'point#', 'ring#'
'ip', 'particle', 'kf-code'
'ix', 'iy', 'iz', 'x_mid', 'y_mid', 'z_surf', 'z_mid'
'ir', 'r_mid', 'r_surf',
'ie','e_mid', 'it', 't_mid', 'ia', 'a_mid', 'il', 'LET_mid', 'e1_mid'
'ic', 'nuclide', 'ZZZAAAM', 'ic/Z/charge', 'ic/A/mass', '#Interactions', 'e2_mid'
'value', 'rel.err.', 'abs.err.'
'value2', 'rel.err.2', 'abs.err.2'
'''
for ierr in range(2): # for pseudo 2d plots, also make a rel.err. plot
if ierr==1 and not pseudo_2d_plot: continue
extra_fname_text = extra_df_fname_text[tdfi] + ['','_err'][ierr]
if plotting_multiple_tallies: print('\tplotting:',tally_metadata['file'], extra_fname_text)
if ierr==0:
hue_palette_name_str = "mako_r" # "cividis" # "rocket_r"
else:
hue_palette_name_str = "Reds" # "cividis" # "rocket_r"
if hue_var=='value':
hue_var = 'rel.err.'
elif hue_var=='value2':
hue_var = 'rel.err.2'
else:
continue
if x_var in cat: plot_kind='scatter'
if plot_kind=='scatter': plot_errorbars = False
if plot_errorbars:
# The following 3 lines are the "hack" to circumvent seaborn stupidly not supporting custom error values
duplicates = 100
df = tally_df.loc[tally_df.index.repeat(duplicates)].copy()
df['value'] = np.random.normal(df['value'].values, df['abs.err.'].values)
errorbar_arg = 'sd'
else:
df = tally_df.copy()
errorbar_arg = None
if ierr==1 and pseudo_2d_plot: df[hue_var] = df[hue_var].replace({'0': '1', 0: 1.0})
# Determine lin/log/symlog usage for axes, with the following rules:
# - default to linear
# - if axis is already evenly linearly binned, keep it linear
# - otherwise, log scale if max/min > maxratio, maxratio = 101
# - if log but dataset contains is not of all same sign , use symlog
x_norm, y_norm = 'linear', 'linear'
hue_norm, size_norm = None, None
maxratio = 101
if x_var not in cat:
x_min, x_max = df[x_var].abs().replace(0, np.nan).min(), df[x_var].abs().replace(0, np.nan).max()
bins_are_lin_spaced = are_bins_linearly_spaced(df[x_var].to_numpy())
if not bins_are_lin_spaced and x_max/x_min>maxratio:
if len(np.sign(df[x_var]).unique())>1:
x_norm = 'log' #'symlog'
else:
x_norm = 'log'
if y_var not in cat:
y_min, y_max = df[y_var].abs().replace(0, np.nan).min(), df[y_var].abs().replace(0, np.nan).max()
bins_are_lin_spaced = are_bins_linearly_spaced(df[y_var].to_numpy())
if not bins_are_lin_spaced and y_max/y_min>maxratio:
if len(np.sign(df[y_var]).unique())>1:
y_norm = 'log' # 'symlog'
else:
y_norm = 'log'
use_colorbar = False
if hue_var != None:
if hue_var not in cat:
hue_min, hue_max = df[hue_var].abs().replace(0, np.nan).min(), df[hue_var].abs().replace(0, np.nan).max()
bins_are_lin_spaced = are_bins_linearly_spaced(df[hue_var].to_numpy())
if not bins_are_lin_spaced and hue_max/hue_min>maxratio:
if len(np.sign(df[hue_var]).unique())>1:
hue_norm = SymLogNorm(linthresh=hue_min)
else:
hue_norm = LogNorm()
if pseudo_2d_plot:
use_colorbar = True
if hue_norm == None: hue_norm = Normalize()
if ierr == 1: hue_norm = Normalize(vmin=0, vmax=1)
cmap = sns.color_palette(hue_palette_name_str, as_cmap=True)
sm = plt.cm.ScalarMappable(cmap=cmap, norm=hue_norm)
if size_var != None and size_var not in cat:
size_min, size_max = df[size_var].abs().replace(0, np.nan).min(), df[size_var].abs().replace(0,np.nan).max()
bins_are_lin_spaced = are_bins_linearly_spaced(df[size_var].to_numpy())
if not bins_are_lin_spaced and size_max/size_min>maxratio:
if len(np.sign(df[size_var]).unique())>1:
size_norm = SymLogNorm(linthresh=size_min)
else:
size_norm = LogNorm()
# Right before plotting, let's rename Pandas DF columns to be nicer on plots
y_var_renamed = 'value'
x_var_renamed = None
hue_var_renamed = None
style_var_renamed = None
size_var_renamed = None
row_var_renamed = None
col_var_renamed = None
df_renamed = df.rename(columns=ax_labels)
if xvar_original_label != None:
x_var_new = ax_labels[xvar_original_label] + ' index'
df_renamed = df_renamed.rename(columns={x_var:x_var_new})
x_var_renamed = x_var_new
else:
x_var_renamed = ax_labels[x_var]
if yvar_original_label != 'value':
y_var_new = ax_labels[yvar_original_label] + ' index'
df_renamed = df_renamed.rename(columns={y_var:y_var_new})
y_var_renamed = y_var_new
else:
y_var_renamed = ax_labels[y_var]
if hue_var != None: hue_var_renamed = ax_labels[hue_var]
if style_var != None: style_var_renamed = ax_labels[style_var]
if size_var != None: size_var_renamed = ax_labels[size_var]
if row_var != None: row_var_renamed = ax_labels[row_var]
if col_var != None: col_var_renamed = ax_labels[col_var]
if in_debug_mode:
print('plot_kind=', plot_kind,
'\ny_var=', y_var, y_var_renamed,
'\nx_var=', x_var, x_var_renamed,
'\nhue_var=', hue_var, hue_var_renamed, '\nhue_norm=', hue_norm,
'\nstyle_var=', style_var, style_var_renamed,
'\nsize_var=', size_var, size_var_renamed,
'\nrow_var=', row_var, row_var_renamed,
'\ncol_var=', col_var, col_var_renamed,
'\npseudo_2d_plot=', pseudo_2d_plot)
print(df_renamed.columns.values)
if plot_kind == 'line':
fg = sns.relplot(data=df_renamed, kind=plot_kind, #num=figi,
height=fig_height, aspect=fig_aspect_ratio,
x=x_var_renamed, y=y_var_renamed,
hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed,
hue_norm=hue_norm, size_norm=size_norm,
row=row_var_renamed, col=col_var_renamed,
errorbar=errorbar_arg, legend='auto', # markers=False,
rasterized = use_rasterization,
facet_kws={"legend_out": True, }
)
else: # scatterplot
if pseudo_2d_plot:
num_cols = df_renamed[x_var_renamed].nunique()
num_rows = df_renamed[y_var_renamed].nunique()
# using horizontal direction
#fig_size_inch = fig_height * fig_aspect_ratio
#margin = 0.05 # 0.12
#n_markers = num_cols
#subplot_fraction = 0.7696385
# for a normal plot w/o rows/cols, widths are: fig=624.475, ax=535.807 (pre adjust) / 480.62 (post adjust), leg=58.875
if use_colorbar:
leg_arg = False
# using vertical direction
# these values are decent but could perhaps use better optimization
fig_size_inch = fig_height
margin = 0.06 # 0.2
n_markers = num_rows
subplot_fraction = 1 - 2 * margin
ar_ax_vs_fig_correction = 0.95
else:
leg_arg = 'auto'
# using vertical direction
fig_size_inch = fig_height
margin = 0.185 # 0.2
n_markers = num_rows
subplot_fraction = 1 - 2 * margin
ar_ax_vs_fig_correction = 1.3
ar_if_square = num_rows / num_cols
mar = fig_aspect_ratio * ar_if_square * ar_ax_vs_fig_correction # marker aspect ratio, width/height
verts = [[-mar, -1], [mar, -1], [mar, 1], [-mar, 1], [-mar, -1]]
fig_ppi = 72 # 300 is default save dpi, different from draw dpi
marker_size = (subplot_fraction * fig_size_inch * fig_ppi / n_markers) ** 2 # Size of the marker, in points^2
s = marker_size # universal marker size for plot
if num_rows <= 15: # adjust vertical label spacing in legend, if needed, before plotting
sns.set(style=None, rc={'legend.labelspacing': 1.2})
fg = sns.relplot(data=df_renamed, kind=plot_kind, # num=figi,
height=fig_height, aspect=fig_aspect_ratio,
x=x_var_renamed, y=y_var_renamed, s=s, linewidth=0, #edgecolor=None,
hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed,
hue_norm=hue_norm, size_norm=size_norm,
row=row_var_renamed, col=col_var_renamed,
legend=leg_arg,
palette=hue_palette_name_str,
marker=verts, alpha=1,
rasterized = use_rasterization,
#facet_kws={"legend_out": True, }
)
#fg.set(aspect='equal')
#cbar = plt.colorbar(sm, ax=plt.gca())
#cbar = fg.fig.colorbar(sm, ax=fg.axes[:, -1])
for col in range(np.shape(fg.axes[:,:])[1]):
for row in range(np.shape(fg.axes[:,:])[0]):
cbar = plt.colorbar(sm, ax=fg.axes[row,col])
cbar.set_label(hue_var_renamed)
else:
fg = sns.relplot(data=df_renamed, kind=plot_kind, # num=figi,
height=fig_height, aspect=fig_aspect_ratio,
x=x_var_renamed, y=y_var_renamed,
hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed,
hue_norm=hue_norm, size_norm=size_norm,
row=row_var_renamed, col=col_var_renamed,
legend='auto', s=100, alpha=1,
rasterized = use_rasterization,
facet_kws={"legend_out": True, }
)
fig, axes = fg.fig, fg.axes
legend_exists = not all(v is None for v in [hue_var, style_var, size_var, col_var, row_var])
if legend_exists and not pseudo_2d_plot:
leg = fg._legend
leg.set_bbox_to_anchor([1, 0.5]) # Move legend to far right
leg._loc = 5 # https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html#matplotlib.axes.Axes.legend
# later figure will be rescaled to accommodate the legend
title_str = tally_metadata['tally_type'] + ', ' + tally_metadata['file'] + '\n' + tally_metadata['title']
#xlabel_str = tally_metadata['axis1_label']
#ylabel_str = tally_metadata['value_label']
st = fg.fig.suptitle(title_str, fontsize=16, va='top', y=0.995)
#fg.fig.get_axes()[0].annotate(title_str, (0.5, 0.95), xycoords='figure fraction', ha='center', fontsize=16)
#fg.set_axis_labels(xlabel_str, ylabel_str)
for ax in fg.axes.flat:
ax.set_xscale(x_norm)
ax.set_yscale(y_norm)
ax.grid(which='both', linewidth=1, color='#EEEEEE', alpha=0.5)
#ax.set_facecolor((0, 0, 0, 0))
#ax.tick_params(labelbottom=True, labelleft=True)
#ax.set_xlabel(xlabel_str, visible=True)
#ax.set_ylabel(ylabel_str, visible=True)
#sns.move_legend(fg, "upper left", bbox_to_anchor=(1, 1))
# Issue 1: tight_layout is needed to not have stuff clipped off edge
# Issue 2: tight_layout causes the legend, if placed outside the plot area, to be cut out
# Soltion: draw canvas early, figure out their relative widths, then adjust the right edge of the figure
plt.tight_layout() # pad=3.0
if legend_exists and not pseudo_2d_plot:
fig.canvas.draw()
fig_width = fig.get_window_extent().width
leg_width = leg.get_window_extent().width
#print(fig_width,ax.get_window_extent().width,leg_width)
fg.fig.subplots_adjust(right=0.98*(fig_width-leg_width)/fig_width)
#print(fg.fig.get_window_extent().width, fg.ax.get_window_extent().width, leg.get_window_extent().width)
#if pseudo_2d_plot:
# fig.canvas.draw()
# fig_width = fig.get_window_extent().width
# #cbar_width = fg.fig.get_window_extent().width - fg.axes.get_window_extent().width
# #fg.fig.subplots_adjust(right=0.98 * (fig_width - cbar_width) / fig_width)
# fg.fig.subplots_adjust(right=0.98 * 0.8)
# add PHITS Tools info
fontdict = {'color':'#666666', 'weight':'normal', 'size': 8, 'style':'italic'}
fig.text(0.005,0.005,'Figure generated by PHITS Tools, github.com/Lindt8/PHITS-Tools',fontdict=fontdict,ha='left', va='bottom', url='https://github.com/Lindt8/PHITS-Tools')
if use_rasterization:
pdf.savefig(bbox_extra_artists=[st], dpi=rasterize_dpi)
else:
pdf.savefig(bbox_extra_artists=[st])
if return_fg_list: fg_list.append(copy.deepcopy(fg))
for ext in additional_save_extensions:
if '.' not in ext: ext = '.'+ext # assume user provides only the extension in event the . is missing
if ext.lower()=='.pdf': ext = '_.pdf'
img_save_path = Path(output_filename.parent, output_filename.stem + extra_fname_text + ext)
plt.savefig(img_save_path, bbox_extra_artists=[st], dpi=rasterize_dpi)
if not show_plots: plt.close(fg.fig)
figi += 1
d = pdf.infodict()
#d['Title'] = 'Multipage PDF Example'
d['Author'] = 'PHITS Tools'
d['Subject'] = 'https://github.com/Lindt8/PHITS-Tools/'
#d['Keywords'] = 'PdfPages multipage keywords author title subject'
d['CreationDate'] = datetime.datetime.today()
d['ModDate'] = datetime.datetime.today()
print('Plot PDF written:', output_filename, '\n')
if show_plots:
plt.show()
if return_fg_list:
return fg_list
else:
return None
def fetch_MC_material(matid=None,matname=None,matsource=None,concentration_type=None,particle=None):
'''
Description:
Returns a materials definition string formatted for use in PHITS or MCNP (including a density estimate);
most available materials are those found in [PNNL-15870 Rev. 1](https://www.osti.gov/biblio/1023125).
Dependencies:
- PYTHONPATH environmental variable must be set and one entry must contain the directory
which contains the vital "MC_materials/Compiled_MC_materials.pkl" file.
Inputs:
(required to enter `matid` OR `matname`, with `matid` taking priority if conflicting)
- `matid` = ID number in the "Compiled_MC_materials" file
- `matname` = exact name of material in "Compiled_MC_materials" file
- `matsource` = exact source of material in "Compiled_MC_materials" file, only used when multiple
materials have identical names
- `concentration_type` = selection between `'weight fraction'` (default if no chemical formula is present in database; e.g., "Incoloy-800") and
`'atom fraction'` (default if a chemical formula is present; e.g. "Ethane" (Formula = C2H6)) to be returned
- `particle` = selection of whether natural (`'photons'`, default) or isotopic (`'neutrons'`) elements are used
Note that if "enriched" or "depleted" appears in the material's name, particle=`'neutrons'` is set automatically.
Outputs:
- `mat_str` = string containing the material's information, ready to be inserted directly into a PHITS/MCNP input file
'''
import os
import pickle
if not matid and not matname:
print('Either "matid" or "matname" MUST be defined')
return None
# First, locate and open materials library
try:
user_paths = os.environ['PYTHONPATH'].split(os.pathsep)
lib_file = None
for i in user_paths:
if 'phits_tools' in i.lower() or 'phits-tools' in i.lower():
lib_file = i + r"\MC_materials\Compiled_MC_materials"
if not lib_file:
print('Could not find "PHITS_tools" folder in PYTHONPATH; this folder contains the vital "MC_materials/Compiled_MC_materials.pkl" file.')
except KeyError:
print('The PYTHONPATH environmental variable must be defined and contain the path to the directory holding "MC_materials/Compiled_MC_materials.pkl"')
return None
# Load materials library
def load_obj(name ):
with open(name + '.pkl', 'rb') as f:
return pickle.load(f)
all_mats_list = load_obj(lib_file)
if matid: # use mat ID number
mi = int(matid)-1
matname = all_mats_list[mi]['name']
else: # use material name and possibly source too
# determine material
mi = None
# first check for exact matches
matching_mi = []
for i in range(len(all_mats_list)):
if all_mats_list[i]['name'].lower()==matname.lower():
matching_mi.append(i)
if len(matching_mi)==1:
mi = matching_mi[0]
elif len(matching_mi)>1:
print('Found multiple materials with this identical matname value:')
for mmi in matching_mi:
print('\tmatid={} matname="{}" source="{}"'.format(str(mmi+1),all_mats_list[mmi]['name'],all_mats_list[mmi]['source']))
if all_mats_list[mmi]['source'] and all_mats_list[mmi]['source']==matsource:
mi = mmi
print('\t\t^ matches inputed "matsource" and will be used')
if mi==None:
print('Please enter a "matsource" value identical to one of these two (or the matid).')
return None
else: # Exact material name not found
# search for similar entries
similar_mi = []
for i in range(len(all_mats_list)):
if matname.lower() in all_mats_list[i]['name'].lower():
similar_mi.append(i)
if len(similar_mi)==0:
print('No materials with that exact name or names containing "matname" were found.')
return None
elif len(similar_mi)==1:
mi = similar_mi[0]
print('Found one similar material (matid={} matname="{}" source="{}"); using it.'.format(str(mi+1),all_mats_list[mi]['name'],all_mats_list[mi]['source']))
else:
print('Found no material with exact "matname" but {} with similar names:'.format(len(similar_mi)))
for smi in similar_mi:
print('\tmatid={} matname="{}" source="{}"'.format(str(smi+1),all_mats_list[smi]['name'],all_mats_list[smi]['source']))
print('The first of these will be used. If another material was desired, please enter its "matid" or exact "matname".')
mi = similar_mi[0]
# Now that material ID has been found, generate text entry
mat = all_mats_list[mi]
banner_width = 60
cc = '$' # comment character
entry_text = '\n'+cc+'*'*banner_width + '\n'
entry_text += cc+' {:<3d} : {} \n'.format(mi+1,mat['name'])
if mat['source'] and mat['source']!='-':
entry_text += cc+' Source = {} \n'.format(mat['source'])
if mat['formula'] and mat['formula']!='-':
entry_text += cc+' Formula = {} \n'.format(mat['formula'])
if mat['molecular weight'] and mat['molecular weight']!='-':
entry_text += cc+' Molecular weight (g/mole) = {} \n'.format(mat['molecular weight'])
if mat['density'] and mat['density']!='-':
entry_text += cc+' Density (g/cm3) = {} \n'.format(mat['density'])
if mat['total atom density'] and mat['total atom density']!='-':
if isinstance(mat['total atom density'],str):
entry_text += cc+' Total atom density (atoms/b-cm) = {} \n'.format(mat['total atom density'])
else:
entry_text += cc+' Total atom density (atoms/b-cm) = {:<13.4E} \n'.format(mat['total atom density'])
if concentration_type==None: # user did not select this, determine which is more appropriate automatically
if mat['formula'] and mat['formula']!='-':
concentration_type = 'atom fraction'
else:
concentration_type = 'weight fraction'
entry_text += cc+' Composition by {} \n'.format(concentration_type)
# Determine if neutron or photon entry will be used
neutron_keyword_list = ['depleted','enriched',' heu',' leu','uranium','plutonium','uranyl']
if particle==None: # user did not select this, determine which is more appropriate automatically
neutron_kw_found_in_name = False
for nki in neutron_keyword_list:
if nki in matname.lower():
neutron_kw_found_in_name = True
if neutron_kw_found_in_name:
particle = 'neutrons'
else:
particle = 'photons'
for j in range(len(mat[particle][concentration_type]['ZA'])):
if isinstance(mat[particle][concentration_type]['value'][j],str):
entry_format = '{:4} {:>7} {:13} '+cc+' {}' + '\n'
else:
entry_format = '{:4} {:>7d} {:<13.6f} '+cc+' {}' + '\n'
if j==0:
mstr = 'M{:<3}'.format(mi+1)
else:
mstr = ' '*4
ZZZAAA = mat[particle][concentration_type]['ZA'][j]
if ZZZAAA == '-':
ZZZAAA = mat['photons'][concentration_type]['ZA'][j]
Z = int(str(ZZZAAA)[:-3])
A = str(ZZZAAA)[-3:]
sym = Element_Z_to_Sym(Z)
if A != '000':
isotope = sym+'-'+A.lstrip('0')
else:
isotope = sym
entry_text += entry_format.format(mstr,ZZZAAA,mat[particle][concentration_type]['value'][j],isotope)
entry_text += cc+'*'*banner_width + '\n'
return entry_text
def ICRP116_effective_dose_coeff(E=1.0,particle='photon',geometry='AP',interp_scale='log',interp_type='cubic',extrapolation_on=False):
'''
Description:
For a given particle at a given energy in a given geometry, returns its
effective dose conversion coefficient from [ICRP 116](https://doi.org/10.1016/j.icrp.2011.10.001)
Dependencies:
- `import numpy as np`
- `from scipy.interpolate import CubicSpline, lagrange, interp1d`
- `find` (function within the "PHITS Tools" package)
Inputs:
- `E` = energy of the particle in MeV (D=`1`)
- `particle` = select particle (D=`'photon'`, options include: `['photon', 'electron', 'positron' ,'neutron' ,'proton', 'negmuon', 'posmuon', 'negpion', 'pospion', 'He3ion']`)
- `geometry` = geometric arrangement (D=`'AP'`, options include: `['AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO', 'H*(10)']` (`'LLAT'`,`'RLAT'`,`'ROT'` only available for photon, proton, and neutron))
- Meanings:
AP, antero-posterior; PA, postero-anterior; LLAT, left lateral; RLAT, right lateral; ROT, rotational; ISO, isotropic.
- Note: `'H*(10)'` ambient dose equivalent is available for photons only
- `interp_scale` = interpolation scale (D=`'log'` to interpolate on a log scale, options include: `['log','lin']`, ICRP 74/116 suggest log-log cubic interpolation)
- `interp_type` = interpolation method (D=`'cubic'` to interpolate with a cubic spline, options include: `['cubic','linear']`, ICRP 74/116 suggest log-log cubic interpolation)
technically, any options available for scipy.interpolate.interp1d() can be used: `['linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'previous']`
- `extrapolation_on` = boolean designating whether values outside of the tabulated energies will be extrapolated (D=`False`)
| | |
| -------------------- | --------------------------------------------------------------------- |
| if False & E < E_min | f(E) = 0 |
| if False & E > E_max | f(E) = f(E_max) |
| if True & E < E_min | f(E) is linearly interpolated between (0,0) and (E_min,f(E_min)) |
| if True & E > E_max | f(E) is extrapolated using the specified interpolation scale and type |
Outputs:
- `f` = effective dose conversion coefficient in pSv*cm^2
'''
import numpy as np
from scipy.interpolate import CubicSpline, lagrange, interp1d
pars_list = ['photon','electron','positron','neutron','proton','negmuon','posmuon','negpion','pospion','He3ion']
geo_list_all = ['AP','PA','LLAT','RLAT','ROT','ISO','H*(10)']
geo_list_short = ['AP','PA','ISO']
if particle not in pars_list or geometry not in geo_list_all:
pstr = 'Please select a valid particle and geometry.\n'
pstr += "Particle selected = {}, options include: ['photon','electron','positron','neutron','proton','negmuon','posmuon','negpion','pospion','He3ion']".format(particle)
pstr += "Geometry selected = {}, options include: ['AP','PA','LLAT','RLAT','ROT','ISO'] ('LLAT','RLAT','ROT' only available for photon, proton, and neutron)"
print(pstr)
return None
if (particle not in ['photon','neutron','proton'] and geometry in ['LLAT','RLAT','ROT']) or (particle!='photon' and geometry=='H*(10)'):
if (particle!='photon' and geometry=='H*(10)'):
pstr = "geometry = {} is only available for photons\n".format(geometry)
else:
pstr = "geometry = {} is only available for photon, neutron, and proton\n".format(geometry)
pstr += "For selected particle = {}, please choose geometry from ['AP','PA','ISO']".format(particle)
print(pstr)
return None
E_photon = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.511, 0.6, 0.662, 0.8, 1, 1.117, 1.33, 1.5, 2, 3, 4, 5, 6, 6.129, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_photon = [
[0.0685, 0.156, 0.225, 0.313, 0.351, 0.37, 0.39, 0.413, 0.444, 0.519, 0.748, 1, 1.51, 2, 2.47, 2.52, 2.91, 3.17, 3.73, 4.49, 4.9, 5.59, 6.12, 7.48, 9.75, 11.7, 13.4, 15, 15.1, 17.8, 20.5, 26.1, 30.8, 37.9, 43.1, 47.1, 50.1, 54.5, 57.8, 63.3, 67.3, 72.3, 75.5, 77.5, 78.9, 80.5, 81.7, 83.8, 85.2, 86.9, 88.1, 88.9, 89.5, 90.2, 90.7],
[0.0184, 0.0155, 0.026, 0.094, 0.161, 0.208, 0.242, 0.271, 0.301, 0.361, 0.541, 0.741, 1.16, 1.57, 1.98, 2.03, 2.38, 2.62, 3.13, 3.83, 4.22, 4.89, 5.39, 6.75, 9.12, 11.2, 13.1, 15, 15.2, 18.6, 22, 30.3, 38.2, 51.4, 62, 70.4, 76.9, 86.6, 93.2, 104, 111, 119, 124, 128, 131, 135, 138, 142, 145, 148, 150, 152, 153, 155, 155],
[0.0189, 0.0416, 0.0655, 0.11, 0.14, 0.16, 0.177, 0.194, 0.214, 0.259, 0.395, 0.552, 0.888, 1.24, 1.58, 1.62, 1.93, 2.14, 2.59, 3.23, 3.58, 4.2, 4.68, 5.96, 8.21, 10.2, 12, 13.7, 13.9, 17, 20.1, 27.4, 34.4, 47.4, 59.2, 69.5, 78.3, 92.4, 103, 121, 133, 148, 158, 165, 170, 178, 183, 193, 198, 206, 212, 216, 219, 224, 228],
[0.0182, 0.039, 0.0573, 0.0891, 0.114, 0.133, 0.15, 0.167, 0.185, 0.225, 0.348, 0.492, 0.802, 1.13, 1.45, 1.49, 1.78, 1.98, 2.41, 3.03, 3.37, 3.98, 4.45, 5.7, 7.9, 9.86, 11.7, 13.4, 13.6, 16.6, 19.7, 27.1, 34.4, 48.1, 60.9, 72.2, 82, 97.9, 110, 130, 143, 161, 172, 180, 186, 195, 201, 212, 220, 229, 235, 240, 244, 251, 255],
[0.0337, 0.0664, 0.0986, 0.158, 0.199, 0.226, 0.248, 0.273, 0.297, 0.355, 0.528, 0.721, 1.12, 1.52, 1.92, 1.96, 2.3, 2.54, 3.04, 3.72, 4.1, 4.75, 5.24, 6.55, 8.84, 10.8, 12.7, 14.4, 14.6, 17.6, 20.6, 27.7, 34.4, 46.1, 56, 64.4, 71.2, 82, 89.7, 102, 111, 121, 128, 133, 136, 142, 145, 152, 156, 161, 165, 168, 170, 172, 175],
[0.0288, 0.056, 0.0812, 0.127, 0.158, 0.18, 0.199, 0.218, 0.239, 0.287, 0.429, 0.589, 0.932, 1.28, 1.63, 1.67, 1.97, 2.17, 2.62, 3.25, 3.6, 4.2, 4.66, 5.9, 8.08, 10, 11.8, 13.5, 13.7, 16.6, 19.6, 26.8, 33.8, 46.1, 56.9, 66.2, 74.1, 87.2, 97.5, 116, 130, 147, 159, 168, 174, 185, 193, 208, 218, 232, 243, 251, 258, 268, 276],
[0.061, 0.83, 1.05, 0.81, 0.64, 0.55, 0.51, 0.52, 0.53, 0.61, 0.89, 1.20, 1.80, 2.38, 2.93, 2.99, 3.44, 3.73, 4.38, 5.20, 5.60, 6.32, 6.90, 8.60, 11.10, 13.40, 15.50, 17.60, 17.86, 21.60, 25.60, 8.53, 8.29, 8.23, 8.26, 8.64, 8.71, 8.86, 9.00, 9.60, 10.20, 10.73, 11.27, 11.80, 11.78, 11.74, 11.70, 11.60, 11.50, 12.10, 12.70, 13.30, 13.08, 12.64, 12.20]
]
E_electron = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_electron = [
[0.0269, 0.0404, 0.0539, 0.081, 0.108, 0.135, 0.163, 0.218, 0.275, 0.418, 0.569, 0.889, 1.24, 1.63, 2.05, 4.04, 7.1, 15, 22.4, 36.1, 48.2, 59.3, 70.6, 97.9, 125, 188, 236, 302, 329, 337, 341, 346, 349, 355, 359, 365, 369, 372, 375, 379, 382, 387, 391, 397, 401, 405, 407, 411, 414],
[0.0268, 0.0402, 0.0535, 0.0801, 0.107, 0.133, 0.16, 0.213, 0.267, 0.399, 0.53, 0.787, 1.04, 1.28, 1.5, 1.68, 1.68, 1.62, 1.62, 1.95, 2.62, 3.63, 5.04, 9.46, 18.3, 53.1, 104, 220, 297, 331, 344, 358, 366, 379, 388, 399, 408, 414, 419, 428, 434, 446, 455, 468, 477, 484, 490, 499, 507],
[0.0188, 0.0283, 0.0377, 0.0567, 0.0758, 0.0948, 0.114, 0.152, 0.191, 0.291, 0.393, 0.606, 0.832, 1.08, 1.35, 1.97, 2.76, 4.96, 7.24, 11.9, 16.4, 21, 25.5, 35.5, 46.7, 76.9, 106, 164, 212, 249, 275, 309, 331, 363, 383, 410, 430, 445, 457, 478, 495, 525, 549, 583, 608, 628, 646, 675, 699]
]
E_positron = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_positron = [
[3.28, 3.29, 3.3, 3.33, 3.36, 3.39, 3.42, 3.47, 3.53, 3.67, 3.84, 4.16, 4.52, 4.9, 5.36, 7.41, 10.5, 18.3, 25.7, 39.1, 51, 61.7, 72.9, 99, 126, 184, 229, 294, 320, 327, 333, 339, 342, 349, 354, 362, 366, 369, 372, 376, 379, 385, 389, 395, 399, 402, 404, 408, 411],
[1.62, 1.64, 1.65, 1.68, 1.71, 1.73, 1.76, 1.82, 1.87, 2.01, 2.14, 2.4, 2.65, 2.9, 3.12, 3.32, 3.37, 3.44, 3.59, 4.19, 5.11, 6.31, 8.03, 14, 23.6, 59, 111, 221, 291, 321, 334, 349, 357, 371, 381, 393, 402, 409, 415, 424, 430, 443, 451, 465, 473, 480, 486, 495, 503],
[1.39, 1.4, 1.41, 1.43, 1.45, 1.47, 1.49, 1.53, 1.57, 1.67, 1.77, 1.98, 2.21, 2.45, 2.72, 3.38, 4.2, 6.42, 8.7, 13.3, 18, 22.4, 26.9, 36.7, 47.6, 75.5, 104, 162, 209, 243, 268, 302, 323, 356, 377, 405, 425, 440, 453, 474, 491, 522, 545, 580, 605, 627, 645, 674, 699]
]
E_neutron = [1.00E-09, 1.00E-08, 2.50E-08, 1.00E-07, 2.00E-07, 5.00E-07, 1.00E-06, 2.00E-06, 5.00E-06, 1.00E-05, 2.00E-05, 5.00E-05, 1.00E-04, 2.00E-04, 5.00E-04, 0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.05, 0.07, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7, 0.9, 1, 1.2, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 30, 50, 75, 100, 130, 150, 180, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 5000, 10000]
f_neutron = [
[3.09, 3.55, 4, 5.2, 5.87, 6.59, 7.03, 7.39, 7.71, 7.82, 7.84, 7.82, 7.79, 7.73, 7.54, 7.54, 7.61, 7.97, 9.11, 12.2, 15.7, 23, 30.6, 41.9, 60.6, 78.8, 114, 177, 232, 279, 301, 330, 365, 407, 458, 483, 494, 498, 499, 499, 500, 500, 499, 495, 493, 490, 484, 477, 474, 453, 433, 420, 402, 382, 373, 363, 359, 363, 389, 422, 457, 486, 508, 524, 537, 612, 716, 933],
[1.85, 2.11, 2.44, 3.25, 3.72, 4.33, 4.73, 5.02, 5.3, 5.44, 5.51, 5.55, 5.57, 5.59, 5.6, 5.6, 5.62, 5.95, 6.81, 8.93, 11.2, 15.7, 20, 25.9, 34.9, 43.1, 58.1, 85.9, 112, 136, 148, 167, 195, 235, 292, 330, 354, 371, 383, 392, 398, 404, 412, 417, 419, 420, 422, 423, 423, 422, 428, 439, 444, 446, 446, 447, 448, 464, 496, 533, 569, 599, 623, 640, 654, 740, 924, 1.17E+03],
[1.04, 1.15, 1.32, 1.7, 1.94, 2.21, 2.4, 2.52, 2.64, 2.65, 2.68, 2.66, 2.65, 2.66, 2.62, 2.61, 2.6, 2.74, 3.13, 4.21, 5.4, 7.91, 10.5, 14.4, 20.8, 27.2, 39.7, 63.7, 85.5, 105, 115, 130, 150, 179, 221, 249, 269, 284, 295, 303, 310, 316, 325, 333, 336, 338, 343, 347, 348, 360, 380, 399, 409, 416, 420, 425, 427, 441, 472, 510, 547, 579, 603, 621, 635, 730, 963, 1.23E+03],
[0.893, 0.978, 1.12, 1.42, 1.63, 1.86, 2.02, 2.11, 2.21, 2.24, 2.26, 2.24, 2.23, 2.24, 2.21, 2.21, 2.2, 2.33, 2.67, 3.6, 4.62, 6.78, 8.95, 12.3, 17.9, 23.4, 34.2, 54.4, 72.6, 89.3, 97.4, 110, 128, 153, 192, 220, 240, 255, 267, 276, 284, 290, 301, 310, 313, 317, 323, 328, 330, 345, 370, 392, 404, 413, 418, 425, 429, 451, 483, 523, 563, 597, 620, 638, 651, 747, 979, 1.26E+03],
[1.7, 2.03, 2.31, 2.98, 3.36, 3.86, 4.17, 4.4, 4.59, 4.68, 4.72, 4.73, 4.72, 4.67, 4.6, 4.58, 4.61, 4.86, 5.57, 7.41, 9.46, 13.7, 18, 24.3, 34.7, 44.7, 63.8, 99.1, 131, 160, 174, 193, 219, 254, 301, 331, 351, 365, 374, 381, 386, 390, 395, 398, 398, 399, 399, 398, 398, 395, 395, 402, 406, 411, 414, 418, 422, 443, 472, 503, 532, 558, 580, 598, 614, 718, 906, 1.14E+03],
[1.29, 1.56, 1.76, 2.26, 2.54, 2.92, 3.15, 3.32, 3.47, 3.52, 3.54, 3.55, 3.54, 3.52, 3.47, 3.46, 3.48, 3.66, 4.19, 5.61, 7.18, 10.4, 13.7, 18.6, 26.6, 34.4, 49.4, 77.1, 102, 126, 137, 153, 174, 203, 244, 271, 290, 303, 313, 321, 327, 332, 339, 344, 346, 347, 350, 352, 353, 358, 371, 387, 397, 407, 412, 421, 426, 455, 488, 521, 553, 580, 604, 624, 642, 767, 1.01E+03, 1.32E+03]
]
E_proton = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_proton = [
[5.46, 8.2, 10.9, 16.4, 21.9, 27.3, 32.8, 43.7, 54.9, 189, 428, 750, 1.02E+03, 1.18E+03, 1.48E+03, 2.16E+03, 2.51E+03, 2.38E+03, 1.77E+03, 1.38E+03, 1.23E+03, 1.15E+03, 1.16E+03, 1.11E+03, 1.09E+03, 1.15E+03, 1.12E+03, 1.23E+03, 1.27E+03, 1.23E+03, 1.37E+03, 1.45E+03, 1.41E+03],
[5.47, 8.21, 10.9, 16.4, 21.9, 27.3, 32.8, 43.7, 54.6, 56.1, 43.6, 36.1, 45.5, 71.5, 156, 560, 1.19E+03, 2.82E+03, 1.93E+03, 1.45E+03, 1.30E+03, 1.24E+03, 1.23E+03, 1.23E+03, 1.23E+03, 1.25E+03, 1.28E+03, 1.34E+03, 1.40E+03, 1.45E+03, 1.53E+03, 1.65E+03, 1.74E+03],
[2.81, 4.21, 5.61, 8.43, 11.2, 14, 16.8, 22.4, 28.1, 50.7, 82.8, 180, 290, 379, 500, 799, 994, 1.64E+03, 2.15E+03, 1.44E+03, 1.27E+03, 1.21E+03, 1.20E+03, 1.19E+03, 1.18E+03, 1.21E+03, 1.25E+03, 1.32E+03, 1.31E+03, 1.39E+03, 1.44E+03, 1.56E+03, 1.63E+03],
[2.81, 4.2, 5.62, 8.41, 11.2, 14, 16.8, 22.4, 28.1, 48.9, 78.8, 172, 278, 372, 447, 602, 818, 1.46E+03, 2.18E+03, 1.45E+03, 1.28E+03, 1.21E+03, 1.20E+03, 1.20E+03, 1.20E+03, 1.23E+03, 1.25E+03, 1.32E+03, 1.33E+03, 1.41E+03, 1.45E+03, 1.59E+03, 1.67E+03],
[4.5, 6.75, 8.98, 13.4, 17.8, 22.1, 26.3, 34.5, 50.1, 93.7, 165, 296, 422, 532, 687, 1.09E+03, 1.44E+03, 2.16E+03, 1.96E+03, 1.44E+03, 1.28E+03, 1.22E+03, 1.22E+03, 1.20E+03, 1.19E+03, 1.23E+03, 1.23E+03, 1.30E+03, 1.29E+03, 1.35E+03, 1.41E+03, 1.49E+03, 1.56E+03],
[3.52, 5.28, 7.02, 10.5, 13.9, 17.3, 20.5, 26.8, 45.8, 80.1, 136, 249, 358, 451, 551, 837, 1.13E+03, 1.79E+03, 1.84E+03, 1.42E+03, 1.25E+03, 1.18E+03, 1.17E+03, 1.17E+03, 1.15E+03, 1.21E+03, 1.22E+03, 1.31E+03, 1.40E+03, 1.43E+03, 1.57E+03, 1.71E+03, 1.78E+03]
]
E_negmuon = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_negmuon = [
[180, 180, 184, 188, 193, 205, 242, 293, 332, 414, 465, 657, 735, 755, 628, 431, 382, 340, 326, 319, 320, 321, 325, 327, 333, 331, 333, 336, 337, 337, 337, 337, 338],
[75.2, 76.8, 78.3, 81.4, 84.8, 87.7, 86.7, 86.8, 88.6, 100, 122, 251, 457, 703, 775, 485, 402, 345, 329, 321, 321, 324, 326, 332, 337, 338, 341, 344, 345, 346, 346, 347, 347],
[78.7, 79.5, 80.9, 83.7, 87.1, 91.5, 98.1, 113, 127, 161, 191, 275, 363, 446, 496, 498, 432, 354, 332, 321, 321, 323, 326, 331, 337, 338, 341, 344, 346, 347, 347, 348, 348]
]
E_posmuon = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000]
f_posmuon = [
[194, 196, 198, 202, 207, 216, 251, 300, 340, 425, 481, 674, 751, 768, 635, 431, 381, 339, 326, 318, 319, 320, 322, 325, 327, 331, 333, 336, 337, 337, 337, 337, 339],
[82.6, 84.1, 85.7, 88.9, 92.1, 94.3, 92.5, 92.8, 94.8, 108, 133, 265, 473, 721, 787, 483, 399, 345, 328, 320, 321, 323, 325, 330, 333, 339, 341, 344, 345, 346, 346, 347, 347],
[85.2, 86.2, 87.5, 90.3, 93.6, 97.7, 103, 117, 132, 167, 199, 284, 373, 456, 506, 502, 432, 354, 332, 320, 320, 322, 324, 329, 333, 338, 341, 344, 346, 347, 347, 348, 348]
]
E_negpion = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 15000, 20000, 30000, 40000, 50000, 60000, 80000, 100000, 150000, 200000]
f_negpion = [
[406, 422, 433, 458, 491, 528, 673, 965, 1.09E+03, 1.25E+03, 1.28E+03, 1.77E+03, 1.92E+03, 1.93E+03, 1.68E+03, 1.14E+03, 995, 927, 902, 848, 844, 869, 901, 947, 977, 1.03E+03, 1.05E+03, 1.03E+03, 1.03E+03, 1.06E+03, 1.09E+03, 1.14E+03, 1.17E+03, 1.21E+03, 1.24E+03, 1.30E+03, 1.35E+03, 1.39E+03, 1.42E+03, 1.48E+03, 1.54E+03, 1.67E+03, 1.78E+03],
[194, 201, 210, 225, 233, 237, 208, 181, 178, 197, 244, 547, 1.02E+03, 1.70E+03, 1.99E+03, 1.31E+03, 991, 889, 871, 843, 850, 880, 917, 976, 1.02E+03, 1.08E+03, 1.12E+03, 1.11E+03, 1.13E+03, 1.18E+03, 1.22E+03, 1.29E+03, 1.34E+03, 1.41E+03, 1.47E+03, 1.56E+03, 1.63E+03, 1.70E+03, 1.75E+03, 1.86E+03, 1.95E+03, 2.15E+03, 2.33E+03],
[176, 189, 198, 215, 232, 251, 271, 317, 361, 439, 508, 676, 868, 1.02E+03, 1.15E+03, 1.15E+03, 1.03E+03, 857, 815, 794, 807, 838, 875, 935, 979, 1.05E+03, 1.09E+03, 1.11E+03, 1.15E+03, 1.20E+03, 1.26E+03, 1.36E+03, 1.43E+03, 1.55E+03, 1.64E+03, 1.79E+03, 1.91E+03, 2.02E+03, 2.11E+03, 2.29E+03, 2.46E+03, 2.80E+03, 3.04E+03]
]
E_pospion = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 15000, 20000, 30000, 40000, 50000, 60000, 80000, 100000, 150000, 200000]
f_pospion = [
[314, 324, 340, 379, 429, 489, 540, 717, 819, 1000, 1.10E+03, 1.52E+03, 1.75E+03, 1.83E+03, 1.66E+03, 1.22E+03, 1.13E+03, 1.22E+03, 1.25E+03, 1.07E+03, 969, 943, 952, 999, 1.04E+03, 1.10E+03, 1.10E+03, 1.06E+03, 1.06E+03, 1.07E+03, 1.10E+03, 1.14E+03, 1.17E+03, 1.22E+03, 1.25E+03, 1.30E+03, 1.34E+03, 1.38E+03, 1.42E+03, 1.48E+03, 1.54E+03, 1.67E+03, 1.78E+03],
[121, 125, 133, 151, 170, 183, 185, 177, 179, 201, 247, 494, 906, 1.48E+03, 1.82E+03, 1.38E+03, 1.12E+03, 1.15E+03, 1.23E+03, 1.10E+03, 998, 970, 980, 1.04E+03, 1.09E+03, 1.16E+03, 1.19E+03, 1.16E+03, 1.16E+03, 1.20E+03, 1.24E+03, 1.31E+03, 1.35E+03, 1.42E+03, 1.48E+03, 1.57E+03, 1.64E+03, 1.70E+03, 1.75E+03, 1.84E+03, 1.94E+03, 2.14E+03, 2.33E+03],
[151, 160, 168, 183, 198, 216, 233, 265, 296, 367, 439, 602, 787, 953, 1.09E+03, 1.16E+03, 1.10E+03, 1.05E+03, 1.08E+03, 1.02E+03, 953, 930, 938, 993, 1.05E+03, 1.13E+03, 1.16E+03, 1.16E+03, 1.18E+03, 1.23E+03, 1.28E+03, 1.37E+03, 1.43E+03, 1.55E+03, 1.64E+03, 1.79E+03, 1.90E+03, 2.01E+03, 2.10E+03, 2.27E+03, 2.42E+03, 2.76E+03, 3.07E+03]
]
E_He3ion = [1, 2, 3, 5, 10, 14, 20, 30, 50, 75, 100, 150, 200, 300, 500, 700, 1000, 2000, 3000, 5000, 10000, 20000, 50000, 100000]
f_He3ion = [
[219, 438, 656, 1.09E+03, 2.19E+03, 4.61E+03, 1.72E+04, 3.01E+04, 4.75E+04, 8.05E+04, 1.01E+05, 9.25E+04, 6.74E+04, 5.14E+04, 4.27E+04, 4.11E+04, 4.00E+04, 4.02E+04, 4.08E+04, 4.12E+04, 4.56E+04, 5.12E+04, 6.12E+04, 7.14E+04],
[219, 438, 657, 1.09E+03, 2.19E+03, 2.56E+03, 1.74E+03, 1.44E+03, 2.88E+03, 1.75E+04, 4.84E+04, 1.10E+05, 7.29E+04, 5.33E+04, 4.49E+04, 4.60E+04, 4.47E+04, 4.80E+04, 5.01E+04, 5.17E+04, 6.26E+04, 6.10E+04, 8.14E+04, 1.01E+05],
[141, 281, 419, 689, 1.82E+03, 2.81E+03, 5.46E+03, 9.86E+03, 1.78E+04, 3.00E+04, 4.55E+04, 6.95E+04, 7.01E+04, 5.25E+04, 4.27E+04, 4.19E+04, 4.09E+04, 4.31E+04, 4.50E+04, 4.76E+04, 5.73E+04, 7.10E+04, 9.67E+04, 1.24E+05]
]
E_all = [E_photon, E_electron, E_positron, E_neutron, E_proton, E_negmuon, E_posmuon, E_negpion, E_pospion, E_He3ion]
f_all = [f_photon, f_electron, f_positron, f_neutron, f_proton, f_negmuon, f_posmuon, f_negpion, f_pospion, f_He3ion]
pi = find(particle, pars_list)
if particle in ['photon','neutron','proton']:
gi = find(geometry, geo_list_all)
else:
gi = find(geometry, geo_list_short)
E_list = E_all[pi]
f_list = f_all[pi][gi]
# Interpolate f given E
if E in E_list:
f = f_list[find(E,E_list)]
else:
if not extrapolation_on and (E < E_list[0] or E > E_list[-1]): # E is outside of bounds and extrapolation is off
if E < E_list[0]:
f = 0 # assume negligibly low energy particle
if E > E_list[-1]:
f = f_list[-1] # just set equal to max energy particle's coefficient
else:
if E < E_list[0]:
E_list = [0] + E_list
f_list = [0] + f_list
interp_scale = 'linear'
if interp_scale=='log':
cs = interp1d(np.log10(np.array(E_list)),np.log10(np.array(f_list)), kind=interp_type,fill_value='extrapolate')
f = 10**cs(np.log10(E))
else:
cs = interp1d(np.array(E_list),np.array(f_list), kind=interp_type,fill_value='extrapolate')
f = cs(E)
# for sake of sanity, return zero for values quite below minimum coefficients
if f < 1e-4:
f = 0.0
#if interp_type=='cubic':
# if interp_scale=='log':
# cs = interp1d(np.log10(np.array(E_list)),np.log10(np.array(f_list)), kind='cubic',fill_value='extrapolate')
# f = 10**cs(np.log10(E))
# else:
# cs = interp1d(np.array(E_list),np.array(f_list), kind='cubic',fill_value='extrapolate')
# f = cs(E)
#else:
# if interp_scale=='log':
# f = 10**np.interp(np.log10(E),np.log10(np.array(E_list)),np.log10(np.array(f_list)))
# else:
# f = np.interp(E,np.array(E_list),np.array(f_list))
#if interp_type=='cubic':
# if interp_scale=='log':
# cs = lagrange(np.log10(np.array(E_list)),np.log10(np.array(f_list)))
# f = 10**cs(np.log10(E))
# else:
# cs = lagrange(np.array(E_list),np.array(f_list))
# f = cs(E)
#if interp_type=='cubic':
# if interp_scale=='log':
# cs = CubicSpline(np.log10(np.array(E_list)),np.log10(np.array(f_list)))
# f = 10**cs(np.log10(E))
# else:
# cs = CubicSpline(np.array(E_list),np.array(f_list))
# f = cs(E)
return f
def merge_dump_file_pickles(dump_filepath_list, merged_dump_base_filepath='merged_dump',
delete_pre_merge_pickles=False, compress_pickles_with_lzma=True):
'''
Description:
Merge the pickle files (namedtuple lists and/or PandasDataFrames) belonging to numerous PHITS dump output files.
Inputs:
- `dump_filepath_list` = list of Path objects or strings denoting filepaths to the PHITS dump files
(e.g., "*_dmp.out") to be merged. Note that all dump files must be structured in the same way in
terms of number of columns and their meanings.
- `merged_dump_base_filepath` = (D=`os.cwd()+'merged_dump'`) Path object or string designating the base file
path name to be used for the merged pickle files,
which will take the form `merged_dump_base_filepath` + "_namedtuple_list.pickle[.xz]" and/or
"_Pandas_df.pickle[.xz]" (the ".xz" being contingent on LZMA usage via `compress_pickles_with_lzma`).
- `delete_pre_merge_pickles` = (optional, D=`False`) Boolean designating whether the numerous pickle files to be
merged into one should be deleted after the merge is successful. (Note: File deletion will only succeed
if you have permissions to delete these files on your system.)
- `compress_pickles_with_lzma` = (optional, D=`True`) Boolean designating whether the pickle files to be saved of
the merged namedtuple lists and/or the Pandas DataFrames
will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included within
the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the file
extension of the saved pickle file(s) will be `'.pickle.xz'` instead of just `'.pickle'`.
A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as:
`with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)`.
While compression will notably slow down the file-saving process, owing to the often large size of
PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred.
Notes:
For each dump file provided, this function will check for the existence of the same filename/path but with
"_namedtuple_list.pickle[.xz]" and "_Pandas_df.pickle[.xz]" at the end. If neither file for a provided
dump file is found but the dump file itself is found to exist, `parse_tally_dump_file()` will be called on it
with default settings except `save_namedtuple_list=True` and `save_Pandas_dataframe=True`.
Outputs:
- `merge_success` = Boolean designating whether merging succeeded or not.
'''
import lzma
import pickle
merge_success = False
if compress_pickles_with_lzma:
compression_file_extension = '.xz'
else:
compression_file_extension = ''
# Scan to see what files are available
namedtuple_list_paths = []
pandads_DF_paths = []
for f in dump_filepath_list:
fp = Path(f)
filebasename = fp.name
if filebasename[-4:]=='.out':
filebasename = filebasename[:-4]
nt_found, pd_found = False, False
nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle.xz')
if nt_path.is_file():
nt_found = True
else:
nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle')
if nt_path.is_file():
nt_found = True
pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle.xz')
if pd_path.is_file():
pd_found = True
else:
pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle')
if pd_path.is_file():
pd_found = True
if not nt_found and not pd_found and fp.is_file():
parse_tally_dump_file(fp,return_namedtuple_list=False, return_Pandas_dataframe=False, save_namedtuple_list=True, save_Pandas_dataframe=True)
nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle.xz')
pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle.xz')
nt_found, pd_found = True, True
if nt_found: namedtuple_list_paths.append(nt_path)
if pd_found: pandads_DF_paths.append(pd_path)
# Merge namedtuple lists
if len(namedtuple_list_paths) > 1:
from numpy.lib.recfunctions import stack_arrays
if len(namedtuple_list_paths) != len(dump_filepath_list):
print('WARNING: While multiple "_namedtuple_list.pickle[.xz]" files were found, some were missing from provided dump file list.')
for i, f in enumerate(namedtuple_list_paths):
if f.suffixes[-1] == '.xz':
with lzma.open(f, 'rb') as file:
dump_data_list = pickle.load(file)
else:
with open(f, 'rb') as file:
dump_data_list = pickle.load(file)
if i==0:
a = dump_data_list
continue
b = dump_data_list
records_np_array = stack_arrays((a, b), asrecarray=True, usemask=False)
a = records_np_array
del a, b # release memory for this, as it can be very, very big
path_to_dump_file = Path(merged_dump_base_filepath)
pickle_path = Path(path_to_dump_file.parent,
path_to_dump_file.stem + '_namedtuple_list.pickle' + compression_file_extension)
if compress_pickles_with_lzma:
with lzma.open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
else:
with open(pickle_path, 'wb') as handle:
pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL)
print('Pickle file written:', pickle_path, '\n')
merge_success = True
del records_np_array # release memory for this, as it can be very, very big
if delete_pre_merge_pickles:
for f in namedtuple_list_paths:
try:
f.unlink()
print('\tPickle file deleted:', f)
except:
pass
print()
# Merge Pandas DataFrames
if len(pandads_DF_paths) > 1:
import pandas as pd
if len(pandads_DF_paths) != len(dump_filepath_list):
print('WARNING: While multiple "_Pandas_df.pickle[.xz]" files were found, some were missing from provided dump file list.')
dfs_to_concat = []
for i, f in enumerate(pandads_DF_paths):
if f.suffixes[-1] == '.xz':
with lzma.open(f, 'rb') as file:
dump_dataframe = pickle.load(file)
else:
with open(f, 'rb') as file:
dump_dataframe = pickle.load(file)
dfs_to_concat.append(dump_dataframe)
combined_df = pd.concat(dfs_to_concat, ignore_index=True)
del dfs_to_concat # release memory for this, as it can be very, very big
path_to_dump_file = Path(merged_dump_base_filepath)
pickle_path = Path(path_to_dump_file.parent,
path_to_dump_file.stem + '_Pandas_df.pickle' + compression_file_extension)
combined_df.to_pickle(pickle_path)
print('Pickle file written:', pickle_path, '\n')
merge_success = True
del combined_df # release memory for this, as it can be very, very big
if delete_pre_merge_pickles:
for f in pandads_DF_paths:
try:
f.unlink()
print('\tPickle file deleted:', f)
except:
pass
print()
return merge_success
def ZZZAAAM_to_nuclide_plain_str(ZZZAAAM,include_Z=False,ZZZAAA=False,delimiter='-'):
'''
Description:
Converts a plaintext string of a nuclide from an integer ZZZAAAM = 10000*Z + 10*A + M
Dependencies:
`Element_Z_to_Sym` (function within the "PHITS Tools" package)
Input:
- `ZZZAAAM` = integer equal to 10000*Z + 10*A + M, where M designates the metastable state (0=ground)
- `include_Z` = Boolean denoting whether the Z number should be included in the output string (D=`False`)
- `ZZZAAA` = Boolean denoting whether the input should be interpreted as a ZZZAAA value (1000Z+A) instead (D=`False`)
- `delimiter` = string which will be used to separate elements of the output string (D=`-`)
Output:
- `nuc_str` = string describing the input nuclide formatted as [Z]-[Symbol]-[A][m]
'''
ZZZAAAM = int(ZZZAAAM)
if ZZZAAA:
ZZZAAAM = ZZZAAAM*10
m = ZZZAAAM % 10
A = (ZZZAAAM % 10000) // 10
Z = ZZZAAAM // 10000
symbol = Element_Z_to_Sym(Z)
m_str = ''
if m>0:
m_str = 'm' + str(m)
nuc_str = ''
if include_Z:
nuc_str += str(Z) + delimiter
nuc_str += symbol + delimiter + str(A) + m_str
return nuc_str
def nuclide_plain_str_to_latex_str(nuc_str,include_Z=False):
'''
Description:
Converts a plaintext string of a nuclide to a LaTeX-formatted raw string
Note: if you already have the Z, A, and isomeric state information determined, the "nuclide_to_Latex_form" function can be used instead
Dependencies:
- `Element_Z_to_Sym` (function within the "PHITS Tools" package) (only required if `include_Z = True`)
Input:
(required)
- `nuc_str` = string to be converted; a huge variety of formats are supported, but they all must follow the following rules:
+ Isomeric/metastable state characters must always immediately follow the atomic mass characters.
Isomeric state labels MUST either:
- (1) be a single lower-case character OR
- (2) begin with any non-numeric character and end with a number
+ Atomic mass numbers must be nonnegative integers OR the string `"nat"` (in which case no metastable states can be written)
+ Elemental symbols MUST begin with an upper-case character
Input:
(optional)
- `include_Z` = `True`/`False` determining whether the nuclide's atomic number Z will be printed as a subscript beneath the atomic mass
Output:
- LaTeX-formatted raw string of nuclide
'''
tex_str = r''
# remove unwanted characters from provided string
delete_characters_list = [' ', '-', '_']
for dc in delete_characters_list:
nuc_str = nuc_str.replace(dc,'')
# determine which characters are letters versus numbers
isalpha_list = []
isdigit_list = []
for c in nuc_str:
isalpha_list.append(c.isalpha())
isdigit_list.append(c.isdigit())
symbol = ''
mass = ''
isost = ''
# string MUST begin with either mass number or elemental symbol
if isdigit_list[0] or nuc_str[0:3]=='nat': # mass first
mass_first = True
else:
mass_first = False
if mass_first:
if nuc_str[0:3]=='nat':
mass = 'nat'
ci = 3
else:
ci = 0
while isdigit_list[ci]:
mass += nuc_str[ci]
ci += 1
mass = str(int(mass)) # eliminate any extra leading zeros
# encountered a non-numeric character, end of mass
# now, determine if metastable state is listed or if element is listed next
# first, check to see if any other numerals are in string
lni = 0 # last numeral index
for i in range(ci,len(nuc_str)):
if isdigit_list[i]:
lni = i
if lni != 0:
# grab all characters between ci and last numeral as metastable state
isost = nuc_str[ci:lni+1]
ci = lni + 1
else: # no more numerals in string, now check for single lower-case letter
if isalpha_list[ci] and nuc_str[ci].islower():
isost = nuc_str[ci]
ci += 1
# Now extract elemental symbol
for i in range(ci,len(nuc_str)):
if isalpha_list[i]:
symbol += nuc_str[i]
else: # if elemental symbol is listed first
if 'nat' in nuc_str:
mass = 'nat'
nuc_str = nuc_str.replace('nat','')
ci = 0
# Extract all characters before first number as the elemental symbol
while nuc_str[ci].isalpha():
symbol += nuc_str[ci]
ci += 1
# now, extract mass
if mass != 'nat':
while nuc_str[ci].isdigit():
mass += nuc_str[ci]
ci += 1
if ci == len(nuc_str):
break
# lastly, extract isomeric state, if present
if ci != len(nuc_str):
isost = nuc_str[ci:]
# treating the cases of lowercase-specified particles (n, d, t, etc.)
if symbol == '' and isost != '':
symbol = isost
isost = ''
# Now assemble LaTeX string for nuclides
if include_Z:
if symbol == 'n':
Z = 0
elif symbol == 'p' or symbol == 'd' or symbol == 't':
Z = 1
else:
Z = Element_Sym_to_Z(symbol)
Z = str(int(Z))
tex_str = r"$^{{{}{}}}_{{{}}}$".format(mass,isost,Z) + "{}".format(symbol)
else:
tex_str = r"$^{{{}{}}}$".format(mass,isost) + "{}".format(symbol)
return tex_str
def Element_Z_to_Sym(Z):
'''
Description:
Returns elemental symbol for a provided atomic number Z
Inputs:
- `Z` = atomic number
Outputs:
- `sym` = string of elemental symbol for element of atomic number Z
'''
elms = ["n ",\
"H ","He","Li","Be","B ","C ","N ","O ","F ","Ne",\
"Na","Mg","Al","Si","P ","S ","Cl","Ar","K ","Ca",\
"Sc","Ti","V ","Cr","Mn","Fe","Co","Ni","Cu","Zn",\
"Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y ","Zr",\
"Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn",\
"Sb","Te","I ","Xe","Cs","Ba","La","Ce","Pr","Nd",\
"Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb",\
"Lu","Hf","Ta","W ","Re","Os","Ir","Pt","Au","Hg",\
"Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th",\
"Pa","U ","Np","Pu","Am","Cm","Bk","Cf","Es","Fm",\
"Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds",\
"Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"]
i = int(Z)
if i < 0 or i > len(elms):
print('Z={} is not valid, please select a number from 0 to 118 (inclusive).'.format(str(Z)))
return None
return elms[i].strip()
def Element_Sym_to_Z(sym):
'''
Description:
Returns atomic number Z for a provided elemental symbol
Dependencies:
`find` (function within the "PHITS Tools" package)
Inputs:
- `sym` = string of elemental symbol for element of atomic number Z
Outputs:
- `Z` = atomic number
'''
elms = ["n ",\
"H ","He","Li","Be","B ","C ","N ","O ","F ","Ne",\
"Na","Mg","Al","Si","P ","S ","Cl","Ar","K ","Ca",\
"Sc","Ti","V ","Cr","Mn","Fe","Co","Ni","Cu","Zn",\
"Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y ","Zr",\
"Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn",\
"Sb","Te","I ","Xe","Cs","Ba","La","Ce","Pr","Nd",\
"Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb",\
"Lu","Hf","Ta","W ","Re","Os","Ir","Pt","Au","Hg",\
"Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th",\
"Pa","U ","Np","Pu","Am","Cm","Bk","Cf","Es","Fm",\
"Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds",\
"Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"]
if len(sym.strip())>2:
print('Please provide a valid elemental symbol (1 or 2 characters), {} is too long'.format(sym))
return -1
# handle exception for neutron first
if sym.strip()=='XX':
return 0
# make sure string is formatted to match entries in elms list
sym2 = sym.strip()
if len(sym2)==1: sym2 += ' '
sym2 = sym2[0].upper() + sym2[1].lower()
Z = find(sym2,elms)
if Z==None:
print('Z could not be found for element "{}"; please make sure entry is correct.'.format(sym))
return -1
return Z
def kfcode_to_common_name(kf_code):
'''
Description:
Converts an integer kf-code to plaintext string of a particle/nuclide
Input:
- `kf_code` = integer kf-code particle identification number (see PHITS manual Table 4.4)
Output:
- `par_nuc_str` = string either naming the particle or describing the input nuclide formatted as [Symbol]-[A]
'''
kf_code = int(kf_code)
named_kf_codes = [2212 ,2112 ,22 ,11 ,-11 ,211 ,111 ,-211 ,-13 ,13 ,321 ,311 ,-321 ]
named_kf_code_strs = ['proton','neutron','photon','electron','positron','pion+','pion0','pion-','muon+','muon-','kaon+','kaon0','kaon-']
if abs(kf_code) < 1000000:
# specifically named particles
if kf_code in named_kf_codes:
i = find(kf_code,named_kf_codes)
par_nuc_str = named_kf_code_strs[i]
else:
par_nuc_str = str(kf_code)
else:
A = kf_code % 1000000
Z = (kf_code-A) / 1000000
ZZZAAA = 1000*Z + A
par_nuc_str = ZZZAAAM_to_nuclide_plain_str(ZZZAAA, ZZZAAA=True)
return par_nuc_str
def is_number(n):
'''
Description:
Determine if a string is that of a number or not.
Inputs:
- `n` = string to be tested
Outputs:
- `True` if value is a number (can be converted to float() without an error)
- `False` otherwise
'''
try:
float(n)
except ValueError:
return False
return True
def find(target, myList):
'''
Description:
Search for and return the index of the first occurance of a value in a list.
Inputs:
- `target` = value to be searched for
- `myList` = list of values
Output:
- index of first instance of `target` in `myList`
'''
for i in range(len(myList)):
if myList[i] == target:
return i
def determine_PHITS_output_file_type(output_file):
'''
Description:
Determine what kind of PHITS file is being hanlded (tally standard output, binary tally dump, ASCII tally dump, etc.)
Inputs:
- `output_file` = a file/filepath (string or Path object) to be judged
Outputs:
- `PHITS_file_type` = a dictionary of Booleans detailing what kind of file `output_file` is (and isn't) with
the following keys (each with a value set to `True` or `False`):
`'is_standard_tally_output'`, `'is_binary_tally_dump'`, `'is_ASCII_tally_dump'`,
`'is_PHITS_input_file'`, `'is_file6_phitsout_file'`,
`'is_unknown_file_type'`, and `'file_does_not_exist'`. By default, all are set to `False` except for
`'is_unknown_file_type'` which is `True` by default.
Notes:
- Dump files are identified with the presence of '_dmp' in their filename. Whether it is ASCII or binary is
simply determined by attempting to read the first line of the file in a try/except statement.
- Standard tally output files are identified by `[` being the very first character in the first line.
- PHITS input files are identified by having a file extension in the following list (case insensitive): `['.inp','.in','.input','.i']`
- 'phits.out' files (`file(6)` in the PHITS [Parameters] section) are identified by its first line consisting of
11 spaces followed by 57 underscores.
'''
import re
PHITS_file_type = {'is_standard_tally_output': False,
'is_binary_tally_dump': False,
'is_ASCII_tally_dump': False,
'is_PHITS_input_file': False,
'is_file6_phitsout_file': False,
'is_unknown_file_type': True,
'file_does_not_exist': False
}
output_file = Path(output_file)
if not output_file.is_file():
print('Provided output file',output_file,'was determined to not be a file!')
PHITS_file_type['is_unknown_file_type'] = False
PHITS_file_type['file_does_not_exist'] = True
return PHITS_file_type
with open(output_file, 'r', encoding='utf-8') as f:
try:
first_line = f.readline().strip()
first_line_lower_no_spaces = re.sub(r"\s+", "", first_line, flags=re.UNICODE).lower()
except: # triggered if encountering binary / non ASCII or UTF-8 file
if '_dmp' in output_file.stem:
PHITS_file_type['is_binary_tally_dump'] = True
PHITS_file_type['is_unknown_file_type'] = False
return PHITS_file_type
if first_line_lower_no_spaces[:3] == '[t-':
PHITS_file_type['is_standard_tally_output'] = True
PHITS_file_type['is_unknown_file_type'] = False
elif '_dmp' in output_file.stem:
PHITS_file_type['is_ASCII_tally_dump'] = True
PHITS_file_type['is_unknown_file_type'] = False
elif output_file.suffix.lower() in ['.inp','.in','.input','.i']:
PHITS_file_type['is_PHITS_input_file'] = True
PHITS_file_type['is_unknown_file_type'] = False
elif '_________________________________________________________' in first_line:
PHITS_file_type['is_file6_phitsout_file'] = True
PHITS_file_type['is_unknown_file_type'] = False
return PHITS_file_type
def search_for_dump_parameters(output_file):
'''
Description:
Try to determine the dump settings used for a dump file by searching for the same file without "_dmp" and parsing
its header for the "dump = " line and subsequent line specifying the column ordering.
Inputs:
- `output_file` = a file/filepath (string or Path object) to be judged
Outputs:
- `dump_data_number` = value following "dump = " in the PHITS tally (integer from -20 to 20, excluding 0) (D=`None`)
- `dump_data_sequence` = list of integers specifying the order and meaning of the dump file columns (D=`None`)
'''
dump_data_number, dump_data_sequence = None, None
output_file = Path(output_file)
output_file_suffixes = output_file.suffixes
if len(output_file_suffixes) > 1 and is_number(output_file_suffixes[-1][1:]) and '_dmp' in output_file.stem: # MPI dump found
origin_tally_file = Path(output_file.parent, output_file.stem.replace('_dmp', ''))
else:
origin_tally_file = Path(output_file.parent, output_file.stem.replace('_dmp','') + output_file.suffix)
PHITS_file_type = determine_PHITS_output_file_type(origin_tally_file)
if PHITS_file_type['file_does_not_exist']:
print("Could not find this dump file's companion original standard tally output file",origin_tally_file)
return dump_data_number, dump_data_sequence
elif not PHITS_file_type['is_standard_tally_output']:
print("Found dump file's suspected companion original standard tally output file, but it does not seem to actually be formatted as a standard tally output file",origin_tally_file)
return dump_data_number, dump_data_sequence
tally_header, tally_content = split_into_header_and_content(origin_tally_file)
for li, line in enumerate(tally_header):
if "dump =" in line:
if line[0] == '#': # commented line
key, value = extract_data_from_header_line(line[1:])
else:
key, value = extract_data_from_header_line(line)
dump_data_number = int(value)
dump_data_sequence_str_list = tally_header[li+1].strip().split()
dump_data_sequence = [int(i) for i in dump_data_sequence_str_list]
break
if dump_data_number == None and dump_data_sequence == None:
print('Was unable to locate dump specification information in tally output file',origin_tally_file)
return dump_data_number, dump_data_sequence
def split_into_header_and_content(output_file_path):
'''
Description:
Initial parsing of a PHITS tally output file to isolate its header section (containing metadata) and main
tally results "content" section for later processing.
Inputs:
- `output_file_path` = path to a PHITS tally output file
Outputs:
- `header` = list of lines belonging to the tally output's header section
- `content` = list of lists of remaining lines after the tally output's header section; the top level list is
broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page.
'''
in_content = False
header, content = [], [[]]
with open(output_file_path, mode='rb') as f:
for line in f:
if b'\x00' in line:
line = line.replace(b"\x00", b"")
#line = line.decode()
try: # skip lines with invalid characters in them...
line = line.decode()
except:
continue
#if "\x00" in line: line = line.replace("\x00", "")
if '#newpage:' in line:
in_content = True
continue
if in_content:
if 'newpage:' in line:
content.append([])
continue
content[-1].append(line.strip())
else:
header.append(line.strip())
# add "footer" to peel off last bit of "content" section?
if in_debug_mode: print(len(content), 'content blocks (pages) found in tally output')
return header, content
def parse_tally_header(tally_header,tally_content):
'''
Description:
Extracts metadata from PHITS tally output header (and some extra info from its contents section)
Dependencies:
- `extract_data_from_header_line` (function within the "PHITS tools" package)
- `parse_group_string` (function within the "PHITS tools" package)
Inputs:
- `tally_header` = list of lines belonging to the tally output's header section
- `tally_content` = list of lists of remaining lines after the tally output's header section; the top level list is
broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page.
Outputs:
- `meta` = Munch object / dictionary containing tally metadata
'''
prefer_to_munch_meta_dict = True
if prefer_to_munch_meta_dict:
try:
from munch import Munch
use_munch = True
except:
use_munch = False
else:
use_munch = False
nlines = len(tally_header)
tally_type = tally_header[0].replace(' ','').replace('off','')
if '[' not in tally_type and ']' not in tally_type: # file is not PHITS tally output
if 'htitle' in tally_type:
tally_type = '[T-Dchain]'
else:
tally_type = 'UNKNOWN'
if use_munch:
meta = Munch({})
else:
meta = {}
meta['tally_type'] = tally_type
unsupported_tally_types = ['[T-WWG]', '[T-WWBG]', '[T-Volume]', '[T-Userdefined]', '[T-Gshow]', '[T-Rshow]',
'[T-3Dshow]', '[T-4Dtrack]', '[T-Dchain]', 'UNKNOWN']
if tally_type in unsupported_tally_types:
return meta
# Initialize variables for possible array
mesh_types = ['e','t','x','y','z','r','a','l']
for m in mesh_types: meta['n'+m] = None
meta['reg'] = None
meta['part'] = None
meta['npart'] = None
meta['nc'] = None
meta['samepage'] = 'part'
found_mesh_kinds = []
reading_axis_data = False
reading_regions = False
in_exceptional_mesh_kind = False
for li, line in enumerate(tally_header):
#if line[0]=='#': # commented line
if 'data =' in line: # data section to parse
reading_axis_data = True
n_values_to_read = meta['n'+current_data_mesh_kind] + 1
remaining_n_values_to_read = n_values_to_read
data_values = []
in_exceptional_mesh_kind = False
#print('read ',n_values_to_read,current_data_mesh_kind,' values')
continue
elif '=' in line:
if line[0] == '#': # commented line
key, value = extract_data_from_header_line(line[1:])
if key=='file' and key in meta and '_dmp' in value: continue # do not overwrite existing file parameter with dump file
else:
key, value = extract_data_from_header_line(line)
if in_exceptional_mesh_kind:
if key[0]=='e':
key = current_data_mesh_kind + key[1:]
elif key=='ne':
key = 'n' + current_data_mesh_kind
meta[key] = value
if (tally_type=='[T-Yield]' or tally_type=='[T-Track]') and key=='axis' and value=='dchain':
# Unsupported tally output!
return meta
if 'type' in key:
current_data_mesh_kind = key.replace('-type','')
if current_data_mesh_kind == 'se': current_data_mesh_kind = 'e'
current_data_mesh_type = value
found_mesh_kinds.append(current_data_mesh_kind)
if current_data_mesh_kind in ['e1','e2']:
in_exceptional_mesh_kind = True
#print(current_data_mesh_kind,current_data_mesh_type)
if key=='part':
part_groups = parse_group_string(str(value))
kf_groups = parse_group_string(tally_header[li + 1].split(':')[1])
if meta['npart'] == None: # first instance of "part"
meta['part_groups'] = part_groups
meta['kf_groups'] = kf_groups
meta['npart'] = len(part_groups)
meta['part_serial_groups'] = ['p'+str(gi+1)+'-group' for gi in range(len(part_groups))]
else: # an additional occurance of part?
if 'multiplier' not in tally_header[li - 1]: # the multiplier can also be followed by an erroneous "part" specification
for pi,pg in enumerate(part_groups):
if pg not in meta['part_groups']:
meta['part_groups'] += [pg]
meta['kf_groups'] += kf_groups[pi]
meta['npart'] += 1
meta['part_serial_groups'] += ['p' + str(pi + 1) + '-group']
if key=='reg':
if meta['tally_type']=='[T-Cross]':
num_regs = value
meta['num_reg_groups'] = num_regs
meta['reg_groups'] = []
# manually read in reg groups
li_start = li+2
li_stop = li_start + num_regs
for lii in range(li_start,li_stop):
non, rfrom, rto, area = tally_header[lii].split()
meta['reg_groups'].append(rfrom+' - '+rto)
else:
reg_groups = parse_group_string(str(value))
eli = 0 # extra line index
if '=' not in tally_header[eli+li+1] and 'volume' not in tally_header[eli+li+1]: # reg specification continues to next line
while '=' not in tally_header[eli+li+1] and 'volume' not in tally_header[eli+li+1]:
reg_groups += parse_group_string(tally_header[eli+li+1].strip())
eli += 1
if 'all' in reg_groups and 'volume' in tally_header[li+1] and '=' not in tally_header[eli+li+1]:
# parse table of regions...
found_reg_grps = []
meta['reg_groups_inputted'] = reg_groups
reg_lines = tally_header[li+3:]
for reg_line in reg_lines:
if '=' in reg_line: break
line_parts = reg_line.split('#')
if len(line_parts) >= 2:
found_reg_grps.append(line_parts[1].strip())
else:
found_reg_grps.append(line_parts[0].split()[1])
meta['reg_groups'] = found_reg_grps
meta['num_reg_groups'] = len(found_reg_grps)
else:
meta['reg_groups'] = reg_groups
meta['num_reg_groups'] = len(reg_groups)
if key == 'point':
num_regs = value
meta['point_detectors'] = {'non':[], 'x':[], 'y':[], 'z':[], 'r0':[]} # [T-Point] points
li_start = li + 2
li_stop = li_start + num_regs
for lii in range(li_start, li_stop):
non, tppx, tppy, tppz, tppr0 = tally_header[lii].split()
meta['point_detectors']['non'].append(non)
meta['point_detectors']['x'].append(tppx)
meta['point_detectors']['y'].append(tppy)
meta['point_detectors']['z'].append(tppz)
meta['point_detectors']['r0'].append(tppr0)
if key == 'ring':
num_regs = value
meta['point_detectors'] = {'non':[], 'axis':[], 'ar':[], 'rr':[], 'r0':[]} # [T-Point] points
li_start = li + 2
li_stop = li_start + num_regs
for lii in range(li_start, li_stop):
non, tppx, tppy, tppz, tppr0 = tally_header[lii].split()
meta['point_detectors']['non'].append(non)
meta['point_detectors']['axis'].append(tppx)
meta['point_detectors']['ar'].append(tppy)
meta['point_detectors']['rr'].append(tppz)
meta['point_detectors']['r0'].append(tppr0)
elif reading_axis_data:
values = line.replace('#','').strip().split()
for val in values:
data_values.append(float(val))
remaining_n_values_to_read += -1
if remaining_n_values_to_read <= 0:
reading_axis_data = False
data_values = np.array(data_values)
meta[current_data_mesh_kind+'-mesh_bin_edges'] = data_values
meta[current_data_mesh_kind+'-mesh_bin_mids'] = 0.5*(data_values[1:]+data_values[:-1])
#meta[current_data_mesh_kind+'-mesh_bin_mids_log'] = np.sqrt(data_values[1:]*data_values[:-1])
# generate log-centered bin mids
bin_mids_log = []
for i in range(len(data_values)-1):
if data_values[i+1]<=0 or data_values[i]<=0: # if one or both edges <= 0
if data_values[i+1]<0 and data_values[i]<0: # both values are negative
bin_mids_log.append(-1*np.sqrt(data_values[i]*data_values[i+1]))
elif data_values[i+1]==0 or data_values[i]==0: # one value is zero
# use linear center instead...
bin_mids_log.append(0.5*(data_values[i]+data_values[i+1]))
elif data_values[i+1]<0 or data_values[i]<0: # bin straddles zero
# use linear center instead...
bin_mids_log.append(0.5*(data_values[i]+data_values[i+1]))
else:
print('unknown binning encountered, skipping generation of log-scale bin mids for '+current_data_mesh_kind+'-mesh')
break
else:
bin_mids_log.append(np.sqrt(data_values[i]*data_values[i+1]))
meta[current_data_mesh_kind+'-mesh_bin_mids_log'] = np.array(bin_mids_log)
continue
else:
continue
meta['found_mesh_kinds'] = found_mesh_kinds
if meta['tally_type']=='[T-Cross]':
if meta['mesh']=='xyz':
if 'enclos' in meta and meta['enclos']==1:
pass # total items remains nx*ny*nz
else:
meta['nz_original'] = meta['nz']
meta['nz'] += 1 # zmesh surfaces are scored, making array nx*ny*(nz+1)
elif meta['mesh']=='r-z':
if 'enclos' in meta and meta['enclos']==1:
pass # total items remains nr*nz
else:
# Current solution addresses this by expanding the ierr axis
meta['nr_original'] = meta['nr']
meta['nz_original'] = meta['nz']
meta['nr'] = meta['nr'] + 1
meta['nz'] = meta['nz'] + 1
# OLD SOLUTION IMPLEMENTED IS BELOW
# max total num of pages = nrsurf*nz + nzsurf*nr = (nr+1)*nz + nr*(nz+1) = 2*nr*nz + nr + nz
# if one radius is 0, this becomes = nr*nz + nr*(nz+1) = 2*nr*nz + nr
# Solution used here:
# use ir to iterate nr, use iy to iterate nrsurf, use iz to iterate nz, use ic to iterate nzsurf
# since only rsurf*z [iy,iz] and r*zsurf [ir,ic] pairs exist, when one pair is being written
# the other will be [-1,-1], hence the dimensions for the array are increased by an extra 1 to prevent overlap
#meta['nr_original'] = meta['nr']
#meta['nz_original'] = meta['nz']
#meta['ny_original'] = meta['ny']
##meta['nc_original'] = meta['nc']
#meta['ny'] = meta['nr'] + 1 + 1
#meta['nc'] = meta['nz'] + 1 + 1
#meta['nr'] = meta['nr'] + 1
#meta['nz'] = meta['nz'] + 1
if meta['tally_type'] == '[T-Point]':
if 'mesh' not in meta:
if 'point' in meta:
meta['mesh'] = 'point'
meta['nreg'] = meta['point']
elif 'ring' in meta:
meta['mesh'] = 'ring'
meta['nreg'] = meta['ring']
axes_1D = ['eng','reg','x','y','z','r','t','cos','the','mass','charge','let','tet','eng1','eng2','sed','rad','deg','act']
axes_2D = ['xy','yz','zx','rz','chart','dchain','t-eng','eng-t','t-e1','e1-t','t-e2','e2-t','e12','e21','xz','yx','zy','zr']
axes_ital_1D = [3, 0, 0, 1, 2, 0, 4, 5, 5, 8, 8, 6, 0, 3, 8, 3, 5, 5, 8]
axes_ital_2D = [ [0,1],[1,2],[2,0],[0,2],[None,None],[None,None],[4,3],[3,4],[4,3],[3,4],[4,8],[8,4],[3,8],[8,3],[0,2],[1,0],[2,1],[2,0]]
if meta['axis'] in axes_1D:
meta['axis_dimensions'] = 1
meta['axis_index_of_tally_array'] = axes_ital_1D[axes_1D.index(meta['axis'])]
elif meta['axis'] in axes_2D:
meta['axis_dimensions'] = 2
meta['axis_index_of_tally_array'] = axes_ital_2D[axes_2D.index(meta['axis'])]
else:
print("WARNING: axis value of ",meta['axis']," is not in list of known/registered values")
meta['axis_dimensions'] = None
meta['axis_index_of_tally_array'] = None
# Now extract portion of metadata only available from tally content
if meta['mesh'] == 'reg' or meta['mesh'] == 'tet':
num, reg, vol = [], [], []
if meta['axis']=='reg' or meta['axis']=='tet': # get number of regions and region data from first block of tally content
outblock = tally_content[0]
in_reg_list = False
for line in outblock:
if '#' in line and ' num ' in line:
cols = line[1:].split()
#print(cols)
in_reg_list = True
continue
if len(line.split()) == 0 or '{' in line:
in_reg_list = False
if in_reg_list:
vals = line.split()
if meta['tally_type'] == '[T-Cross]':
num.append(vals[0])
reg.append(vals[0])
vol.append(vals[1])
else:
num.append(vals[0])
reg.append(vals[1])
vol.append(vals[2])
else: # scan output for region numbers:
regcount = 0
for outblock in tally_content:
for line in outblock:
if 'reg =' in line or 'reg =' in line:
eq_strs = split_str_of_equalities(line[1:])
reg_eq_str = ''
for eqsi in eq_strs:
if 'reg' in eqsi:
reg_eq_str = eqsi
break
regnum = reg_eq_str.split('=')[1].strip()
#regnum = line.strip().split('reg =')[1].strip().replace("'",'')
if regnum not in reg:
regcount += 1
num.append(regcount)
reg.append(regnum)
vol.append(None)
continue
if meta['mesh'] == 'reg':
meta['reg_serial_num'] = num
meta['reg_num'] = reg
if meta['tally_type'] == '[T-Cross]':
meta['reg_area'] = vol
else:
meta['reg_volume'] = vol
meta['nreg'] = len(reg)
elif meta['mesh'] == 'tet':
meta['tet_serial_num'] = num
meta['tet_num'] = reg
meta['reg_num'] = reg
#meta['tet_volume'] = vol
if meta['tally_type'] == '[T-Cross]':
meta['tet_area'] = vol
else:
meta['tet_volume'] = vol
meta['ntet'] = len(reg)
#if meta['tally_type'] == '[T-Cross]':
# meta['reg_groups'] = reg
elif meta['mesh'] == 'tet':
num, reg, vol = [], [], []
if meta['axis'] == 'tet':
pass
else:
pass
print('mesh=tet has not been tested!')
meta['ntet'] = 0
axis1_label = ''
axis2_label = ''
value_label = ''
hc_passed = False # passed colorbar definition line
outblock = tally_content[0]
for line in outblock:
if len(line) == 0: continue
if line[:2] == 'x:':
axis1_label = line[2:].strip()
if line[:2] == 'y:':
if meta['axis_dimensions'] == 1:
value_label = line[2:].strip()
#break
elif meta['axis_dimensions'] == 2:
if hc_passed: # second instance of y:
value_label = line[2:].strip()
break
else: # first instance of y:
axis2_label = line[2:].strip()
hc_passed = True
#if line[:3] == 'hc:':
# hc_passed = True
h_line_str = ''
if line[0] == 'h' and (line[1] == ':' or line[2] == ':'):
if meta['axis_dimensions'] == 1:
ndatacol = line.count(' y')
if ndatacol != 1: # multiple columns are present "samepage"
# get first string with y
col_groups = parse_group_string(line)
i_first_y = next((i for i,v in enumerate(col_groups) if v[0]=='y'), None) # index of first column with "y"
first_data_col_header = col_groups[i_first_y][2:]
for m in mesh_types:
if first_data_col_header[0] == m:
if m == 'e':
meta['samepage'] = 'eng'
elif m == 'r':
if first_data_col_header[:3] == 'reg':
meta['samepage'] = 'reg'
else:
meta['samepage'] = m
elif m == 'l':
meta['samepage'] = 'let'
elif m == 'a':
if first_data_col_header[:3] not in ['all','alp']:
meta['samepage'] = 'the' # or cos
else:
meta['samepage'] = m
if meta['samepage'] == 'part': # still is default value
# double check to see if it could be region numbers vs particle names
if ndatacol != meta['npart']:
if 'num_reg_groups' in meta and ndatacol == meta['num_reg_groups']:
meta['samepage'] = 'reg'
else:
print('"samepage" was not correctly identified; needs to be implemented')
if meta['samepage'] == 'reg':
hcols = parse_group_string(line[3:])
num, reg, vol = [], [], []
reg_ser_num = 1
for hcol in hcols:
if hcol[0] == 'y':
num.append(reg_ser_num)
reg_ser_num += 1
reg.append(hcol.split(')')[0].replace('y(reg',''))
vol.append(None)
meta['reg_serial_num'] = num
meta['reg_num'] = reg
meta['reg_volume'] = vol
meta['nreg'] = len(reg)
break
meta['axis1_label'] = axis1_label
meta['axis2_label'] = axis2_label
meta['value_label'] = value_label
# Now do any final overrides for specific tallies / circumstances
if meta['tally_type'] == '[T-Deposit2]':
meta['nreg'] = 1
meta['reg_serial_num'] = [1]
meta['reg_num'] = ['1']
meta['reg_volume'] = [None]
if meta['num_reg_groups'] > 1:
meta['num_reg_groups'] = 1
meta['reg_groups'] = [meta['reg_groups'][0] + ' ' + meta['reg_groups'][1]]
if meta['tally_type'] == '[T-Heat]':
if 'npart' not in meta or meta['npart'] == None: meta['npart'] = 1
if 'part_groups' not in meta: meta['part_groups'] = ['all']
return meta
def parse_tally_content(tdata,meta,tally_blocks,is_err_in_separate_file,err_mode=False):
'''
Description:
Parses the PHITS tally output content section and extract its results
Dependencies:
- `split_str_of_equalities` (function within the "PHITS tools" package)
- `parse_group_string` (function within the "PHITS tools" package)
- `data_row_to_num_list` (function within the "PHITS tools" package)
Inputs:
- `tdata` = 10-dimensional NumPy array of zeros of correct size to hold tally output/results
- `meta` = Munch object / dictionary containing tally metadata
- `tally_blocks` = blocks of tally output as outputted by the `split_into_header_and_content` function
- `is_err_in_separate_file` = Boolean denoting whether the tally's relative errors are located in a separate file
- `err_mode` = Boolean (D=`False`) used for manually forcing all read values to be regarded as relative uncertainties
as is necessary when processing dedicated *_err files.
Outputs:
- `tdata` = updated `tdata` array containing read/extracted tally results
'''
global ir, iy, iz, ie, it, ia, il, ip, ic, ierr
global ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max
ierr = 0
if is_err_in_separate_file and err_mode:
ierr = 1
mesh_kind_chars = ['e', 't', 'x', 'y', 'z', 'r', 'a', 'l']
mesh_kind_iax = [3, 4, 0, 1, 2, 0, 5, 6]
tdata_ivar_strs = ['ir', 'iy', 'iz', 'ie', 'it', 'ia', 'il', 'ip', 'ic']
ir, iy, iz, ie, it, ia, il, ip, ic = 0, 0, 0, 0, 0, 0, 0, 0, 0
ignored_eq_strs = ['axis','axs','ar','rr','m jm','Z','cmax nmax']
replace_eq_strs_dict = {'ang':'a'}
ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata)
axes_1D = ['eng', 'reg', 'x', 'y', 'z', 'r', 't', 'cos', 'the', 'mass', 'charge', 'let', 'tet', 'eng1', 'eng2',
'sed', 'rad', 'deg', 'act']
axes_2D = ['xy', 'yz', 'zx', 'rz', 'chart', 'dchain',
't-eng', 'eng-t', 't-e1', 'e1-t', 't-e2', 'e2-t',
'e12', 'e21', 'xz', 'yx', 'zy', 'zr']
axes_ital_1D = [3, 0, 0, 1, 2, 0, 4, 5, 5, 8, 8, 6, 0, 3, 8,
3, 5, 5, 8]
axes_ital_2D = [[0, 1], [1, 2], [2, 0], [0, 2], [None, None], [None, None],
[4, 3], [3, 4], [4, 3], [3, 4], [4, 8], [8, 4],
[3, 8], [8, 3], [0, 2], [1, 0], [2, 1], [2, 0]]
ierr_mod = 0 # add to ierr for weird [T-Cross], mesh=r-z, enclos=0 case
banked_uninterpreted_lines = [] # store lines with equalities that may be useful but are skipped owing to being a bit exceptional
i_metastable = 0
ZZZAAAM_list = []
if meta['axis_dimensions']==1:
for bi, block in enumerate(tally_blocks):
hli, fli = 0,0
ierr_mod = 0
hli_found = False
for li, line in enumerate(block):
if len(line) == 0: continue
if line[:2].lower() == 'h:': # start of data is here
hli = li
hli_found = True
continue
if hli_found and (line[:12] == '# sum over' or line[:7] == '# sum' or line[:5] == '#----' or line[:12] == '# overflow' or (len(block[li-1]) == 0 and hli != 0 and li>hli+2) or "'" in line or '{' in line):
fli = li
if (len(block[li-1]) == 0 and hli != 0 and li>hli+2): fli = li - 1 # triggered by blank line after data
#if "'" in line or '{' in line:
# fli = li-1
break
data_header = block[:hli]
data_table = block[hli:fli]
data_footer = block[fli:]
if bi == len(tally_blocks) - 1:
ffli = len(data_footer)
for li, line in enumerate(data_footer):
if line[:37] == '# Information for Restart Calculation':
ffli = li
break
data_footer = data_footer[:ffli]
# print(data_header)
#print(data_table)
# print(data_footer)
hash_line_already_evaluated = False
# try to get relevant indices data from header and footer blocks
for li, line in enumerate(data_header+data_footer):
if len(line) == 0: continue
if '=' in line and (line[0] == "'" or (line[0] == "#" and ('no.' in line or 'i' in line or 'reg' in line or 'part' in line))):
if line[0] == "#":
hash_line_already_evaluated = True
elif line[0] == "'" and hash_line_already_evaluated:
if meta['samepage'] == 'part':
continue # '-starting lines tend to have more problematic formatting, best skipped if possible
elif meta['npart'] == 1:
continue # can still skip if only one particle group tallied
else:
pass # but this needs to be parsed if not using samepage = part and npart > 1
parts = split_str_of_equalities(line)
#print(line)
for part in parts:
mesh_char = part.split('=')[0].strip().replace('i','')
#print(mesh_char)
if mesh_char == 'no.':
if '***' in part and bi<998:
break # this is a bugged line (or "no." is 4+ digits, which is fine)
continue
elif mesh_char == 'part.' or mesh_char == 'partcle' or mesh_char == 'part':
part_grp_name = part.split('=')[1].strip()
if part_grp_name in meta['part_groups']:
ip = (meta['part_groups']).index(part_grp_name)
elif part_grp_name in meta['part_serial_groups']:
ip = (meta['part_serial_groups']).index(part_grp_name)
else:
raise ValueError('ERROR! Particle "'+part_grp_name+'" could not be identified.')
elif mesh_char == 'reg':
regnum = part.split('=')[1].strip()
ir = (meta['reg_num']).index(regnum)
elif mesh_char == 'pont' or mesh_char == 'rng': # [T-Point]
value_str = part.split('=')[1].strip()
ir = int(value_str) - 1
elif mesh_char == 'e1': # [T-Deposit2]
value_str = part.split('=')[1].strip()
ie = int(value_str) - 1
elif mesh_char == 'e2': # [T-Deposit2]
value_str = part.split('=')[1].strip()
ic = int(value_str) - 1
elif mesh_char in mesh_kind_chars or mesh_char in replace_eq_strs_dict:
if mesh_char in replace_eq_strs_dict:
mesh_char = replace_eq_strs_dict[mesh_char]
if 'i'+mesh_char not in part: continue # only looking for indices for meshes, not values
imesh = mesh_kind_chars.index(mesh_char)
itdata_axis = mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value_str = part.split('=')[1].strip()
if ' - ' in value_str:
vals = value_str.split('-')
if int(vals[0]) == int(vals[1]):
value_str = vals[0]
else: # samepage axis
value_str = vals[0] # this will be overwritten later
value = str(int(value_str)-1)
exec(tdata_ivar_str + ' = ' + value, globals())
elif mesh_char in ignored_eq_strs:
continue
elif meta['tally_type']=='[T-Cross]':
if meta['mesh'] == 'xyz' and mesh_char=='z surf':
#imesh = mesh_kind_chars.index('z')
itdata_axis = 2 #mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value_str = part.split('=')[1].strip()
value = str(int(value_str) - 1)
exec(tdata_ivar_str + ' = ' + value, globals())
elif meta['mesh'] == 'r-z':
if mesh_char=='r surf':
itdata_axis = 0 # mesh_kind_iax[imesh]
#itdata_axis = 1 # set to iy
ierr_mod = int(ierr_max/2)
#ir, ic = -1, -1
# imesh = mesh_kind_chars.index('y')
elif mesh_char == 'z surf':
itdata_axis = 2 # mesh_kind_iax[imesh]
#itdata_axis = 8 # set to ic
ierr_mod = 0
#iy, iz = -1, -1
# imesh = mesh_kind_chars.index('c')
else:
raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+'] found')
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value_str = part.split('=')[1].strip()
if ' - ' in value_str:
vals = value_str.split('-')
if int(vals[0]) == int(vals[1]):
value_str = vals[0]
else: # samepage axis
value_str = vals[0] # this will be overwritten later
value = str(int(value_str) - 1)
exec(tdata_ivar_str + ' = ' + value, globals())
else:
raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+ '] found')
elif meta['tally_type'] == '[T-Heat]':
banked_uninterpreted_lines.append(line)
else:
raise ValueError('ERROR! Unregistered potential index ['+part.split('=')[0].strip()+'] found')
if in_debug_mode:
print('\tcontent block',bi,', indices:', ir, iy, iz, ie, it, ia, il, ip, ic)
# extract data from table
# determine meaning of table rows
row_ivar = tdata_ivar_strs[meta['axis_index_of_tally_array']]
# determine meaning of table columns
hcols = parse_group_string(data_table[0][3:])
nhcols = len(hcols)
col_names_line_str = data_table[1][1:]
icol_mod = 0 # account for weirdness in column presence/absence
if 'r surface position' in col_names_line_str:
icol_mod = -1
ierr_mod = int(ierr_max / 2)
# Test for error in hcols
num_data_vals_in_first_row = len(data_row_to_num_list(data_table[2])) # first row of data
if num_data_vals_in_first_row != nhcols:
if num_data_vals_in_first_row == (nhcols+1):
# most likely issue is hcol string is missing the "n" for the ?-lower column
nhcols = nhcols + 1
icol_mod = 1
is_col_data = np.full(nhcols,False)
data_col_indices = []
is_col_err = np.full(nhcols,False)
err_col_indices = []
for iii in range(len(hcols)):
if hcols[iii][0] == 'y':
is_col_data[iii+icol_mod] = True
is_col_err[iii+1+icol_mod] = True
data_col_indices.append(iii+icol_mod)
err_col_indices.append(iii+1+icol_mod)
#print(is_col_data)
#print(is_col_err)
cols = data_table[1][1:].strip().split()
ncols = len(cols)
ndata_cols = np.sum(is_col_data) # number of data values per row
# determine what variable this corresponds to, should be val of samepage
# by default, this is usually particles (samepage = part by default)
if meta['samepage'] == 'part':
if meta['npart'] != ndata_cols:
raise ValueError('ERROR! samepage number of particle types ('+str(meta['npart'])+') not equal to number of data columns y(part) = '+str(ndata_cols))
data_ivar = 'ip'
data_ivar_indices = [j for j in range(ndata_cols)]
else: # figure out what axis samepage is on
if meta['samepage'] not in axes_1D:
raise ValueError('ERROR! samepage parameter ('+str(meta['samepage'])+') must be "part" or one of valid options for "axis" parameter')
data_ivar = tdata_ivar_strs[axes_ital_1D[axes_1D.index(meta['samepage'])]]
if ndata_cols != eval(data_ivar+'_max'):
if meta['tally_type']=='[T-Cross]' and ndata_cols+1 == eval(data_ivar+'_max'):
# This is fine; for T-Cross, ndata cols can be one less than max length...
pass
elif meta['tally_type']=='[T-Cross]' and data_ivar == 'ir' and ndata_cols+2 == eval(data_ivar+'_max'):
# This is fine; for T-Cross, ndata cols for radius can be two less than max length if rmin=0...
pass
else:
raise ValueError('ERROR! number of data columns ('+str(ndata_cols)+') not equal to tally array dimension for '+str(data_ivar)+', '+str(eval(data_ivar+'_max')))
data_ivar_indices = [j for j in range(ndata_cols)]
#print(cols)
#print(ndata_cols)
for li, line in enumerate(data_table[2:]):
if len(line)==0: continue
#print(line)
rowi = li
exec(row_ivar + '=' + str(rowi),globals())
#print(row_ivar + '=' + str(rowi))
values = data_row_to_num_list(line)
dcoli = 0
ecoli = 0
for vi, value in enumerate(values):
if is_col_data[vi]:
exec(data_ivar + '=' + str(dcoli),globals())
#print(data_ivar + '=' + str(dcoli))
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0+ierr_mod] = value
dcoli += 1
if is_col_err[vi]:
exec(data_ivar + '=' + str(ecoli),globals())
#print(data_ivar + '=' + str(ecoli))
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1+ierr_mod] = value
ecoli += 1
elif meta['axis_dimensions']==2:
for bi, block in enumerate(tally_blocks):
hli, fli = 0 , 0
data_keyword_found = False
for li, line in enumerate(block):
if meta['2D-type'] in [1, 2, 3, 6, 7]:
if len(line) == 0: continue
if line[:3].lower() in ['hc:', 'h2:', 'hd:', 'hc2']: # start of data is here
hli = li
if line[:12] == '#-----------':
fli = li
#if bi != len(tally_blocks) - 1:
break
elif meta['2D-type'] == 4:
if line == '' and hli != 0:
fli = li
#if bi != len(tally_blocks) - 1:
break
elif line == '': # start of data is here
hli = li
elif meta['2D-type'] == 5:
if 'data' in line:
hli = li + 3
if line == '' and hli != 0 and li>hli+2:
fli = li
#if bi != len(tally_blocks) - 1:
break
data_header = block[:hli]
data_table = block[hli:fli]
data_footer = block[fli:]
#print(data_header)
#print(data_table)
#print(data_footer)
hash_line_already_evaluated = False
if bi == len(tally_blocks) - 1:
for li, line in enumerate(data_footer):
if line[:37] == '# Information for Restart Calculation':
ffli = li
break
data_footer = data_footer[:ffli]
# try to get relevant indices data from header block
for li, line in enumerate(data_header+data_footer): # +data_footer
if len(line) == 0: continue
#if 'reg =' in line:
# regnum = line.strip().split('reg =')[1].strip()
# ir = (meta.reg_num).index(regnum)
# # print(ir)
if '=' in line and (line[0] == "'" or (line[0] == "#" and ('no.' in line or 'i' in line or 'reg' in line or 'part' in line))):
if line[0] == "#":
hash_line_already_evaluated = True
elif line[0] == "'" and hash_line_already_evaluated:
if meta['samepage'] == 'part':
continue # '-starting lines tend to have more problematic formatting, best skipped if possible
elif meta['npart'] == 1:
continue # can still skip if only one particle group tallied
else:
pass # but this needs to be parsed if not using samepage = part and npart > 1
parts = split_str_of_equalities(line)
for part in parts:
mesh_char = part.split('=')[0].strip().replace('i', '')
#print(mesh_char)
if mesh_char == 'no.':
continue
elif mesh_char == 'part.' or mesh_char == 'partcle':
part_grp_name = part.split('=')[1].strip()
try:
ip = (meta['part_groups']).index(part_grp_name)
except:
ip = (meta['part_serial_groups']).index(part_grp_name)
elif mesh_char == 'reg': # and meta['samepage'] != 'reg':
regnum = part.split('=')[1].strip()
ir = (meta['reg_num']).index(regnum)
elif mesh_char == 'e1': # [T-Deposit2]
value_str = part.split('=')[1].strip()
ie = int(value_str) - 1
elif mesh_char == 'e2': # [T-Deposit2]
value_str = part.split('=')[1].strip()
ic = int(value_str) - 1
elif mesh_char in mesh_kind_chars or mesh_char in replace_eq_strs_dict:
if mesh_char in replace_eq_strs_dict:
mesh_char = replace_eq_strs_dict[mesh_char]
if 'i'+mesh_char not in part: continue # only looking for indices for meshes, not values
imesh = mesh_kind_chars.index(mesh_char)
itdata_axis = mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value = str(int(part.split('=')[1].strip()) - 1)
if mesh_char == 'l' and meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart':
i_metastable = int(value) + 1
il = 0
else:
exec(tdata_ivar_str + ' = ' + value, globals())
elif mesh_char in ignored_eq_strs:
continue
elif meta['tally_type']=='[T-Cross]':
ierr_mod = 0
if meta['mesh'] == 'xyz' and mesh_char=='z surf':
#imesh = mesh_kind_chars.index('z')
itdata_axis = 2 #mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value = str(int(part.split('=')[1].strip()) - 1)
exec(tdata_ivar_str + ' = ' + value, globals())
elif meta['mesh'] == 'r-z':
if mesh_char=='r surf':
# imesh = mesh_kind_chars.index('y')
itdata_axis = 0 #1 # mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value = str(int(part.split('=')[1].strip()) - 1)
exec(tdata_ivar_str + ' = ' + value, globals())
#ir, ic = -1, -1
ierr_mod = int(ierr_max / 2)
elif mesh_char=='z surf':
# imesh = mesh_kind_chars.index('c')
itdata_axis = 2 #8 # mesh_kind_iax[imesh]
tdata_ivar_str = tdata_ivar_strs[itdata_axis]
value = str(int(part.split('=')[1].strip()) - 1)
exec(tdata_ivar_str + ' = ' + value, globals())
iy, iz = -1, -1
ierr_mod = 0
else:
raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+'] found')
else:
raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+ '] found')
else:
raise ValueError('ERROR! Unregistered potential index ['+part.split('=')[0].strip()+'] found')
# Now read data_table, with formatting dependent on 2D-type, and can be inferred from last line of header
axis1_ivar = meta['axis_index_of_tally_array'][0]
axis2_ivar = meta['axis_index_of_tally_array'][1]
if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart': # this setting does not respect 2D-type and uses its own formatting
data_write_format_str = data_table[0][3:]
Z_y_segment = data_write_format_str.split(';')[0]
N_x_segment = data_write_format_str.split(';')[1]
Z_y_vals = Z_y_segment.replace('=','').replace('to','').replace('by','').replace('y','').strip().split()
N_x_vals = N_x_segment.replace('=','').replace('to','').replace('by','').replace('x','').strip().split()
Z_y_max, Z_y_min, Z_y_increment = int(Z_y_vals[0]), int(Z_y_vals[1]), int(Z_y_vals[2])
N_x_max, N_x_min, N_x_increment = int(N_x_vals[1]), int(N_x_vals[0]), int(N_x_vals[2])
#print(Z_y_max, Z_y_min, Z_y_increment, N_x_max, N_x_min, N_x_increment )
elif meta['2D-type'] != 4:
data_write_format_str = data_header[-2][1:]
if 'data' not in data_write_format_str:
for line in data_header[::-1]:
if 'data' in line:
data_write_format_str = line[1:]
break
#print(data_write_format_str)
if 'data' not in data_write_format_str:
# failed to find a "data" line telling us how the values are ordered
# have to make guesses about output ordering...
# axis variable should give us a hint
axis = meta['axis']
if 'eng' in axis or 'e1' in axis or 'e2' in axis:
if axis == 'e12' or axis == 'e21':
ax1_ivar = 'ie'
ax2_ivar = 'ie'
else: # energy vs time
if axis[0] == 't':
ax1_ivar = 'it'
ax2_ivar = 'ie'
else:
ax1_ivar = 'ie'
ax2_ivar = 'it'
else:
if 'axis1_label' in meta and meta['axis1_label'][0] in axis:
# we know horizontal axis variable
ax1_ivar = 'i' + meta['axis1_label'][0]
ax2_ivar = 'i' + axis.replace(meta['axis1_label'][0],'')
else:
ax1_ivar = 'i' + meta['axis'][1]
ax2_ivar = 'i' + meta['axis'][0]
else:
# We can, with confidence, determine output value ordering :)
for dsi in data_write_format_str.split():
if 'data' in dsi:
data_index_str = dsi
ax_vars = data_index_str.replace('data','').replace('(','').replace(')','')
#print(data_index_str)
#print(ax_vars)
ax1_ivar, ax2_ivar = ax_vars.split(',')[:2]
ax1_ivar = 'i' + ax1_ivar
ax2_ivar = 'i' + ax2_ivar
#print(data_write_format_str)
else: # 2D-type = 4
cols = data_table[1][1:].split()
ax1_ivar, ax2_ivar = cols[0], cols[1]
ax1_ivar = 'i' + ax1_ivar
ax2_ivar = 'i' + ax2_ivar
# manually fix [T-Deposit2] axes
if meta['tally_type'] == '[T-Deposit2]':
if meta['axis'] == 'e12':
ax1_ivar, ax2_ivar = 'ie', 'ic'
elif meta['axis'] == 'e21':
ax1_ivar, ax2_ivar = 'ic', 'ie'
elif meta['axis'] == 't-e1':
ax1_ivar, ax2_ivar = 'it', 'ie'
elif meta['axis'] == 't-e2':
ax1_ivar, ax2_ivar = 'it', 'ic'
elif meta['axis'] == 'e1-t':
ax1_ivar, ax2_ivar = 'ie', 'it'
elif meta['axis'] == 'e2-t':
ax1_ivar, ax2_ivar = 'ic', 'it'
if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart':
remaining_ndata_to_read = (Z_y_max - Z_y_min + 1) * (N_x_max - N_x_min + 1)
else:
# check if this is one of the backwards instances
expected_ax1_ivar = tdata_ivar_strs[axis1_ivar]
expected_ax2_ivar = tdata_ivar_strs[axis2_ivar]
if meta['mesh']=='xyz':
if expected_ax1_ivar == 'ir': expected_ax1_ivar = 'ix'
if expected_ax2_ivar == 'ir': expected_ax1_ivar = 'ix'
if ax1_ivar==expected_ax1_ivar and ax2_ivar==expected_ax2_ivar:
pass # all is correct as is
elif ax2_ivar == expected_ax1_ivar and ax1_ivar == expected_ax2_ivar:
axis1_ivar_temp = axis1_ivar
axis1_ivar = axis2_ivar
axis2_ivar = axis1_ivar_temp
#axis1_ivar = tdata_ivar_strs.index(ax1_ivar)
#axis2_ivar = tdata_ivar_strs.index(ax2_ivar)
#print('backwards!')
else:
raise ValueError('ERROR! Unknown axes ('+ax1_ivar+' '+ax2_ivar +
') encountered that did not match expected axes (' +
tdata_ivar_strs[meta['axis_index_of_tally_array'][0]]+' ' +
tdata_ivar_strs[meta['axis_index_of_tally_array'][1]]+')')
axis1_ivar_str = tdata_ivar_strs[axis1_ivar]
axis2_ivar_str = tdata_ivar_strs[axis2_ivar]
axis1_size = np.shape(tdata)[axis1_ivar]
axis2_size = np.shape(tdata)[axis2_ivar]
ndata_to_read = axis1_size*axis2_size
#print(axis1_ivar_str,axis2_ivar_str)
#print(axis1_size,axis2_size,ndata_to_read)
remaining_ndata_to_read = ndata_to_read
iax1 = 0
iax2 = axis2_size - 1
if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart':
#Z_y_max, Z_y_min, Z_y_increment # big, 1, -1
#N_x_max, N_x_min, N_x_increment # big, 1, 1
current_Z = Z_y_max
current_N = N_x_min - N_x_increment
ic = 0
for line in data_table[1:]:
values = data_row_to_num_list(line)
for value in values:
remaining_ndata_to_read += -1
current_N += N_x_increment
if current_N > N_x_max:
current_N = N_x_min
current_Z += Z_y_increment
#print('Z=',current_Z,', N=',current_N)
if value != 0:
ZZZAAAM = 10000*current_Z + 10*(current_Z+current_N) + i_metastable
if ZZZAAAM not in ZZZAAAM_list:
ic = len(ZZZAAAM_list)
ZZZAAAM_list.append(ZZZAAAM)
else:
ic = ZZZAAAM_list.index(ZZZAAAM)
#print(ic, i_metastable)
#print(ic,value)
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value
if remaining_ndata_to_read <= 0:
break
elif meta['2D-type'] in [1,2,3,6,7]:
for line in data_table[1:]:
values = data_row_to_num_list(line)
#print(line)
for value in values:
exec(axis1_ivar_str + ' = ' + str(iax1), globals())
exec(axis2_ivar_str + ' = ' + str(iax2), globals())
#print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr, '\t', value)
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value
remaining_ndata_to_read += -1
#print(iax1, iax2)
iax1 += 1
if iax1 == axis1_size:
iax1 = 0
iax2 += -1
if remaining_ndata_to_read <= 0:
break
elif meta['2D-type'] == 4:
iax2 = 0
for line in data_table[2:]:
values = data_row_to_num_list(line)
value = values[2]
value_err = values[3]
exec(axis1_ivar_str + ' = ' + str(iax1), globals())
exec(axis2_ivar_str + ' = ' + str(iax2), globals())
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0 + ierr_mod] = value
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1 + ierr_mod] = value_err
# print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr,'\t',value)
remaining_ndata_to_read += -1
# print(iax1, iax2)
iax1 += 1
if iax1 == axis1_size:
iax1 = 0
iax2 += 1
if remaining_ndata_to_read <= 0:
break
elif meta['2D-type'] == 5:
for line in data_table[2:]:
values = data_row_to_num_list(line)
#print(line)
for vi, value in enumerate(values):
if vi==0: continue # header column
exec(axis1_ivar_str + ' = ' + str(iax1), globals())
exec(axis2_ivar_str + ' = ' + str(iax2), globals())
#print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr, '\t', value)
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value
remaining_ndata_to_read += -1
# print(iax1, iax2)
iax1 += 1
if iax1 == axis1_size:
iax1 = 0
iax2 += -1
if remaining_ndata_to_read <= 0:
break
else:
raise ValueError('ERROR! unsupported 2D-type of '+str(meta['2D-type'])+' provided; legal values are [1,2,3,4,5,6,7]')
else:
raise ValueError(str(meta['axis_dimensions'])+'axis dimensions is unknown, ERROR!')
if len(banked_uninterpreted_lines) != 0:
print('The following potentially useful output lines were found but not stored anywhere:')
for line in banked_uninterpreted_lines:
print('\t'+line)
return_updated_metadata_too = False
if meta['tally_type'] == '[T-Yield]' and meta['axis'] in ['chart','charge','mass']:
return_updated_metadata_too = True
if meta['axis'] == 'chart':
meta['nuclide_ZZZAAAM_list'] = ZZZAAAM_list
meta['nuclide_isomer_list'] = [ZZZAAAM_to_nuclide_plain_str(i) for i in ZZZAAAM_list]
nc_max = len(ZZZAAAM_list) #+ 1
meta['nc'] = nc_max
tdata = tdata[:,:,:,:,:,:,:,:,:nc_max,:]
elif meta['axis'] == 'charge' or meta['axis'] == 'mass':
ic_axis_tdata_sum = tdata.sum(axis=(0,1,2,3,4,5,6,7,9))
nc_max = np.max(np.nonzero(ic_axis_tdata_sum)) + 1
meta['nc'] = nc_max
tdata = tdata[:, :, :, :, :, :, :, :, :nc_max, :]
if return_updated_metadata_too:
return tdata, meta
else:
return tdata
def extract_data_from_header_line(line):
'''
Description:
Extract a "key" and its corresponding value from a PHITS tally output header line
Dependencies:
- `is_number` (function within the "PHITS tools" package)
Inputs:
- `line` = string to be processed
Outputs:
- `key` = a string "key" to become a key in the metadata dictionary
- `value` = corresponding value they "key" is equal to; dtype is string, int, or float
'''
if '#' in line:
info, trash = line.split('#',1)
else:
info = line
key, value = info.split('=')
key = key.strip()
value = value.strip()
if is_number(value):
if '.' in value:
value = float(value)
else:
value = int(value)
return key, value
def split_str_of_equalities(text):
'''
Description:
Extract relevant regions, indices, etc. from somewhat inconsistently formatted lines in PHITS tally output content section.
Dependencies:
- `is_number` (function within the "PHITS tools" package)
Inputs:
- `text` = string to be processed
Outputs:
- `equalities_str_list` = list of strings of equalities each of the format "key = value"
'''
equalities_str_list = []
original_text = text
#if text[0] == "'": # more loosely formatted text
# problem_strs = ['tot DPA']
if text[0] == "#" and 'no. =***' in text and text[-1]=='=': return [] # skip broken lines
text = text.replace("'",'').replace(',',' ').replace('#','').replace('=',' = ').replace('***','999999999')
text_pieces = text.split()
#i_equal_sign = [i for i, x in enumerate(text_pieces) if x == "="]
is_i_equal_sign = [x=='=' for x in text_pieces]
#i_is_number = [i for i, x in enumerate(text_pieces) if is_number(x)]
is_i_number = [is_number(x) for x in text_pieces]
#num_equalities = len(i_equal_sign)
#remaining_equalities = num_equalities
equality_str = ''
# the only condition enforced is that the last item in each value be numeric or )
current_equality_contains_equalsign = False
for i in reversed(range(len(text_pieces))): # easiest to build from right to left
equality_str = text_pieces[i] + ' ' + equality_str
if is_i_equal_sign[i]:
current_equality_contains_equalsign = True
elif current_equality_contains_equalsign: # looking to terminate if next item is numeric
if i==0 or (is_i_number[i-1] or text_pieces[i-1][-1]==')'): # either final equality completed or next item belongs to next equality
equalities_str_list.insert(0,equality_str.strip())
equality_str = ''
current_equality_contains_equalsign = False
if '(' in text: # need to break up potential (ia,ib) pairs
new_eq_str_list = []
for x in equalities_str_list:
if '(' in x:
keys, values = x.split('=')
keys = keys.strip().replace('(','').replace(')','').split()
values = values.strip().replace('(','').replace(')','').split()
for i in range(len(keys)):
new_eq_str = keys[i].strip() + ' = ' + values[i].strip()
new_eq_str_list.append(new_eq_str)
else:
new_eq_str_list.append(x)
equalities_str_list = new_eq_str_list
#print(equalities_str_list)
return equalities_str_list
def parse_group_string(text):
'''
Description:
Separate "groups" in a string, wherein a group is a standalone value or a series of values inside parentheses.
Inputs:
- `text` = string to be processed
Outputs:
- `groups` = a list of strings extracted from `text`
'''
# returns list of items from PHITS-formatted string, e.g. w/ ()
parts = text.strip().split()
#print(parts)
groups = []
curly_vals = []
in_brackets_group = False
in_curly_brace_group = False
num_group_members = 0
for i in parts:
if '(' in i and ')' in i:
in_brackets_group = False
groups.append(i)
elif '(' in i:
in_brackets_group = True
groups.append(i)
elif ')' in i:
in_brackets_group = False
num_group_members = 0
groups[-1] += i
elif '{' in i:
in_curly_brace_group = True
curly_vals = []
elif '}' in i:
in_curly_brace_group = False
curly_int_strs = [str(j) for j in range(int(curly_vals[0]), int(curly_vals[-1])+1)]
curly_vals = []
groups += curly_int_strs
else:
if in_brackets_group or in_curly_brace_group:
if in_brackets_group:
if num_group_members>0: groups[-1] += ' '
groups[-1] += i
num_group_members += 1
if in_curly_brace_group:
if i != '-':
curly_vals.append(i)
else:
groups.append(i)
#print(groups)
return groups
def initialize_tally_array(tally_metadata,include_abs_err=True):
'''
Description:
Initializes main tally data array in which tally results will be stored when read
Dependencies:
- `import numpy as np`
Inputs:
- `tally_metadata` = Munch object / dictionary containing tally metadata
- `include_abs_err` = a Boolean (D=`True`) on whether absolute error will be calculated; the final dimension of `tdata` is
`3/2` if this value is `True/False`
Outputs:
- `tdata` = 10-dimensional NumPy array of zeros of correct size for holding tally results
'''
ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max = 1, 1, 1, 1, 1, 1, 1, 1, 1
if include_abs_err:
ierr_max = 3
else:
ierr_max = 2
if tally_metadata['mesh'] == 'reg':
ir_max = tally_metadata['nreg']
elif tally_metadata['mesh'] == 'xyz':
ir_max = tally_metadata['nx']
iy_max = tally_metadata['ny']
iz_max = tally_metadata['nz']
elif tally_metadata['mesh'] == 'r-z':
ir_max = tally_metadata['nr']
iz_max = tally_metadata['nz']
if 'ny' in tally_metadata and tally_metadata['ny'] != None: iy_max = tally_metadata['ny']
if 'nc' in tally_metadata and tally_metadata['nc'] != None: ic_max = tally_metadata['nc']
elif tally_metadata['mesh'] == 'tet':
ir_max = tally_metadata['ntet']
elif tally_metadata['mesh'] == 'point' or tally_metadata['mesh'] == 'ring':
ir_max = tally_metadata['nreg']
else:
raise ValueError('ERROR! Unknown geometry mesh:'+ str(tally_metadata['mesh']))
if tally_metadata['na'] != None: ia_max = tally_metadata['na']
if tally_metadata['nt'] != None: it_max = tally_metadata['nt']
if tally_metadata['nl'] != None: il_max = tally_metadata['nl']
if 'nc' in tally_metadata and tally_metadata['nc'] != None: ic_max = tally_metadata['nc']
#if 'npart' in tally_metadata and tally_metadata.npart != None: ip_max = tally_metadata.np
if tally_metadata['ne'] == None:
if tally_metadata['tally_type'] == '[T-Deposit2]':
if 'ne1' in tally_metadata:
ie_max = tally_metadata['ne1']
if 'ne2' in tally_metadata:
ic_max = tally_metadata['ne2']
elif 'e1' in tally_metadata['axis'] or 'e2' in tally_metadata['axis']: # This should now be redundant?
if tally_metadata['axis'] == 'e12':
ie_max = tally_metadata['ne1']
ic_max = tally_metadata['ne2']
elif tally_metadata['axis'] == 'e21':
ie_max = tally_metadata['ne1']
ic_max = tally_metadata['ne2']
elif 'e1' in tally_metadata['axis'] or 'eng1' in tally_metadata['axis']:
ie_max = tally_metadata['ne1']
if 'ne2' in tally_metadata:
ic_max = tally_metadata['ne2']
elif 'e2' in tally_metadata['axis'] or 'eng2' in tally_metadata['axis']:
ic_max = tally_metadata['ne2']
if 'ne1' in tally_metadata:
ie_max = tally_metadata['ne1']
else:
if 'ne1' in tally_metadata:
ie_max = tally_metadata['ne1']
if 'ne2' in tally_metadata:
ic_max = tally_metadata['ne2']
else:
ie_max = tally_metadata['ne']
ip_max = tally_metadata['npart']
if tally_metadata['tally_type'] == '[T-Cross]' and tally_metadata['mesh'] == 'r-z':
if 'enclos' in tally_metadata and tally_metadata['enclos'] == 1:
pass
else: # enclos = 0 case
ierr_max = 2*ierr_max
if tally_metadata['tally_type'] == '[T-Yield]':
if tally_metadata['axis'] == 'charge':
ic_max = 130
elif tally_metadata['axis'] == 'mass':
ic_max = 320
elif tally_metadata['axis'] == 'chart':
if int(tally_metadata['mxnuclei']) == 0:
ic_max = 10000
else:
ic_max = int(tally_metadata['mxnuclei'])
if tally_metadata['tally_type'] == '[T-Interact]' and tally_metadata['axis'] == 'act':
ic_max = 100
if 'maxact' in tally_metadata:
ic_max = tally_metadata['maxact']
if in_debug_mode:
dims_str = 'tally dims: nr={:g}, ny={:g}, nz={:g}, ne={:g}, nt={:g}, na={:g}, nl={:g}, np={:g}, nc={:g}, nerr={:g}'
print(dims_str.format(ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max))
tally_data = np.zeros((ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max))
return tally_data
def data_row_to_num_list(line):
'''
Description:
Extract numeric values from line of text from PHITS tally output content section
Dependencies:
- `is_number` (function within the "PHITS tools" package)
Inputs:
- `line` = string to be processed
Outputs:
- `values` = a list of ints and/or floats of numeric values in `line`
'''
value_strs = line.strip().split()
values = []
for value in value_strs:
if is_number(value):
if '.' in value:
value = float(value)
else:
value = int(value)
values.append(value)
return values
def calculate_tally_absolute_errors(tdata):
'''
Description:
Calculates the absolute uncertainty for every value in the PHITS tally data array
Inputs:
- `tdata` = 10-dimensional NumPy array containing read/extracted tally results
Outputs:
- `tdata` = updated `tdata` array now with absolute uncertainties in `ierr = 2` index
'''
ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata)
for ir in range(ir_max):
for iy in range(iy_max):
for iz in range(iz_max):
for ie in range(ie_max):
for it in range(it_max):
for ia in range(ia_max):
for il in range(il_max):
for ip in range(ip_max):
for ic in range(ic_max):
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2] = \
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0] * \
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1]
if ierr_max==6:
for ir in range(ir_max):
for iy in range(iy_max):
for iz in range(iz_max):
for ie in range(ie_max):
for it in range(it_max):
for ia in range(ia_max):
for il in range(il_max):
for ip in range(ip_max):
for ic in range(ic_max):
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 5] = \
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 3] * \
tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 4]
return tdata
def build_tally_Pandas_dataframe(tdata,meta):
'''
Description:
Calculates the absolute uncertainty for every value in the PHITS tally data array
Dependencies:
- `import pandas as pd`
Inputs:
- `tdata` = 10-dimensional NumPy array containing read/extracted tally results
- `meta` = Munch object / dictionary containing tally metadata
Outputs:
- `tally_df` = Pandas dataframe containing the entire contents of the `tdata` array;
note that tally_df.attrs returns values which are the same for all rows
'''
import pandas as pd
ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata)
num_df_rows = ir_max * iy_max * iz_max * ie_max * it_max * ia_max * il_max * ip_max * ic_max
# determine what columns to include, based on what info was specified vs left at default values
col_names_list = []
in_irregular_TCross_rz_mesh = False
in_irregular_TCross_xyz_mesh = False
ierr_mod = 0
if meta['tally_type'] == '[T-Cross]' and (meta['mesh'] == 'xyz' or meta['mesh'] == 'r-z'):
if 'enclos' in meta and meta['enclos'] == 1:
pass
else:
if meta['mesh'] == 'r-z':
in_irregular_TCross_rz_mesh = True
min_r_is_zero = False
if meta['r-mesh_bin_edges'][0]==0:
min_r_is_zero = True
ierr_mod = int(ierr_max / 2)
else:
in_irregular_TCross_xyz_mesh = True
# region columns
if meta['mesh'] == 'reg':
reg_cols = ['ir','reg','reg#'] # use meta.reg_groups and meta.reg_num
elif meta['mesh'] == 'xyz':
if in_irregular_TCross_xyz_mesh:
reg_cols = ['ix', 'iy', 'iz', 'x_mid', 'y_mid', 'z_surf']
else:
reg_cols = ['ix','iy','iz','x_mid','y_mid','z_mid']
elif meta['mesh'] == 'r-z':
if in_irregular_TCross_rz_mesh:
#reg_cols = ['ir', 'ic', 'r_mid', 'z_surf', 'iy', 'iz', 'r_surf', 'z_mid']
reg_cols = ['ir', 'iz', 'r_mid', 'z_surf', 'r_surf', 'z_mid']
else:
reg_cols = ['ir','iz','r_mid','z_mid']
elif meta['mesh'] == 'tet':
reg_cols = ['ir','tet'] #,'tet#']
elif meta['mesh'] == 'point':
reg_cols = ['ir','point#']
elif meta['mesh'] == 'ring':
reg_cols = ['ir','ring#']
col_names_list += reg_cols
# Determine what other columns will be present
ecols, tcols, acols, lcols, pcols, ccols = False, False, False, False, False, False
single_specified_bin_axes = [] # log axes which are provided by user but only contain 1 bin
single_bin_ranges_or_values = []
if meta['ne'] != None or ie_max>1: #
if meta['ne']==1:
single_specified_bin_axes.append('e')
single_bin_ranges_or_values.append(['Energy',meta['e-mesh_bin_edges']])
elif meta['tally_type']=='[T-Deposit2]':
if meta['ne1']==1:
single_specified_bin_axes.append('e1')
single_bin_ranges_or_values.append(['Energy1', meta['e1-mesh_bin_edges']])
else:
ecols = True
ecol_names_list = ['ie', 'e1_mid']
col_names_list += ecol_names_list
else:
ecols = True
ecol_names_list = ['ie','e_mid']
col_names_list += ecol_names_list
else:
single_bin_ranges_or_values.append(['Energy','default/all'])
if meta['nt'] != None:
if meta['nt']==1:
single_specified_bin_axes.append('t')
single_bin_ranges_or_values.append(['Time',meta['t-mesh_bin_edges']])
else:
tcols = True
tcol_names_list = ['it', 't_mid']
col_names_list += tcol_names_list
else:
single_bin_ranges_or_values.append(['Time','default/all'])
if meta['na'] != None:
if meta['na']==1:
single_specified_bin_axes.append('a')
single_bin_ranges_or_values.append(['Angle',meta['a-mesh_bin_edges']])
else:
acols = True
acol_names_list = ['ia', 'a_mid']
col_names_list += acol_names_list
else:
single_bin_ranges_or_values.append(['Angle','default/all'])
if meta['nl'] != None:
if meta['nl']==1:
single_specified_bin_axes.append('l')
single_bin_ranges_or_values.append(['LET',meta['l-mesh_bin_edges']])
else:
lcols = True
lcol_names_list = ['il', 'LET_mid']
col_names_list += lcol_names_list
else:
single_bin_ranges_or_values.append(['LET','default/all'])
if meta['nc'] != None or ic_max>1:
if meta['nc'] == 1 or ic_max==1:
pass
else:
ccols = True
if meta['tally_type'] == '[T-Yield]':
if meta['axis'] == 'chart':
ccol_names_list = ['ic', 'nuclide', 'ZZZAAAM']
col_names_list += ccol_names_list
elif meta['axis'] == 'charge':
ccol_names_list = ['ic/Z/charge']
col_names_list += ccol_names_list
elif meta['axis'] == 'mass':
ccol_names_list = ['ic/A/mass']
col_names_list += ccol_names_list
elif meta['tally_type'] == '[T-Interact]':
if meta['axis'] == 'act':
ccol_names_list = ['ic', '#Interactions']
col_names_list += ccol_names_list
elif meta['tally_type'] == '[T-Deposit2]':
if meta['ne2'] == 1:
single_specified_bin_axes.append('e2')
single_bin_ranges_or_values.append(['Energy2', meta['e2-mesh_bin_edges']])
else:
ccol_names_list = ['ic', 'e2_mid']
col_names_list += ccol_names_list
if meta['npart'] != None: # and meta['part_groups'][0]=='all':
if meta['npart']==1:
single_specified_bin_axes.append('p')
single_bin_ranges_or_values.append(['Particle',meta['part_groups'][0]])
else:
pcols = True
pcol_names_list = ['ip', 'particle', 'kf-code']
col_names_list += pcol_names_list
else:
single_bin_ranges_or_values.append(['Particle','default/all'])
# HANDLE SPECIAL COLUMNS HERE (ic / ccols)
# value columns come last
val_names_list = ['value', 'rel.err.']
if ierr_max == 3 or ierr_max == 6: val_names_list += ['abs.err.']
if ierr_max >= 4: val_names_list += ['value2', 'rel.err.2']
if ierr_max == 6: val_names_list += ['abs.err.2']
col_names_list += val_names_list
# Initialize dictionary
df_dict = {}
for col in col_names_list:
df_dict[col] = []
# Populate dictionary
for ir in range(ir_max):
for iy in range(iy_max):
for iz in range(iz_max):
for ie in range(ie_max):
for it in range(it_max):
for ia in range(ia_max):
for il in range(il_max):
for ip in range(ip_max):
for ic in range(ic_max):
# Region columns
if in_irregular_TCross_rz_mesh:
if (ir == ir_max - 1 and iz == iz_max - 1): # only index that should be empty
continue
# ['ir', 'iz', 'r_mid', 'z_surf', 'r_surf', 'z_mid']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(iz)
if ir==ir_max-1:
df_dict[reg_cols[2]].append(None)
else:
df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir])
df_dict[reg_cols[3]].append(meta['z-mesh_bin_edges'][iz])
df_dict[reg_cols[4]].append(meta['r-mesh_bin_edges'][ir])
if iz == iz_max - 1:
df_dict[reg_cols[5]].append(None)
else:
df_dict[reg_cols[5]].append(meta['z-mesh_bin_mids'][iz])
# OLD IMPLEMENTATION IS BELOW:
'''
# skip unwritten indices
# reg_cols = ['ir', 'ic', 'r_mid', 'z_surf', 'iy', 'iz', 'r_surf', 'z_mid']
if (ir==ir_max-1 and ic==ic_max-1):
if (iy == iy_max - 1 or iz == iz_max - 1): continue
if min_r_is_zero and iy==0: continue # surface vals not written for r=0.0
df_dict[reg_cols[0]].append(None)
df_dict[reg_cols[1]].append(None)
df_dict[reg_cols[2]].append(None)
df_dict[reg_cols[3]].append(None)
df_dict[reg_cols[4]].append(iy)
df_dict[reg_cols[5]].append(iz)
df_dict[reg_cols[6]].append(meta['r-mesh_bin_edges'][iy])
df_dict[reg_cols[7]].append(meta['z-mesh_bin_mids'][iz])
elif (iy==iy_max-1 and iz==iz_max-1):
if (ir == ir_max - 1 or ic == ic_max - 1): continue
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(ic)
df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir])
df_dict[reg_cols[3]].append(meta['z-mesh_bin_edges'][ic])
df_dict[reg_cols[4]].append(None)
df_dict[reg_cols[5]].append(None)
df_dict[reg_cols[6]].append(None)
df_dict[reg_cols[7]].append(None)
else: # all other indices should not have any content written into them
continue
'''
else:
if meta['mesh'] == 'reg': #reg_cols = ['ir','reg', 'reg#'] # use meta.reg_groups and meta.reg_num
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(meta['reg_groups'][ir])
df_dict[reg_cols[2]].append(meta['reg_num'][ir])
elif meta['mesh'] == 'xyz':
#reg_cols = ['ix', 'iy', 'iz', 'xmid', 'ymid', 'zmid']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(iy)
df_dict[reg_cols[2]].append(iz)
df_dict[reg_cols[3]].append(meta['x-mesh_bin_mids'][ir])
df_dict[reg_cols[4]].append(meta['y-mesh_bin_mids'][iy])
if in_irregular_TCross_xyz_mesh:
df_dict[reg_cols[5]].append(meta['z-mesh_bin_edges'][iz])
else:
df_dict[reg_cols[5]].append(meta['z-mesh_bin_mids'][iz])
elif meta['mesh'] == 'r-z':
#reg_cols = ['ir', 'iz', 'rmid', 'zmid']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(iz)
df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir])
df_dict[reg_cols[3]].append(meta['z-mesh_bin_mids'][iz])
elif meta['mesh'] == 'tet':
#reg_cols = ['ir','tet']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(meta['tet_num'][ir])
elif meta['mesh'] == 'point':
#reg_cols = ['ir','point#']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(str(ir+1))
elif meta['mesh'] == 'ring':
#reg_cols = ['ir','ring#']
df_dict[reg_cols[0]].append(ir)
df_dict[reg_cols[1]].append(str(ir+1))
#ecols, tcols, acols, lcols, pcols, ccols
if pcols: # pcol_names_list = ['ip', 'particle', 'kf-code']
df_dict[pcol_names_list[0]].append(ip)
df_dict[pcol_names_list[1]].append(meta['part_groups'][ip])
df_dict[pcol_names_list[2]].append(meta['kf_groups'][ip])
if ecols: # ecol_names_list = ['ie','e_mid']
df_dict[ecol_names_list[0]].append(ie)
if meta['tally_type'] == '[T-Deposit2]':
df_dict[ecol_names_list[1]].append(meta['e1-mesh_bin_mids'][ie])
else:
df_dict[ecol_names_list[1]].append(meta['e-mesh_bin_mids'][ie])
if tcols: # tcol_names_list = ['it','t_mid']
df_dict[tcol_names_list[0]].append(it)
df_dict[tcol_names_list[1]].append(meta['t-mesh_bin_mids'][it])
if acols: # acol_names_list = ['ia','a_mid']
df_dict[acol_names_list[0]].append(ia)
df_dict[acol_names_list[1]].append(meta['a-mesh_bin_mids'][ia])
if lcols: # lcol_names_list = ['il','LET_mid']
df_dict[lcol_names_list[0]].append(il)
df_dict[lcol_names_list[1]].append(meta['l-mesh_bin_mids'][il])
if ccols:
if meta['tally_type'] == '[T-Yield]':
if meta['axis'] == 'chart':
#ccol_names_list = ['ic', 'nuclide', 'ZZZAAAM']
df_dict[ccol_names_list[0]].append(ic)
df_dict[ccol_names_list[1]].append(meta['nuclide_isomer_list'][ic])
df_dict[ccol_names_list[2]].append(meta['nuclide_ZZZAAAM_list'][ic])
elif meta['axis'] == 'charge':
#ccol_names_list = ['ic/Z/charge']
df_dict[ccol_names_list[0]].append(ic)
elif meta['axis'] == 'mass':
#ccol_names_list = ['ic/A/mass']
df_dict[ccol_names_list[0]].append(ic)
elif meta['tally_type'] == '[T-Interact]':
if meta['axis'] == 'act':
#ccol_names_list = ['act']
df_dict[ccol_names_list[0]].append(ic)
df_dict[ccol_names_list[1]].append(ic+1)
elif meta['tally_type'] == '[T-Deposit2]':
df_dict[ccol_names_list[0]].append(ic)
df_dict[ccol_names_list[1]].append(meta['e2-mesh_bin_mids'][ic])
# Value columns
#val_names_list = ['value', 'rel.err.','abs.err.']
df_dict[val_names_list[0]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0])
df_dict[val_names_list[1]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1])
if ierr_max == 3 or ierr_max == 6:
df_dict[val_names_list[2]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2])
if in_irregular_TCross_rz_mesh:
df_dict[val_names_list[0+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0+ierr_mod])
df_dict[val_names_list[1+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1+ierr_mod])
if ierr_max == 6:
df_dict[val_names_list[2+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2 + ierr_mod])
# Convert dictionary to Pandas dataframe
#for key in df_dict.keys():
# print(key,len(df_dict[key]))
#sys.exit()
tally_df = pd.DataFrame(df_dict)
# store information on settings provided by user that are different from default but same for all rows
if len(single_bin_ranges_or_values) > 0:
for i in single_bin_ranges_or_values:
col, val = i
tally_df.attrs[col] = val
#with pd.option_context('display.max_rows', None, 'display.max_columns', None): print(tally_df)
if in_debug_mode:
#print(tally_df.to_string())
print(tally_df.attrs)
return tally_df
def extract_tally_outputs_from_phits_input(phits_input, use_path_and_string_mode=False, only_seek_phitsout=False):
'''
Description:
Extract a list of output files produced from a PHITS input file (or its "phits.out" file, using its input echo).
In cases where the PHITS `infl:{*}` function is used to insert text files of PHITS input (namely input for
tallies), it is strongly recommended to pass this function the "phits.out" file (`file(6)` in the PHITS
[Parameters] section) to have access to the complete input echo including all inserted files. Note that this
function's output will only include files that actually exist.
Inputs:
- `phits_input` = string or Path object denoting the path to the PHITS input file to be parsed (or the
"phits.out" file, which should be located in the same directory as the PHITS input producing it)
- `use_path_and_string_mode` = (optional, D=`False`) Boolean for special use case by `parse_phitsout_file()`.
If `True`, `phits_input` should instead be a dictionary with the following keys:
- `'sim_base_dir_path'` : a [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
object pointing to the directory containing the PHITS input file and phits.out file
- `'input_echo'` : a string of the input echo as generated by `parse_phitsout_file()`
- `only_seek_phitsout` = (optional, D=`False`) Boolean designating if this function should _only_ search for
the phits.out file and no other outputs.
Outputs:
- `files_dict` = a dictionary organizing and listing files that were to be produced by the PHITS input and were
found to exist, with the following keys:
- `'standard_output'` : a list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
objects pointing to the locations of all of the standard tally output files found.
- `'dump_output'` : a list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
objects pointing to the locations of all of the dump tally output files found.
- `'phitsout'` : a single [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
object pointing to the location of the "phits.out" file.
- `'active_infl_found'` : a Boolean denoting whether an active `infl:{*}` insert file function was found
in the PHITS input. If `True`, it is possible this files dictionary is missing some produced
output files from tallies / tally portions included in the input file via `infl:{*}`.
This should always be `False` when provided a "phits.out" file since it includes inserted file contents.
To conveniently view the contents of `files_dict`, one can import the built-in
pprint library `import pprint` and then use `pprint.pp(dict(files_dict))`.
'''
'''
Basically, look for "file =", accounting for presence of "dump =" in the tally and whether "off" is present in the
tally's first line. Off tallies should be excluded from the list.
Once an initial list of files is formed, search for them actually existing (including dump files / MPI splits) and
return a list of the existing files.
'''
import io
def merge_paths_if_not_absolute(known_directory,path_segment):
if not Path(path_segment).is_absolute():
path = Path(known_directory,path_segment)
else:
path = Path(path_segment)
if not path.exists():
path = None
return path
if use_path_and_string_mode:
sim_base_dir_path = phits_input['sim_base_dir_path']
file = io.StringIO(phits_input['input_echo'])
else:
sim_base_dir_path = Path(phits_input).parent
PHITS_file_type = determine_PHITS_output_file_type(phits_input)
if PHITS_file_type['is_file6_phitsout_file']: # if determined to be a phits.out-type file
file = io.StringIO(parse_phitsout_file(phits_input['input_echo']))
else: # otherwise, assume an input file was passed
file = open(Path(phits_input), 'r', encoding="utf-8")
files_dict = {'standard_output':[], 'dump_output':[], 'phitsout':None}
in_valid_tally = False
in_parameters_section = False
phitsout_file = None
tally_out_files = []
tally_has_dump = []
# scan tally input for files produced by PHITS
active_infl_found = False
if only_seek_phitsout:
for line in file:
if len(line.strip())==0: continue
if line.strip()[0] == '[' and ']' in line: # in a section header block
in_parameters_section = False
line_section_title_slug = line.lower().replace(' ', '')
if '[parameters]' in line_section_title_slug and ']off' not in line_section_title_slug:
in_parameters_section = True
if in_parameters_section and line.strip()[:7] == 'file(6)':
phitsout_file = extract_data_from_header_line(line)[1]
else:
for line in file:
if len(line.strip()) == 0: continue
if line.strip()[0] in ['$', '#']: continue # comment line
if line.lstrip()[0] == 'c' and 'c ' in line[:min(6,len(line))]: continue # comment line
if line.strip()[0] == '[' and ']' in line: # in a section header block
line_section_title_slug = line.lower().replace(' ', '')
in_valid_tally = False
in_parameters_section = False
if '[t-' in line_section_title_slug and ']off' not in line_section_title_slug:
in_valid_tally = True
tally_has_dump.append(False)
elif '[parameters]' in line_section_title_slug and ']off' not in line_section_title_slug:
in_parameters_section = True
elif 'infl:' in line:
active_infl_found = True
if in_valid_tally:
if line.strip()[0] == '#': continue
if line.strip()[:4]=='file':
key, value = extract_data_from_header_line(line)
tally_out_files.append(value)
if line.strip()[:4] == 'dump':
tally_has_dump[-1] = True
elif in_parameters_section:
if line.strip()[:7] == 'file(6)':
key, value = extract_data_from_header_line(line)
phitsout_file = value
file.close()
files_dict['active_infl_found'] = active_infl_found
if phitsout_file is None:
phitsout_file = 'phits.out'
files_dict['phitsout'] = merge_paths_if_not_absolute(sim_base_dir_path, phitsout_file)
if not only_seek_phitsout:
for i, file in enumerate(tally_out_files):
file_path = merge_paths_if_not_absolute(sim_base_dir_path, file)
if file_path is not None:
files_dict['standard_output'].append(file_path)
if tally_has_dump[i]:
dump_files_for_this_tally = list(set(file_path.parent.glob(file_path.stem+"_dmp*"))-set(file_path.parent.glob(file_path.stem+"*.pickle*")))
files_dict['dump_output'] += dump_files_for_this_tally
return files_dict
if run_with_CLI_inputs:
def validate_file(arg):
if (file := Path(arg)).is_file():
return file
else:
if (file := Path(arg)).is_dir():
return file
else:
raise FileNotFoundError(arg)
parser = argparse.ArgumentParser()
parser.add_argument("file", type=validate_file, help="path to PHITS output file to parse or directory (or PHITS input or phits.out file) containing files to parse (relative or absolute path)")
# Flags for standard output files
parser.add_argument("-g", "--GUI", help="Launch the PHITS Tools GUI and ignore all other command line inputs", action="store_true")
parser.add_argument("-np", "--disable_PandasDF", help="[standard output] disable automatic creation of Pandas DataFrame of PHITS output", action="store_true")
parser.add_argument("-na", "--disable_abs_err_calc", help="[standard output] disable automatic calculation of absolute errors", action="store_true")
parser.add_argument("-lzma", "--use_lzma_compression", help="[standard output] compress tally output pickle with LZMA", action="store_true")
parser.add_argument("-po", "--parse_phitsout", help="[standard output] parse the phits.out file and add it to the metadata dictionary", action="store_true")
parser.add_argument("-p", "--plot", help="[standard output] save a plot of the tally's output to a PDF and PNG", action="store_true")
parser.add_argument("-skip", "--skip_existing_pickles", help="[all output] skip files where output pickle already exists", action="store_true")
# Not going to add below option. Why would you ever run this in CLI if not trying to generate the pickle file?
# parser.add_argument("-ns", "--disable_saving_pickle", help="disable saving of pickle of of PHITS output", action="store_true")
# Flags for dump files
parser.add_argument("-d", "--is_dump_file", action="store_true", help="add this flag if the file is a dump file, omit if standard PHITS tally output; if inputting a directory path to 'file' (or a PHITS input or phits.out file), this flag specifies that dump files will be read too (by default, they will be skipped), and if so the below flags will be applied to the settings used when parsing them")
parser.add_argument('-dvals', '--dump_data_sequence', nargs='+', type=int, help='[dump output] provide a series of integers separated by spaces that match the line after "dump = " in the tally whose dump file is being parsed, detailing how the columns of the dump file are to be interpreted. (REQUIRED for dump files, but an attempt to assign automatically will be made first if left unspecified)')
parser.add_argument("-dbin", "--dump_file_is_binary", action="store_true", help="[dump output] specify that the provided dump file is binary; otherwise it is assumed to be ASCII (REQUIRED for dump files, but an attempt to assign automatically will be made first if left unspecified)")
parser.add_argument("-dnmax", "--dump_max_entries_read", type=int, help="[dump output] specify maximum integer number of entries to read (read all by default)")
parser.add_argument("-ddir", "--dump_return_directional_info", action="store_true", help="[dump output] return extra directional information: radial distance r from the origin in cm, radial distance rho from the z-axis in cm, polar angle theta between the direction vector and z-axis in radians [0,pi] (or degrees), and azimuthal angle phi of the direction vector in radians [-pi,pi] (or degrees). Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file.")
parser.add_argument("-ddeg", "--dump_use_degrees", action="store_true", help="[dump output] angular quantities will be in degrees instead of radians")
parser.add_argument("-dnsl", "--dump_no_save_namedtuple_list", action="store_true", help="[dump output] do NOT save parsed dump file info to list of namedtuples to pickle file (-dnsl and -dnsp cannot both be enabled if parsing a dump file)")
parser.add_argument("-dnsp", "--dump_no_save_Pandas_dataframe", action="store_true", help="[dump output] do NOT save parsed dump file info to Pandas DataFrame to pickle file (-dnsl and -dnsp cannot both be enabled if parsing a dump file)")
parser.add_argument("-dmaxGB", "--dump_max_GB_per_chunk", type=float, default=20, help="[dump output] split binary dump files larger than inputted number of GB for processing (20 GB by default)")
parser.add_argument("-dsplit", "--dump_split_handling", type=int, default=0, help="[dump output] (0 by default) how split/merged dump files are handled; enter 0 (merge at end, delete parts), 1 (no merge), or 2 (merge, keep parts)")
# Flags for processing files in a directory
parser.add_argument("-dnmmpi", "--dump_no_merge_MPI_dumps", action="store_true", help="[directory with dump output] do NOT merge the dump namedtuple list / Pandas DataFrame pickle files for dump files from the same tally split up by MPI")
parser.add_argument("-dndmpi", "--dump_no_delete_MPI_subpickles", action="store_true", help="[directory with dump output] do NOT delete the individual dump namedtuple list / Pandas DataFrame pickle files for dump files from the same tally split up by MPI after they have been merged (ignored if -dnmmpi is used)")
parser.add_argument("-r", "--recursive_search", action="store_true", help="[directory parsing] If the provided 'file' is a directory, also recursively search subdirectories for files to process.")
parser.add_argument("-fpre", "--file_prefix", default='', help="[directory parsing] A string specifying what characters processed filenames (including the file extension) must begin with to be included. This condition is not enforced if set to an empty string (default).")
parser.add_argument("-fsuf", "--file_suffix", type=str, default=None, help="[directory parsing] A string specifying what characters processed filenames (including the file extension) must end in to be included. This condition is not enforced if set to an empty string. This is '.out' by deault unless `file` is a PHITS input or phits.out file, in which case it defaults to an empty string.")
parser.add_argument("-fstr", "--file_required_string", default='', help="[directory parsing] A string which must be present anywhere within processed filenames (including the file extension) to be included. This condition is not enforced if set to an empty string (default).")
parser.add_argument("-m", "--merge_tally_outputs", help="[directory parsing] merge all tally outputs into a single pickled dictionary object", action="store_true")
parser.add_argument("-smo", "--save_merged_only", help="[directory parsing] same as -m but saves ONLY this merged pickle file; do not save pickles for each tally output", action="store_true")
parser.add_argument("-pa", "--plot_all", help="[directory parsing] save plots of all tally outputs to a single PDF", action="store_true")
args = parser.parse_args()
output_file_path = Path(args.file)
is_dump_file = args.is_dump_file
is_path_a_dir = output_file_path.is_dir()
is_path_a_file = output_file_path.is_file()
if is_path_a_file:
PHITS_output_file_type = determine_PHITS_output_file_type(output_file_path)
if PHITS_output_file_type['is_PHITS_input_file'] or PHITS_output_file_type['is_file6_phitsout_file']:
is_path_a_dir = True
is_path_a_file = False
if not is_path_a_file and not is_path_a_dir:
raise ValueError("ERROR! The inputted filepath is neither a recognized file nor directory.")
# directory options
recursive_search = args.recursive_search
file_suffix = args.file_suffix
file_prefix = args.file_prefix
file_reqstr = args.file_required_string
# Standard output options
make_PandasDF = not args.disable_PandasDF
calculate_absolute_errors = not args.disable_abs_err_calc
# Dump output options
dump_data_sequence = args.dump_data_sequence
if dump_data_sequence != None:
dump_data_number = len(dump_data_sequence)
if not args.dump_file_is_binary:
dump_data_number = -1 * dump_data_number
else:
dump_data_number = None
return_namedtuple_list = False
return_Pandas_dataframe = False
max_entries_read = args.dump_max_entries_read
return_directional_info = args.dump_return_directional_info
use_degrees = args.dump_use_degrees
no_save_namedtuple_list = args.dump_no_save_namedtuple_list
no_save_Pandas_dataframe = args.dump_no_save_Pandas_dataframe
dump_no_merge_MPI_dumps = args.dump_no_merge_MPI_dumps
dump_no_delete_MPI_subpickles = args.dump_no_delete_MPI_subpickles
use_lzma_compression = args.use_lzma_compression
skip_existing_pickles = args.skip_existing_pickles
dump_max_GB_per_chunk = args.dump_max_GB_per_chunk
dump_split_handling = args.dump_split_handling
autoplot_tally_output = args.plot
autoplot_all_tally_output_in_dir = args.plot_all
include_phitsout_in_metadata = args.parse_phitsout
merge_tally_outputs = args.merge_tally_outputs
save_merged_only = args.save_merged_only
save_namedtuple_list = not no_save_namedtuple_list
save_Pandas_dataframe = not no_save_Pandas_dataframe
dump_merge_MPI_subdumps = not dump_no_merge_MPI_dumps
dump_delete_MPI_subdumps_post_merge = not dump_no_delete_MPI_subpickles
compress_pickle_with_lzma = use_lzma_compression
prefer_reading_existing_pickle = skip_existing_pickles
split_binary_dumps_over_X_GB = dump_max_GB_per_chunk
merge_split_dump_handling = dump_split_handling
dir_mode_save_output_pickle=True
save_pickle_of_merged_tally_outputs=None
if save_merged_only:
dir_mode_save_output_pickle = False
merge_tally_outputs = True
save_pickle_of_merged_tally_outputs = True
if is_path_a_dir:
parse_all_tally_output_in_dir(output_file_path, output_file_suffix=file_suffix, output_file_prefix=file_prefix,
output_file_required_string=file_reqstr, include_subdirectories=recursive_search,
return_tally_output=False,
make_PandasDF=make_PandasDF, calculate_absolute_errors=calculate_absolute_errors,
save_output_pickle=dir_mode_save_output_pickle,
prefer_reading_existing_pickle=prefer_reading_existing_pickle,
compress_pickle_with_lzma=compress_pickle_with_lzma,
autoplot_tally_output=autoplot_tally_output,
include_dump_files=is_dump_file,
dump_data_number=dump_data_number, dump_data_sequence=dump_data_sequence,
dump_return_directional_info=return_directional_info, dump_use_degrees=use_degrees,
dump_max_entries_read=max_entries_read,
dump_save_namedtuple_list=save_namedtuple_list,
dump_save_Pandas_dataframe=save_Pandas_dataframe,
dump_merge_MPI_subdumps=dump_merge_MPI_subdumps,
dump_delete_MPI_subdumps_post_merge=dump_delete_MPI_subdumps_post_merge,
split_binary_dumps_over_X_GB=split_binary_dumps_over_X_GB,
merge_split_dump_handling=merge_split_dump_handling,
autoplot_all_tally_output_in_dir=autoplot_all_tally_output_in_dir,
include_phitsout_in_metadata=include_phitsout_in_metadata,
merge_tally_outputs=merge_tally_outputs,
save_pickle_of_merged_tally_outputs=save_pickle_of_merged_tally_outputs
)
else: # if is_path_a_file
if is_dump_file:
if dump_data_number == None:
dump_data_number, dump_data_sequence = search_for_dump_parameters(output_file_path)
if dump_data_number == None or dump_data_sequence == None:
raise ValueError('You MUST provide a space-delimited list of integers to the -dvals / --dump_data_sequence input specifying ' +
'how the data columns in the dump file are to be interpreted, the same as the line following "dump = " in your PHITS tally input. ' +
'An attempt was made to automatically find these values, but it failed (thus, manual specification is required).')
if no_save_namedtuple_list and no_save_Pandas_dataframe:
raise ValueError('You MUST select how the dump file data is to be saved by disabling either or both of the following flags:' +
' -dsl / --dump_save_namedtuple_list AND/OR -dsp / --dump_save_Pandas_dataframe')
parse_tally_dump_file(output_file_path, dump_data_number, dump_data_sequence,
return_directional_info=return_directional_info, use_degrees=use_degrees,
max_entries_read=max_entries_read,
return_namedtuple_list=return_namedtuple_list,
return_Pandas_dataframe=return_Pandas_dataframe,
save_namedtuple_list=save_namedtuple_list,
save_Pandas_dataframe=save_Pandas_dataframe,
prefer_reading_existing_pickle=prefer_reading_existing_pickle,
split_binary_dumps_over_X_GB=split_binary_dumps_over_X_GB,
merge_split_dump_handling=merge_split_dump_handling)
else:
parse_tally_output_file(output_file_path, make_PandasDF=make_PandasDF,
calculate_absolute_errors=calculate_absolute_errors,
save_output_pickle=True,
include_phitsout_in_metadata=include_phitsout_in_metadata,
prefer_reading_existing_pickle=prefer_reading_existing_pickle,
compress_pickle_with_lzma=compress_pickle_with_lzma,
autoplot_tally_output=autoplot_tally_output)
elif launch_GUI:
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
from tkinter import ttk
import warnings
import sys
# Function to issue a warning on unexpected closure and then exit the program
def on_closing(window):
window.destroy()
warnings.warn("Window closed unexpectedly", UserWarning)
sys.exit()
opt3_def_pady = 0
# Initialize the settings dictionary
settings = {}
standard_mode_short_text = "[STANDARD mode]"
dump_mode_short_text = "[DUMP mode]"
directory_mode_short_text = "[DIRECTORY mode]"
input_file_mode_short_text = "[INPUT_FILE mode]"
standard_mode_full_text = standard_mode_short_text + " for processing a single standard PHITS tally output file"
dump_mode_full_text = dump_mode_short_text + " for processing a single PHITS tally dump output file (*_dmp.out)"
directory_mode_full_text = directory_mode_short_text + " for processing all PHITS output files in a directory"
input_file_mode_full_text = input_file_mode_short_text + " for processing all PHITS output files generated by \na single PHITS run (as identified in the PHITS input or its phits.out file)"
def on_option_selected():
option = selected_option.get()
file_chosen = None
try:
if option in [1, 2]:
if option == 1:
window_name_str = 'Select standard PHITS tally output file'
else: # option == 2
window_name_str = 'Select PHITS tally dump output file'
file_chosen = filedialog.askopenfilename(title=window_name_str)
if not file_chosen:
raise ValueError("File selection is required")
settings['file'] = file_chosen
elif option in [3, 4]:
if option == 3:
directory_chosen = filedialog.askdirectory(title="Select Directory of PHITS outputs to parse")
if not directory_chosen:
raise ValueError("Directory selection is required")
else: # option == 4
directory_chosen = filedialog.askopenfilename(title='Select PHITS input file or its "phits.out" file')
if not directory_chosen:
raise ValueError("File selection is required")
settings['directory'] = directory_chosen
except:
raise ValueError("User closed the file/directory dialog")
else:
root.withdraw()
create_secondary_gui(option)
def create_secondary_gui(option):
secondary_gui = tk.Toplevel(root)
if option in [3, 4]: # This window tends to be tall and should be placed at top of screen
x = root.winfo_x()
# y = root.winfo_y()
secondary_gui.geometry("+%d+%d" % (x + 10, 10))
def on_closing_secondary_gui():
on_closing(secondary_gui)
secondary_gui.protocol("WM_DELETE_WINDOW", on_closing_secondary_gui)
inputs = {
1: ['Checkbox 1', 'Checkbox 2', 'Checkbox 3'],
2: ['Checkbox A', 'Checkbox B', 'Radio 1', 'Radio 2', 'Radio 3', 'Input 1 (str)', 'Input 2 (int)',
'Input 3 (int)'],
3: ['Checkbox 1', 'Checkbox 2', 'Checkbox 3', 'Checkbox A', 'Checkbox B', 'Radio 1', 'Radio 2', 'Radio 3',
'Input 1 (str)', 'Input 2 (int)', 'Input 3 (int)', 'Input 4 (str)', 'Input 5 (str)', 'Input 6 (str)',
'Extra Checkbox 1', 'Extra Checkbox 2']
}
inputs[4] = inputs[3]
def save_settings():
settings.update({
'main_mode': selected_option.get()
})
if option == 1:
settings.update({
'option_1_cb1': cb1_var.get(),
'option_1_cb2': cb2_var.get(),
'option_1_cb3': cb3_var.get(),
'option_1_cb4': cb4_var.get()
})
elif option == 2:
settings.update({
'option_2_cb1': cb1_var.get(),
'option_2_cb2': cb2_var.get(),
'radio': radio_var.get(),
'input_str': entry_str.get() or None,
'input_int1': entry_int1.get() or None,
'input_int2': entry_int2.get() or None,
})
elif option in [3, 4]:
settings.update({
'option_3_cb1': cb1_var.get(), 'option_3_cb2': cb2_var.get(),
'option_3_cb3': cb3_var.get(), 'option_3_cb4': cb4_var.get(),
'option_3_cb5': cb5_var.get(), 'option_3_cb6': cb6_var.get(),
'option_3_cb7': cb7_var.get(), 'option_3_cb8': cb8_var.get(),
'radio': radio_var.get(),
'input_str_1': secondary_entry_str1.get() or None,
'input_int_1': secondary_entry_int1.get() or None,
'input_int_2': secondary_entry_int2.get() or None,
'input_str_2': secondary_entry_str2.get() or None,
'input_str_3': secondary_entry_str3.get() or None,
'input_str_6': extra_entry_str1.get() or None, # Renamed 'Extra Input 1' to 'Input 6'
'extra_cb1': extra_cb1_var.get(),
'extra_cb2': extra_cb2_var.get(),
})
secondary_gui.destroy()
root.destroy() # Ensure root window is destroyed after closing secondary GUI
common_widgets = []
if option == 1:
sample_text_label = tk.Label(secondary_gui, text=standard_mode_full_text,
anchor=tk.W, font='16')
sample_text_label.pack(anchor=tk.W, padx=10, pady=2)
cb1_var = tk.BooleanVar()
cb2_var = tk.BooleanVar()
cb3_var = tk.BooleanVar()
cb4_var = tk.BooleanVar()
common_widgets.append(tk.Checkbutton(secondary_gui, text="Also make and save Pandas DataFrame object of results (in addition to default NumPy array)", variable=cb1_var, anchor=tk.W))
common_widgets[-1].select() # This makes the checkbox be ticked by default
common_widgets.append(tk.Checkbutton(secondary_gui, text="Also calculate absolute uncertainties", variable=cb2_var, anchor=tk.W))
common_widgets[-1].select() # This makes the checkbox be ticked by default
common_widgets.append(tk.Checkbutton(secondary_gui, text='Also include metadata from the "phits.out" file', variable=cb4_var, anchor=tk.W))
common_widgets[-1].select() # This makes the checkbox be ticked by default
common_widgets.append(tk.Checkbutton(secondary_gui, text="Also make a plot of the tally's output and save it as a PDF and PNG", variable=cb3_var, anchor=tk.W))
common_widgets[-1].select() # This makes the checkbox be ticked by default
elif option == 2:
sample_text_label = tk.Label(secondary_gui, text=dump_mode_full_text,
anchor=tk.W, font='16')
sample_text_label.pack(anchor=tk.W, padx=10, pady=2)
cb1_var = tk.BooleanVar()
cb2_var = tk.BooleanVar()
radio_var = tk.IntVar(value=3)
entry_str = tk.Entry(secondary_gui, width=50)
entry_int1 = tk.Entry(secondary_gui)
entry_int2 = tk.Entry(secondary_gui)
dir_info_str = "Return extra directional information (relative to the origin and z-axis); \nthis requires all position and direction values [x,y,z,u,v,w] to be included in the dump file."
common_widgets.append(tk.Checkbutton(secondary_gui, text=dir_info_str, variable=cb1_var, anchor=tk.W, justify='left'))
common_widgets.append(tk.Checkbutton(secondary_gui, text="Use degrees (instead of radians) for extra directional information", variable=cb2_var, anchor=tk.W))
options_frame = tk.LabelFrame(secondary_gui, text="Data output format options")
options_frame.pack(padx=10, pady=10, anchor=tk.W)
tk.Radiobutton(options_frame, text="Save only a pickle file of a list of namedtuples with dump event information", variable=radio_var, value=1, anchor=tk.W).pack(anchor=tk.W)
tk.Radiobutton(options_frame, text="Save only a pickle file of a Pandas DataFrame of dump event information", variable=radio_var, value=2, anchor=tk.W).pack(anchor=tk.W)
tk.Radiobutton(options_frame, text="Save both the namedtuples list pickle file and the Pandas DataFrame pickle file", variable=radio_var, value=3, anchor=tk.W).pack(anchor=tk.W)
dump_instrcutions = 'If in the same directory as your dump file exists the corresponding standard tally output file,\n' + \
'and the only difference in their file names is the "_dmp" at the end of the dump file, the \n' + \
'below two fields can be left blank as PHITS Tools should automatically find this information.\n' + \
'Otherwise, in the below two boxes, place what you entered following "dump = " in your PHITS tally.\n' + \
'In the first box, enter a nonzero integer between -20 and 20 specifying the number of dump\n' + \
'columns and whether the data will be in ASCII (<0) or binary (>0) format.\n' + \
'In the second box, enter a sequence of that many numbers, separated by spaces, describing \n' + \
'the column order of the dump file.'
common_widgets.append(tk.Label(secondary_gui, text=dump_instrcutions, anchor=tk.W, justify="left"))
common_widgets.append(entry_int1)
#common_widgets.append(tk.Label(secondary_gui, text="Input 1 (string)", anchor=tk.W))
common_widgets.append(entry_str)
common_widgets.append(tk.Label(secondary_gui, text="\nMaximum number of dump entries to read. Leave blank to read all.", anchor=tk.W))
common_widgets.append(entry_int2)
elif option in [3, 4]:
cb1_var = tk.BooleanVar()
cb2_var = tk.BooleanVar()
cb3_var = tk.BooleanVar()
cb4_var = tk.BooleanVar()
cb5_var = tk.BooleanVar()
cb6_var = tk.BooleanVar()
cb7_var = tk.BooleanVar()
cb8_var = tk.BooleanVar()
radio_var = tk.IntVar(value=3)
secondary_entry_str1 = tk.Entry(secondary_gui, width=50) # Extra width added here
secondary_entry_int1 = tk.Entry(secondary_gui)
secondary_entry_int2 = tk.Entry(secondary_gui)
secondary_entry_str2 = tk.Entry(secondary_gui)
if option == 3:
secondary_entry_str2.insert(0, ".out") # this is how default values have to be specified for tkinter...
secondary_entry_str3 = tk.Entry(secondary_gui)
extra_entry_str1 = tk.Entry(secondary_gui)
extra_cb1_var = tk.BooleanVar()
extra_cb2_var = tk.BooleanVar()
# Add extra sample text label at the top of the secondary GUI
if option == 3:
top_sample_label = tk.Label(secondary_gui, text=directory_mode_full_text, anchor=tk.W, font='16')
else: # option == 4
top_sample_label = tk.Label(secondary_gui, text=input_file_mode_short_text+' for processing all output files in a PHITS run', anchor=tk.W, font='16')
top_sample_label.pack(anchor=tk.W, padx=10, pady=10)
common_widgets.append(tk.Checkbutton(secondary_gui, text="Also include contents of all subdirectories", variable=cb1_var, anchor=tk.W))
common_widgets.append(tk.Checkbutton(secondary_gui, text="Include dump files (otherwise, they will be skipped)", variable=cb2_var, anchor=tk.W))
common_widgets.append(tk.Checkbutton(secondary_gui, text="Merge all tally outputs into a combined dictionary object", variable=cb8_var, anchor=tk.W))
common_widgets.append(tk.Checkbutton(secondary_gui, text="Save plots of all tally outputs to a single PDF", variable=cb6_var, anchor=tk.W))
common_widgets[-1].select() # This makes the checkbox be ticked by default
# Pack common widgets with left alignment.
for widget in common_widgets:
widget.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
if option in [3, 4]:
name_instructions_str = 'In the below 3 fields, specify what characters processed filenames (including the file extension)\n' + \
'must either end with, start with, or contain in order to be processed. Leave blank to ignore.'
tk.Label(secondary_gui, text=name_instructions_str, anchor=tk.W, justify='left').pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
tk.Label(secondary_gui, text="End of filename character string (suffix)", anchor=tk.W).pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
secondary_entry_str2.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
tk.Label(secondary_gui, text="Start of filename character string (prefix)", anchor=tk.W).pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
secondary_entry_str3.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
tk.Label(secondary_gui, text="String which must appear in filename (anywhere)", anchor=tk.W).pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
extra_entry_str1.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
# Add horizontal separator immediately beneath "Checkbox 2"
separator = ttk.Separator(secondary_gui, orient='horizontal')
separator.pack(fill=tk.X, padx=10, pady=10)
sample_text_label2 = tk.Label(secondary_gui, text="Options for processing standard PHITS tally output files",
anchor=tk.W, font='14')
sample_text_label2.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
cb3obj = tk.Checkbutton(secondary_gui, text="Also make and save Pandas DataFrame object of results (in addition to default NumPy array)", variable=cb3_var, anchor=tk.W)
cb3obj.select() # This makes the checkbox be ticked by default
cb3obj.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
cb4obj = tk.Checkbutton(secondary_gui, text="Also calculate absolute uncertainties", variable=cb4_var, anchor=tk.W)
cb4obj.select() # This makes the checkbox be ticked by default
cb4obj.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
cb7obj = tk.Checkbutton(secondary_gui, text='Also include metadata from the "phits.out" file', variable=cb7_var, anchor=tk.W)
cb7obj.select() # This makes the checkbox be ticked by default
cb7obj.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
cb5obj = tk.Checkbutton(secondary_gui, text="Also make a plot of each tally's output and save it as a PDF and PNG", variable=cb5_var, anchor=tk.W)
cb5obj.select() # This makes the checkbox be ticked by default
cb5obj.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
options_frame = tk.LabelFrame(secondary_gui, text="Data output format options for dump files")
tk.Radiobutton(options_frame, text="Save only a pickle file of a list of namedtuples with dump event information", variable=radio_var, value=1, anchor=tk.W).pack(anchor=tk.W, pady=opt3_def_pady)
tk.Radiobutton(options_frame, text="Save only a pickle file of a Pandas DataFrame of dump event information", variable=radio_var, value=2, anchor=tk.W).pack(anchor=tk.W, pady=opt3_def_pady)
tk.Radiobutton(options_frame, text="Save both the namedtuples list pickle file and the Pandas DataFrame pickle file", variable=radio_var, value=3, anchor=tk.W).pack(anchor=tk.W, pady=opt3_def_pady)
# Add horizontal separator immediately beneath "Input 3 (integer)"
separator_1 = ttk.Separator(secondary_gui, orient='horizontal')
separator_1.pack(fill=tk.X, padx=10, pady=10)
sample_text_label = tk.Label(secondary_gui, text="Options for processing PHITS tally dump output files",
anchor=tk.W, font='14')
sample_text_label.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
options_frame.pack(padx=10, pady=10, anchor=tk.W) # radio buttons
dir_info_str = "Return extra directional information (relative to the origin and z-axis); \nthis requires all position and direction values [x,y,z,u,v,w] to be included in the dump file."
tk.Checkbutton(secondary_gui, text=dir_info_str, variable=extra_cb1_var, anchor=tk.W, justify='left').pack(
anchor=tk.W, padx=10, pady=opt3_def_pady)
tk.Checkbutton(secondary_gui, text="Use degrees (instead of radians) for extra directional information", variable=extra_cb2_var, anchor=tk.W).pack(
anchor=tk.W, padx=10, pady=opt3_def_pady)
dump_instrcutions = 'If in the same directory as the found dump file exists the corresponding standard tally output file,\n' + \
'and the only difference in their file names is the "_dmp" at the end of the dump file, the \n' + \
'below two fields can be left blank as PHITS Tools should automatically find this information.\n' + \
'Otherwise, in the below two boxes, place what you entered following "dump = " in your PHITS tally.\n' + \
'In the first box, enter a nonzero integer between -20 and 20 specifying the number of dump\n' + \
'columns and whether the data will be in ASCII (<0) or binary (>0) format.\n' + \
'In the second box, enter a sequence of that many numbers, separated by spaces, describing \n' + \
'the column order of the dump file.'
tk.Label(secondary_gui, text=dump_instrcutions, anchor=tk.W, justify='left').pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
secondary_entry_int1.pack(anchor=tk.W, padx=10, pady=opt3_def_pady+1)
#tk.Label(secondary_gui, text="Input 1 (string)", anchor=tk.W).pack(anchor=tk.W, padx=10, pady=2)
secondary_entry_str1.pack(anchor=tk.W, padx=10, pady=opt3_def_pady+1)
tk.Label(secondary_gui, text="\nMaximum number of dump entries to read. Leave blank to read all.", anchor=tk.W).pack(anchor=tk.W, padx=10, pady=2)
secondary_entry_int2.pack(anchor=tk.W, padx=10, pady=opt3_def_pady)
save_btn = tk.Button(secondary_gui, text="Run PHITS Tools with selected settings", command=save_settings)
save_btn.pack(pady=10)
root = tk.Tk()
#root.eval('tk::PlaceWindow . center')
#root.geometry("+%d+%d" % (30, 10))
root.title('PHITS Tools')
# protocol for main menu window to issue warning and exit if closed
root.protocol("WM_DELETE_WINDOW", lambda: on_closing(root))
selected_option = tk.IntVar(value=1)
sample_text_label2 = tk.Label(text="Select what mode PHITS Tools should be ran in:",anchor=tk.W,font='16')
sample_text_label2.pack(anchor=tk.W, padx=10, pady=2)
tk.Radiobutton(root, text=standard_mode_full_text, variable=selected_option, value=1).pack(anchor=tk.W)
tk.Radiobutton(root, text=dump_mode_full_text, variable=selected_option, value=2).pack(anchor=tk.W)
tk.Radiobutton(root, text=directory_mode_full_text, variable=selected_option, value=3).pack(anchor=tk.W)
tk.Radiobutton(root, text=input_file_mode_full_text, variable=selected_option, value=4).pack(anchor=tk.W)
confirm_btn = tk.Button(root, text="Select", command=on_option_selected)
confirm_btn.pack(pady=4)
root.mainloop()
# Print final settings dictionary
if in_debug_mode:
print("Settings:", settings)
if settings['main_mode'] == 1: # standard tally mode
make_PandasDF = settings['option_1_cb1']
calculate_absolute_errors = settings['option_1_cb2']
autoplot_tally_output = settings['option_1_cb3']
include_phitsout_in_metadata = settings['option_1_cb4']
parse_tally_output_file(Path(settings['file']), make_PandasDF=make_PandasDF,
calculate_absolute_errors=calculate_absolute_errors,
autoplot_tally_output=autoplot_tally_output,
include_phitsout_in_metadata=include_phitsout_in_metadata,
save_output_pickle=True, prefer_reading_existing_pickle=False)
elif settings['main_mode'] == 2: # dump tally mode
output_file_path = Path(settings['file'])
return_directional_info = settings['option_2_cb1']
use_degrees = settings['option_2_cb2']
save_namedtuple_list = False
save_Pandas_dataframe = False
if settings['radio'] == 1:
save_namedtuple_list = True
elif settings['radio'] == 2:
save_Pandas_dataframe = True
elif settings['radio'] == 3:
save_namedtuple_list = True
save_Pandas_dataframe = True
dump_data_number = settings['input_int1']
if dump_data_number != None: dump_data_number = int(dump_data_number)
dump_data_sequence = settings['input_str']
max_entries_read = settings['input_int2']
if max_entries_read != None: max_entries_read = int(max_entries_read)
if dump_data_number == None:
dump_data_number, dump_data_sequence = search_for_dump_parameters(output_file_path)
if dump_data_number == None or dump_data_sequence == None:
raise ValueError(
'You MUST provide a space-delimited list of integers to the -dvals / --dump_data_sequence input specifying ' +
'how the data columns in the dump file are to be interpreted, the same as the line following "dump = " in your PHITS tally input. ' +
'An attempt was made to automatically find these values, but it failed (thus, manual specification is required).')
parse_tally_dump_file(output_file_path, dump_data_number, dump_data_sequence,
return_directional_info=return_directional_info, use_degrees=use_degrees,
max_entries_read=max_entries_read,
return_namedtuple_list=False,
return_Pandas_dataframe=False,
save_namedtuple_list=save_namedtuple_list,
save_Pandas_dataframe=save_Pandas_dataframe)
elif settings['main_mode'] == 3 or settings['main_mode'] == 4: # directory mode or input file mode
recursive_search = settings['option_3_cb1']
include_dump_files = settings['option_3_cb2']
make_PandasDF = settings['option_3_cb3']
calculate_absolute_errors = settings['option_3_cb4']
autoplot_tally_output = settings['option_3_cb5']
autoplot_all_tally_output_in_dir = settings['option_3_cb6']
include_phitsout_in_metadata = settings['option_3_cb7']
merge_tally_outputs = settings['option_3_cb8']
file_suffix = settings['input_str_2']
if file_suffix == None: file_suffix = ''
file_prefix = settings['input_str_3']
if file_prefix == None: file_prefix = ''
file_reqstr = settings['input_str_6']
if file_reqstr == None: file_reqstr = ''
save_namedtuple_list = False
save_Pandas_dataframe = False
if settings['radio'] == 1:
save_namedtuple_list = True
elif settings['radio'] == 2:
save_Pandas_dataframe = True
elif settings['radio'] == 3:
save_namedtuple_list = True
save_Pandas_dataframe = True
dump_data_sequence = settings['input_str_1']
dump_data_number = settings['input_int_1']
if dump_data_number != None: dump_data_number = int(dump_data_number)
max_entries_read = dump_data_number = settings['input_int_2']
if max_entries_read != None: max_entries_read = int(max_entries_read)
return_directional_info = settings['extra_cb1']
use_degrees = settings['extra_cb2']
parse_all_tally_output_in_dir(Path(settings['directory']),
output_file_suffix=file_suffix, output_file_prefix=file_prefix,
output_file_required_string=file_reqstr, include_subdirectories=recursive_search,
autoplot_all_tally_output_in_dir=autoplot_all_tally_output_in_dir,
return_tally_output=False,
make_PandasDF=make_PandasDF, calculate_absolute_errors=calculate_absolute_errors,
save_output_pickle=True, prefer_reading_existing_pickle=False,
autoplot_tally_output=autoplot_tally_output,
include_dump_files=include_dump_files,
dump_data_number=dump_data_number, dump_data_sequence=dump_data_sequence,
dump_return_directional_info=return_directional_info,
dump_use_degrees=use_degrees,
dump_max_entries_read=max_entries_read,
dump_save_namedtuple_list=save_namedtuple_list,
dump_save_Pandas_dataframe=save_Pandas_dataframe,
include_phitsout_in_metadata=include_phitsout_in_metadata,
merge_tally_outputs=merge_tally_outputs
)
else:
raise ValueError('ERROR: Main mode for PHITS Tools not selected correctly in first GUI')
elif test_explicit_files_dirs:
base_path = r'G:\Cloud\OneDrive\work\PHITS\test_tallies\tally\\'
#output_file_path = Path(base_path + 't-deposit\deposit_reg.out')
#output_file_path = Path(base_path + 't-deposit\deposit_eng_sp-reg.out')
output_file_path = Path(base_path + 't-track\\track_reg.out')
#output_file_path = Path(base_path + 't-track\\track_r-z.out')
#output_file_path = Path(base_path + 't-track\\track_xyz-xy.out')
#output_file_path = Path(base_path + r't-track\track_r-z_axis-rad.out')
#output_file_path = Path(base_path + r't-track\track_r-z_axis-deg.out')
#output_file_path = Path(base_path + 't-deposit\deposit_r-z.out')
#output_file_path = Path(base_path + 't-deposit\deposit_r-z_2dtype4.out')
#output_file_path = Path(base_path + 't-deposit\deposit_r-z_2dtype5.out')
#output_file_path = Path(base_path + 't-deposit\deposit_xyz_2dtype5.out')
#output_file_path = Path(base_path + 'tet_test\deposit-tet_axis-tet.out')
#output_file_path = Path(base_path + 'tet_test\deposit-tet_axis-eng.out')
#output_file_path = Path(base_path + 't-cross\cross_reg_axis-eng.out')
#output_file_path = Path(base_path + 't-cross\cross_reg_axis-reg.out')
#output_file_path = Path(base_path + 't-cross\cross_xyz_axis-eng.out')
#output_file_path = Path(base_path + 't-cross\cross_xyz_axis-eng_enclosed.out')
###output_file_path = Path(base_path + 't-cross\cross_xyz_axis-reg.out')
#output_file_path = Path(base_path + 't-cross\cross_xyz_axis-xy.out')
#output_file_path = Path(base_path + 't-cross\cross-r-z_axis-eng.out')
#output_file_path = Path(base_path + 't-cross\cross-r-z_axis-eng_0r.out')
#output_file_path = Path(base_path + 't-cross\cross-r-z_axis-eng_enclosed.out')
#output_file_path = Path(base_path + 't-cross\complex\proton_in_hist_rz.out')
#output_file_path = Path(base_path + 't-cross\complex\\neutron_yield_rz-e-a-mesh.out')
#output_file_path = Path(base_path + 't-cross\complex\\neutron_yield.out')
#output_file_path = Path(base_path + 't-cross\complex\\xtra_neutron_yield_EvsTheta_whole-target.out')
#output_file_path = Path(base_path + 't-dpa\dpa_reg.out')
#output_file_path = Path(base_path + 't-dpa\dpa_xyz.out')
#output_file_path = Path(base_path + 't-dpa\dpa_r-z.out')
#output_file_path = Path(base_path + 'samepage\\proton_in_hist_rz_axis-eng_samepage-z.out')
#output_file_path = Path(base_path + 'samepage\\proton_in_hist_rz_reduced.out') # has NULL characters in it
#output_file_path = Path(base_path + 'samepage\\proton_in_hist_rz_sp-eng.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-e21.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-t-e1.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-t-e2.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-e1-t.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-e2-t.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-eng1.out')
#output_file_path = Path(base_path + 't-deposit2\deposit2_reg_axis-eng2.out')
#output_file_path = Path(base_path + 't-heat\heat_reg.out')
#output_file_path = Path(base_path + 't-heat\heat_xyz.out')
#output_file_path = Path(base_path + 't-interact\interact_reg.out')
#output_file_path = Path(base_path + 't-interact\interact_reg_axis-act.out')
#output_file_path = Path(base_path + 't-interact\interact_xyz.out')
#output_file_path = Path(base_path + 't-let\let-distribution_reg.out')
#output_file_path = Path(base_path + 't-let\let-distribution_r-z.out')
#output_file_path = Path(base_path + r't-point\point.out')
#output_file_path = Path(base_path + r't-point\ring.out')
#output_file_path = Path(base_path + 't-product\product_reg.out')
#output_file_path = Path(base_path + 't-sed\y-distribution_reg.out')
#output_file_path = Path(base_path + 't-sed\y-distribution_xyz.out')
#output_file_path = Path(base_path + r't-time\time_reg.out')
#output_file_path = Path(base_path + r't-time\time_xyz.out')
#output_file_path = Path(base_path + r't-yield\yield_reg_axis-charge.out')
#output_file_path = Path(base_path + r't-yield\yield_reg_axis-mass.out')
#output_file_path = Path(base_path + r't-yield\yield_reg_axis-chart.out')
#output_file_path = Path(base_path + r't-yield\yield_xyz_axis-chart.out')
#base_path = r'G:\Cloud\OneDrive\work\PHITS\test_tallies\\'
#output_file_path = Path(base_path + r'tally\t-deposit\deposit_reg_spec-all.out')
#output_file_path = Path(base_path + r'sample\icrp\mrcp\External\result\Dose_MRCP-AF_reg.out')
#output_file_path = Path(base_path + r'sample\misc\batch_source\track_yz_001.out')
#output_file_path = Path(base_path + r'sample\source\Cosmicray\GCR-ground\cross.out')
#output_file_path = Path(base_path + r'recommendation\Fusion\track.out')
#output_file_path = Path(base_path + r'sample\benchmark\Iwamoto-JNST2022\case5-isis800\output\rr_air_model.out')
#output_file_path = Path(base_path + r'recommendation\muon\product.out')
#output_file_path = Path(base_path + r'recommendation\BNCT\dose.out')
#output_file_path = Path(base_path + r'recommendation\SemiConductor\deposit.out')
#output_file_path = Path(base_path + r'recommendation\Shielding\track-rz.out')
#output_file_path = Path(base_path + r'recommendation\TrackStructure\interact.out')
#output_file_path = Path(base_path + r'recommendation\CosmicRay\dose.out') # single value output
#output_file_path = Path(base_path + r'recommendation\CosmicRay\track.out')
#output_file_path = Path(base_path + r'sample\source\Cosmicray\Airshower\depthdose.out')
#output_file_path = Path(base_path + r'sample\misc\history_counter\track_yz_ch1.out')
#output_file_path = Path(base_path + r'recommendation\Counter\track.out')
#output_file_path = Path(base_path + r'MPI_dumps\\CT_1D_phantom\\neutron_yield.out')
phitsout_file_path = Path(output_file_path.parent, 'phits.out')
input_file_path = Path(r'G:\Cloud\OneDrive\work\PHITS\test_tallies\MPI_dumps\CT_1D_phantom\beam-on-target_phits-input.inp')
#input_file_path = Path(r'G:\Cloud\OneDrive\work\PHITS\test_tallies\tally\t-cross\complex\beam-on-target.inp')
phitsout_file_path = Path(input_file_path.parent, 'phits.out')
test_parsing_of_dir = False #True
if test_parsing_of_dir:
dir_path = output_file_path = Path(base_path + r't-cross\complex\proton_in_hist_rz.out')
dir_output_list = parse_all_tally_output_in_dir(dir_path)
print(dir_output_list)
sys.exit()
test_parsing_of_dir_with_merge = False # True
if test_parsing_of_dir_with_merge:
dir_path = output_file_path = Path(base_path + r't-cross\complex\proton_in_hist_rz.out')
#dir_path = output_file_path = Path(base_path + r't-cross\complex\beam-on-target.inp')
dir_output_dict = parse_all_tally_output_in_dir(dir_path,
merge_tally_outputs=True,
return_tally_output=True,
compress_pickle_with_lzma=True,
#include_phitsout_in_metadata=True,
include_subdirectories=False,
prefer_reading_existing_pickle=False,
save_output_pickle=False,
save_pickle_of_merged_tally_outputs=True
)
print(dir_output_dict.keys())
print(dir_output_dict['energy_deposit.out']['tally_metadata'].keys())
#pprint.pp(dir_output_dict)
sys.exit()
test_large_dump_splitting = False
if test_large_dump_splitting:
base_path = r'G:\Cloud\OneDrive\work\PHITS\test_tallies\\'
dump_file_path = Path(base_path + r'big_dumps\\proton_track_dmp.out')
print('Elapsed time before reading dump file: {:0.2f}'.format(time.time() - start))
x = parse_tally_dump_file(dump_file_path, save_namedtuple_list=True, save_Pandas_dataframe=True,
return_namedtuple_list=False, return_Pandas_dataframe=False)
print('Elapsed time after saving dump file output containers: {:0.2f}'.format(time.time() - start))
sys.exit()
test_parsing_of_MPI_dump_dir = False
if test_parsing_of_MPI_dump_dir:
base_path = r'G:\Cloud\OneDrive\work\PHITS\test_tallies\\'
dir_path = output_file_path = Path(base_path + r'MPI_dumps\\CT_1D_phantom\\')
#dump_file_path = Path(dir_path, 'gamma_yield_dmp.out.004')
#print(dump_file_path.is_file(),dump_file_path)
#nt_list, df = parse_tally_dump_file(dump_file_path, save_namedtuple_list=True, save_Pandas_dataframe=True)
dir_output_list = parse_all_tally_output_in_dir(dir_path, include_dump_files=True)
print(dir_output_list)
sys.exit()
test_dump_file = False
if test_dump_file:
#dump_file_path = Path(base_path + 't-cross\complex\\neutron_yield_dmp.out')
#dump_control_str = '2 3 4 5 6 7 8 10'
dump_file_path = Path(base_path + r'tally\\t-product\\dump\\product_dmp.out')
dump_control_str = '1 2 3 4 8 10 9 17 18 19 20'
#nt_list, df = parse_tally_dump_file(dump_file_path,8,dump_control_str, save_namedtuple_list=True, save_Pandas_dataframe=True)
# test automatic finding of dump parameters
print('Elapsed time before reading dump file: {:0.2f}'.format(time.time() - start))
nt_list, df = parse_tally_dump_file(dump_file_path, save_namedtuple_list=True, save_Pandas_dataframe=True)
print('Elapsed time after saving dump file output containers: {:0.2f}'.format(time.time() - start))
# test dill of namedtuple list
#import dill
import pickle
import lzma
#path_to_pickle_file = Path(base_path + 't-cross\complex\\neutron_yield_dmp_namedtuple_list.dill.xz')
path_to_pickle_file = Path(base_path + r'tally\\t-product\\dump\\product_dmp_namedtuple_list.pickle.xz')
with lzma.open(path_to_pickle_file, 'rb') as handle:
nt_list_dill = pickle.load(handle)
print(nt_list[0])
print(nt_list_dill[0])
print(nt_list_dill[0].kf, nt_list[0].kf)
print(nt_list_dill[0]['kf']) #, nt_list[0]['kf'])
print(nt_list_dill.dtype.names)
if [tuple(t) for t in nt_list]==[tuple(t) for t in nt_list_dill]: print('It works!')
sys.exit()
testing_phitsout_parsing = False
if testing_phitsout_parsing:
phitsout_meta = parse_phitsout_file(phitsout_file_path)
pprint.pp(dict(phitsout_meta))
sys.exit()
testing_phits_input_parsing = False
if testing_phits_input_parsing:
x = extract_tally_outputs_from_phits_input(input_file_path)
pprint.pp(x)
phitsout_file_path = Path(input_file_path.parent, 'phits.out')
y = extract_tally_outputs_from_phits_input(phitsout_file_path)
pprint.pp(y)
print(x==y)
sys.exit()
testing_dir_mode_with_phits_input = True
if testing_dir_mode_with_phits_input:
x = parse_all_tally_output_in_dir(phitsout_file_path,
include_dump_files=False,
include_phitsout_in_metadata=None,
autoplot_all_tally_output_in_dir=False)
print(x)
sys.exit()
tally_output_filepath = output_file_path
tally_output = parse_tally_output_file(tally_output_filepath, make_PandasDF=True, calculate_absolute_errors=True,
save_output_pickle=True)
tally_data = tally_output['tally_data']
tally_metadata = tally_output['tally_metadata']
tally_df = tally_output['tally_dataframe']
pprint.pp(dict(tally_metadata))
testing_autoplot = False
if testing_autoplot:
import matplotlib.pyplot as plt
fg_list = autoplot_tally_results(tally_output, return_fg_list=True) #, additional_save_extensions=['.png','jpeg'])
#fg = fg_list[0]
#for ax in fg.axes.flat:
# ax.set_facecolor((0, 1, 0, 1))
# ax.set_xlabel('TEST', visible=True)
#plt.show()
sys.exit()
# ir, iy, iz, ie, it, ia, il, ip, ic, ierr
print(tally_data[ :, 0, 0, :, 0, 0, 0, 0, 0, 0])
print(tally_data[ :, 0, 0, :, 0, 0, 0, 0, 0, 1])
print(tally_data[0, :, :, 0, 0, 0, 0, 0, 0, 0])
print(np.shape(tally_data))
#print(tally_data[ 1, 0, 0, 0, 0, 0, 0, 0, :, 0])
#print(tally_metadata['nuclide_ZZZAAAM_list'])
#print(tally_metadata['nuclide_isomer_list'])
#ic = tally_metadata['nuclide_ZZZAAAM_list'].index(10020)
#print(tally_data[1, 0, 0, 0, 0, 0, 0, 0, ic, 0])
Functions
def parse_tally_output_file(tally_output_filepath, make_PandasDF=True, calculate_absolute_errors=True, save_output_pickle=True, include_phitsout_in_metadata=False, prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False, autoplot_tally_output=False)
-
Description
Parse any PHITS tally output file, returning tally metadata and an array of its values (and optionally this data inside of a Pandas dataframe too). Note the separate
parse_tally_dump_file()
function for parsing PHITS dump files. If a DCHAIN input file (output from the [T-Dchain] tally) or DCHAIN output*.act
file is provided, an attempt will be made to import the DCHAIN Tools module and process the found DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file.Dependencies
import numpy as np
import pandas as pd
(ifmake_PandasDF = True
)import seaborn as sns
(ifautoplot_tally_output = True
)from munch import Munch
(will still run if package not found)
Inputs
(required)
tally_output_filepath
= string or Path object denoting the path to the tally output file to be parsed
Inputs
(optional)
make_PandasDF
= (D=True
) A Boolean denoting whether a Pandas dataframe of the tally data array will be madecalculate_absolute_errors
= (D=True
) A Boolean determining whether the absolute uncertainty of each tally output value is to be calculated (simply as the product of the value and relative error); ifFalse
, the final dimension oftally_data
,ierr
, will be of length-2 rather than length-3save_output_pickle
= (D=True
) A Boolean determining whether thetally_output
dictionary object is saved as a pickle file; ifTrue
, the file will be saved with the same path and name as the provided PHITS tally output file but with the .pickle extension.include_phitsout_in_metadata
= (D=False
) A Boolean determining whether the "phits.out" file (file(6)
in the [Parameters] section of a PHITS input file) in the same directory astally_output_filepath
, if found, should be processed viaparse_phitsout_file()
and have its informational dictionary about the PHITS run added to the'tally_metadata'
dictionary under the key'phitsout'
. IfTrue
, this function assumesfile(6) = phits.out
(the default setting) in the PHITS input.- If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided
dictionary will be taken as that to be added as the
'phitsout'
key in the'tally_metadata'
dictionary. - Otherwise, if a string or
pathlib.Path()
object is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its informational dictionary added to the'tally_metadata'
dictionary. - Setting
include_phitsout_in_metadata = None
functions the same asinclude_phitsout_in_metadata = True
but setsinclude_input_echo = False
in theparse_phitsout_file()
call.
- If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided
dictionary will be taken as that to be added as the
prefer_reading_existing_pickle
= (D=False
) A Boolean determining what this function does if the pickle file this function seeks to generate already exists. IfFalse
(default behavior), this function will parse the PHITS output files as usual and overwrite the existing pickle file. IfTrue
, this function will instead simply just read the existing found pickle file and return its storedtally_output
contents.compress_pickle_with_lzma
= (D=False
; requiressave_output_pickle=True
) Boolean designating whether the pickle file to be saved will be compressed with LZMA compression (included in the baseline Python standard library); if so, the pickle file's extension will be'.pickle.xz'
instead of just'.pickle'
. A *.pickle.xz file can then be opened (after importingpickle
andlzma
) as:
with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)
.
For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for output from massive tallies.autoplot_tally_output
= (D=False
; requiresmake_PandasDF=True
) Boolean denoting whether the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path astally_output_filepath
but ending in.pdf
/.png
) using theautoplot_tally_results()
function.
Output
tally_output
= a dictionary object with the below keys and values:'tally_data'
= a 10-dimensional NumPy array containing all tally results, explained in more detail below'tally_metadata'
= a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units'tally_dataframe'
= a Pandas dataframe version oftally_data
(None
ifmake_PandasDF = False
)
†If you have the Munch package installed, the
tally_metadata
dictionary will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access (e.g.,tally_metadata.mesh
instead of onlytally_metadata['mesh']
). If you do not have Munch, thentally_metadata
will just be a standard dictionary object. If, for whatever reason, you have Munch installed but do not wish fortally_metadata
to be a Munch object, then in the first line of code for theparse_tally_header()
function, setprefer_to_munch_meta_dict = False
.
Notes
Quick code for loading pickled tally output
import pickle, lzma; from pathlib import Path p = Path('path/to/pickle/file.pickle[.xz]') tally_output = pickle.load(lzma.open(p, 'rb') if p.name[-3:]=='.xz' else open(p, 'rb')) tally_data, tally_metadata, tally_dataframe = [tally_output[k] for k in tally_output.keys()]
The
'tally_data'
10-dimensional NumPy arrayMany quantities can be scored across the various tallies in the PHITS code. This function outputs a "universal" array
tally_data
(tally_data = tally_output['tally_data']
) that can accomodate all of the different scoring geometry meshes, physical quantities with assigned meshes, and output axes provided within PHITS. This is achieved with a 10-dimensional array accessible astally_data[ ir, iy, iz, ie, it, ia, il, ip, ic, ierr ]
, with indices explained below:Tally data indices and corresponding mesh/axis:
0
|ir
, Geometry mesh:reg
/x
/r
/tet
([T-Cross]ir surf
ifmesh=r-z
withenclos=0
)1
|iy
, Geometry mesh:1
/y
/1
2
|iz
, Geometry mesh:1
/z
/z
([T-Cross]iz surf
ifmesh=xyz
ormesh=r-z
withenclos=0
)3
|ie
, Energy mesh:eng
([T-Deposit2]eng1
)4
|it
, Time mesh5
|ia
, Angle mesh6
|il
, LET mesh7
|ip
, Particle type (part =
)8
|ic
, Special: [T-Deposit2]eng2
; [T-Yield]mass
,charge
,chart
; [T-Interact]act
9
|ierr = 0/1/2
, Value / relative uncertainty / absolute uncertainty (expanded to3/4/5
, or2/3
ifcalculate_absolute_errors = False
, for [T-Cross]mesh=r-z
withenclos=0
case; see notes further below)
By default, all array dimensions are length-1 (except
ierr
, which is length-3). These dimensions are set/corrected automatically when parsing the tally output file. Thus, for very simple tallies, most of these indices will be set to 0 when accessing tally results, e.g.tally_data[2,0,0,:,0,0,0,:,0,:]
to access the full energy spectrum in the third region for all scored particles / particle groups with the values and uncertainties.
The
'tally_metadata'
dictionaryThe output
tally_metadata
dictionary contains all information needed to identify every bin along every dimension: region numbers/groups, particle names/groups, bin edges and midpoints for all mesh types (x, y, z, r, energy, angle, time, and LET) used in the tally.It typically contains quite a bit of information. To conveniently view its contents, one can import the built-in pprint library
import pprint
and then usepprint.pp(dict(tally_output['tally_metadata']))
.At a basic level, the "header" of the PHITS output file (everything before the first instance of
newpage:
) is parsed and, in effect, a key+value pair is created for every line containing an equals sign=
.The following keys exist denoting the lengths of the first 9 axes/dimensions of the
tally_data
NumPy array:'nreg'
/'nx'
/'nr'
/'ntet'
,'ny'
,'nz'
,'ne'
,'nt'
,'na'
,'nl'
,'npart'
, and'nc'
. These keys are all initialized asNone
and assigned a value if found in the PHITS output file. Thus, if one of these retains its value ofNone
, the corresponding array axis length should be 1. The'found_mesh_kinds'
key lists which of these meshes are identified. For these values with a defined numerical mesh, taking energy'e'
for example here, a number of different keys and values will be read/generated for the metadata dictionary, such as:'e-type'
,'emin'
,'emax'
,'edel'
,'e-mesh_bin_edges'
,'e-mesh_bin_mids'
, and'e-mesh_bin_mids_log'
.Complications arise in PHITS allowing the flexibility of grouping regions and particles together for scoring. For example, something like
(1 2 5)
can be provided toreg =
or something like(neutron photon)
can be provided topart =
in a tally. To accomodate this, some additional key+value pairs are introduced for the metadata dictionary:For regions:
'reg'
= the string followingreg =
in the PHITS output'reg_groups'
= a list of strings of each "group" of region(s) provided toreg =
in the PHITS output'num_reg_groups'
= an integer of the length of'reg_groups'
, which is also the length of the first dimension of thetally_data
array and should equal'nreg'
'nreg'
= an integer of the number of scoring regions, should equal'num_reg_groups'
'reg_serial_num'
= a list of integers counting the region groups, starting at 1'reg_num'
= a list of unique strings of the region numbers found (for individual regions) or assigned by PHITS (for combined regions)
For particles:
'part'
= the string followingpart =
in the PHITS output'npart'
= the integer number of particle scoring groups provided topart =
in the PHITS output'part_groups'
= a list of strings of each "group" of particle(s) provided topart =
in the PHITS output'kf_groups'
= a list of strings like'part_groups'
but containing the kf-code particle identification numbers (see PHITS manual Table 4.4) for each particle/group;0
is assigned topart = all
'part_serial_groups'
= a list of strings with the serialized name of each particle scoring group ('p1-group'
,'p2-group'
, etc.)
Perhaps of use for quickly making plots using PHITS Tools, the following keys also exist:
'title'
= the title string of the plots used in the .eps files'axis_dimensions'
=1
or2
denoting whether the value provided toaxis =
indicated one or two dimensional plots'axis_index_of_tally_array'
= integer specifying what axis oftally_data
should be accessed for the quantity provided toaxis =
'axis1_label'
= the string of the horizontal axis label of the plots used in the .eps files'value_label'
= the string of the vertical axis label of the 1D plots used in the .eps files'axis2_label'
= the string of the vertical axis label of the 2D plots used in the .eps files
Note that for some tallies there may be additional special entries (like for [T-Point]) or that some of the key names may differ slightly from those stated here (such as for [T-Deposit2]).
If the
include_phitsout_in_metadata
setting is enabled,parse_phitsout_file()
will be called to provide a dictionary containing metadata about the PHITS run itself—by default also including the PHITS run's input echo—added to this metadata dictionary under the'phitsout'
key. See theparse_phitsout_file()
function's description for more details about the structure of this dictionary.
The
'tally_dataframe'
Pandas DataFrameThe
tally_dataframe
Pandas dataframe output functions as normal. The dataframe contains a number of rows equal to the number of Values in the NumPy array (i.e., the product of the lengths of the first nine dimensions of the ten-dimensional array) and extra columns for each array dimension of length greater than one (e.g., if the tally includes an energy and/or angle mesh, columns will be present stating the energy/angle bin of each row). A dictionary containing supplemental information that is common to all rows of the dataframe is included and can be accessed withtally_dataframe.attrs
.
Exceptions
Unsupported tallies and DCHAIN
The following tallies are NOT supported by this function:
- [T-WWG], [T-WWBG], and [T-Volume] (due to being "helper" tallies for generating text sections meant for reinsertion into a PHITS input file)
- [T-Gshow], [T-Rshow], [T-3Dshow], and [T-4Dtrack] (due to being visualization tallies meant for ANGEL/PHIG-3D)
- [T-Userdefined] (due to having no standard format)
- [T-Dchain]‡
‡If provided with the output file of [T-Dchain] (the input file for the DCHAIN code) or the
*.act
main output file produced by the DCHAIN code, this function will attempt to import the DCHAIN Tools module and process the found DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file. Otherwise, for [T-Dchain] or [T-Yield] withaxis = dchain
, you can manually use the separate suite of parsing functions included in the DCHAIN Tools module.
[T-Cross] special case
The [T-Cross] tally is unique (scoring across region boundaries rather than within regions), creating some additional challenges. In the
mesh = reg
case, much is the same except each region number is composed of ther-from
andr-to
values, e.g.'100 - 101'
.For
xyz
andr-z
meshes, an additional parameter is at play:enclos
. By default,enclos=0
. In the eventenclos=1
is set, the total number of geometric regions is still eithernx*ny*nz
ornr*nz
forxyz
andr-z
meshes, respectively. Forenclos=0
in themesh = xyz
case, the length of the z dimension (iz
index) is instead equal tonzsurf
, which is simply one greater thannz
(# regions =nx*ny*(nz+1)
).For
enclos=0
in themesh = r-z
case, this is much more complicated as PHITS will output every combination ofnr*nzsurf
ANDnrsurf*nz
, notingnzsurf=nz+1
andnrsurf=nr+1
(ornrsurf=nr
if the first radius bin edge isr=0.0
). The solution implemented here is to, for only this circumstance (in only theenclos=0 mesh=r-z
case), set the length of their
andiz
dimensions tonrsurf
andnzsurf
, respectively, and also to expand the length of the final dimension oftally_data
from 3 to 6 (or from 2 to 4 ifcalculate_absolute_errors=False
), where:ierr = 0/1/2
refer to the combinations ofnr
andnzsurf
(or0/1
ifcalculate_absolute_errors=False
)ierr = 3/4/5
refer to the combinations ofnrsurf
andnz
(or2/3
ifcalculate_absolute_errors=False
)
In this case, the Pandas dataframe, if enabled, will contain 3 (or 2) extra columns
value2
andrel.err.2
[andabs.err.2
], which correspond to the combinations ofnrsurf
andnz
(while the original columns without the "2" refer to values for combinations of andnr
andnzsurf
).
[T-Yield] special case
[T-Yield] is also a bit exceptional. When setting the
axis
parameter equal tocharge
,mass
, orchart
, theic
dimension oftally_data
is used for each entry of charge (proton number, Z), mass (A), or isotope/isomer, respectively.In the case of
axis = charge
oraxis = mass
, the value ofic
refers to the actual charge/proton number Z or mass number A when accessingtally_data
; for instance,tally_data[:,:,:,:,:,:,:,:,28,:]
references results from nuclei with Z=28 ifaxis = charge
or A=28 ifaxis = mass
. The length of theic
dimension is initialized as 130 or 320 but is later reduced to only just include the highest charge or mass value.In the case of
axis = chart
, the length of theic
dimension is initially set equal to themxnuclei
parameter in the [T-Yield] tally. Ifmxnuclei = 0
is set, then the length of theic
dimension is initially set to 10,000. Thisic
dimension length is later reduced to the total number of unique nuclides found in the output. Owing to the huge number of possible nuclides, a list of found nuclides with nonzero yield is assembled and added totally_metadata
under the keysnuclide_ZZZAAAM_list
andnuclide_isomer_list
, i.e.tally_metadata['nuclide_ZZZAAAM_list']
andtally_metadata['nuclide_isomer_list']
. These lists should be referenced to see what nuclide each of indexic
refers to. The entries of the ZZZAAAM list are intergers calculated with the formula 10000*Z + 10*A + M, where M is the metastable state of the isomer (0 = ground state, 1 = 1st metastable/isomeric state, etc.). The entries of the isomer list are these same nuclides in the same order but written as plaintext strings, e.g.'Al-28'
and'Xe-133m1'
. The lists are ordered in the same order nuclides are encountered while parsing the output file. Thus, to sensibly access the yield of a specific nuclide, one must first find its indexic
in one of the two metadata lists of ZZZAAAM values or isomer names and then use that to accesstally_data
. For example, to get the yield results of production of carbon-14 (C-14), one would use the following code:ic = tally_metadata['nuclide_ZZZAAAM_list'].index(60140)
OR
ic = tally_metadata['nuclide_isomer_list'].index('C-14')
then
my_yield_values = tally_data[:,:,:,:,:,:,:,:,ic,:]
Expand source code
def parse_tally_output_file(tally_output_filepath, make_PandasDF = True, calculate_absolute_errors = True, save_output_pickle=True, include_phitsout_in_metadata=False, prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False, autoplot_tally_output=False): ''' Description: Parse any PHITS tally output file, returning tally metadata and an array of its values (and optionally this data inside of a Pandas dataframe too). Note the separate `parse_tally_dump_file` function for parsing PHITS dump files. If a DCHAIN input file (output from the [T-Dchain] tally) or DCHAIN output `*.act` file is provided, an attempt will be made to import the [DCHAIN Tools module](https://github.com/Lindt8/DCHAIN-Tools) and process the found DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file. Dependencies: - `import numpy as np` - `import pandas as pd` (if `make_PandasDF = True`) - `import seaborn as sns` (if `autoplot_tally_output = True`) - `from munch import Munch` (will still run if package not found) Inputs: (required) - **`tally_output_filepath`** = string or Path object denoting the path to the tally output file to be parsed Inputs: (optional) - **`make_PandasDF`** = (D=`True`) A Boolean denoting whether a Pandas dataframe of the tally data array will be made - **`calculate_absolute_errors`** = (D=`True`) A Boolean determining whether the absolute uncertainty of each tally output value is to be calculated (simply as the product of the value and relative error); if `False`, the final dimension of `tally_data`, `ierr`, will be of length-2 rather than length-3 - **`save_output_pickle`** = (D=`True`) A Boolean determining whether the `tally_output` dictionary object is saved as a pickle file; if `True`, the file will be saved with the same path and name as the provided PHITS tally output file but with the .pickle extension. - **`include_phitsout_in_metadata`** = (D=`False`) A Boolean determining whether the "phits.out" file (`file(6)` in the [Parameters] section of a PHITS input file) in the same directory as `tally_output_filepath`, if found, should be processed via `parse_phitsout_file()` and have its informational dictionary about the PHITS run added to the `'tally_metadata'` dictionary under the key `'phitsout'`. If `True`, this function assumes `file(6) = phits.out` (the default setting) in the PHITS input. - If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided dictionary will be taken as that to be added as the `'phitsout'` key in the `'tally_metadata'` dictionary. - Otherwise, if a string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its informational dictionary added to the `'tally_metadata'` dictionary. - Setting `include_phitsout_in_metadata = None` functions the same as `include_phitsout_in_metadata = True` but sets `include_input_echo = False` in the `parse_phitsout_file()` call. - **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the pickle file this function seeks to generate already exists. If `False` (default behavior), this function will parse the PHITS output files as usual and overwrite the existing pickle file. If `True`, this function will instead simply just read the existing found pickle file and return its stored `tally_output` contents. - **`compress_pickle_with_lzma`** = (D=`False`; requires `save_output_pickle=True`) Boolean designating whether the pickle file to be saved will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included in the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`. A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as: `with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`. For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for output from massive tallies. - **`autoplot_tally_output`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting whether the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path as `tally_output_filepath` but ending in `.pdf`/`.png`) using the `autoplot_tally_results()` function. Output: - **`tally_output`** = a dictionary object with the below keys and values: - `'tally_data'` = a 10-dimensional NumPy array containing all tally results, explained in more detail below - `'tally_metadata'` = a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units - `'tally_dataframe'` = a Pandas dataframe version of `tally_data` (`None` if `make_PandasDF = False`) †_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `tally_metadata` dictionary will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access (e.g., `tally_metadata.mesh` instead of only `tally_metadata['mesh']`). If you do not have Munch, then `tally_metadata` will just be a standard dictionary object. If, for whatever reason, you have Munch installed but do not wish for `tally_metadata` to be a Munch object, then in the first line of code for the `parse_tally_header()` function, set `prefer_to_munch_meta_dict = False`._ ----- ----- Notes: **Quick code for loading pickled tally output** import pickle, lzma; from pathlib import Path p = Path('path/to/pickle/file.pickle[.xz]') tally_output = pickle.load(lzma.open(p, 'rb') if p.name[-3:]=='.xz' else open(p, 'rb')) tally_data, tally_metadata, tally_dataframe = [tally_output[k] for k in tally_output.keys()] ----- **The `'tally_data'` 10-dimensional NumPy array** Many quantities can be scored across the various tallies in the PHITS code. This function outputs a "universal" array `tally_data` (`tally_data = tally_output['tally_data']`) that can accomodate all of the different scoring geometry meshes, physical quantities with assigned meshes, and output axes provided within PHITS. This is achieved with a 10-dimensional array accessible as `tally_data[ ir, iy, iz, ie, it, ia, il, ip, ic, ierr ]`, with indices explained below: Tally data indices and corresponding mesh/axis: - `0` | `ir`, Geometry mesh: `reg` / `x` / `r` / `tet` ([T-Cross] `ir surf` if `mesh=r-z` with `enclos=0`) - `1` | `iy`, Geometry mesh: `1` / `y` / `1` - `2` | `iz`, Geometry mesh: `1` / `z` / `z` ([T-Cross] `iz surf` if `mesh=xyz` or `mesh=r-z` with `enclos=0`) - `3` | `ie`, Energy mesh: `eng` ([T-Deposit2] `eng1`) - `4` | `it`, Time mesh - `5` | `ia`, Angle mesh - `6` | `il`, LET mesh - `7` | `ip`, Particle type (`part = `) - `8` | `ic`, Special: [T-Deposit2] `eng2`; [T-Yield] `mass`, `charge`, `chart`; [T-Interact] `act` - `9` | `ierr = 0/1/2`, Value / relative uncertainty / absolute uncertainty (expanded to `3/4/5`, or `2/3` if `calculate_absolute_errors = False`, for [T-Cross] `mesh=r-z` with `enclos=0` case; see notes further below) By default, all array dimensions are length-1 (except `ierr`, which is length-3). These dimensions are set/corrected automatically when parsing the tally output file. Thus, for very simple tallies, most of these indices will be set to 0 when accessing tally results, e.g. `tally_data[2,0,0,:,0,0,0,:,0,:]` to access the full energy spectrum in the third region for all scored particles / particle groups with the values and uncertainties. ----- **The `'tally_metadata'` dictionary** The output `tally_metadata` dictionary contains all information needed to identify every bin along every dimension: region numbers/groups, particle names/groups, bin edges and midpoints for all mesh types (x, y, z, r, energy, angle, time, and LET) used in the tally. It typically contains quite a bit of information. To conveniently view its contents, one can import the built-in pprint library `import pprint` and then use `pprint.pp(dict(tally_output['tally_metadata']))`. At a basic level, the "header" of the PHITS output file (everything before the first instance of `newpage:`) is parsed and, in effect, a key+value pair is created for every line containing an equals sign `=`. The following keys exist denoting the lengths of the first 9 axes/dimensions of the `tally_data` NumPy array: `'nreg'`/`'nx'`/`'nr'`/`'ntet'`, `'ny'`, `'nz'`, `'ne'`, `'nt'`, `'na'`, `'nl'`, `'npart'`, and `'nc'`. These keys are all initialized as `None` and assigned a value if found in the PHITS output file. Thus, if one of these retains its value of `None`, the corresponding array axis length should be 1. The `'found_mesh_kinds'` key lists which of these meshes are identified. For these values with a defined numerical mesh, taking energy `'e'` for example here, a number of different keys and values will be read/generated for the metadata dictionary, such as: `'e-type'`, `'emin'`, `'emax'`, `'edel'`, `'e-mesh_bin_edges'`, `'e-mesh_bin_mids'`, and `'e-mesh_bin_mids_log'`. Complications arise in PHITS allowing the flexibility of grouping regions and particles together for scoring. For example, something like `(1 2 5)` can be provided to `reg = ` or something like `(neutron photon)` can be provided to `part = ` in a tally. To accomodate this, some additional key+value pairs are introduced for the metadata dictionary: For regions: - `'reg'` = the string following `reg = ` in the PHITS output - `'reg_groups'` = a list of strings of each "group" of region(s) provided to `reg = ` in the PHITS output - `'num_reg_groups'` = an integer of the length of `'reg_groups'`, which is also the length of the first dimension of the `tally_data` array and should equal `'nreg'` - `'nreg'` = an integer of the number of scoring regions, should equal `'num_reg_groups'` - `'reg_serial_num'` = a list of integers counting the region groups, starting at 1 - `'reg_num'` = a list of unique strings of the region numbers found (for individual regions) or assigned by PHITS (for combined regions) For particles: - `'part'` = the string following `part = ` in the PHITS output - `'npart'` = the integer number of particle scoring groups provided to `part = ` in the PHITS output - `'part_groups'` = a list of strings of each "group" of particle(s) provided to `part = ` in the PHITS output - `'kf_groups'` = a list of strings like `'part_groups'` but containing the kf-code particle identification numbers (see PHITS manual Table 4.4) for each particle/group; `0` is assigned to `part = all` - `'part_serial_groups'` = a list of strings with the serialized name of each particle scoring group (`'p1-group'`, `'p2-group'`, etc.) Perhaps of use for quickly making plots using PHITS Tools, the following keys also exist: - `'title'` = the title string of the plots used in the .eps files - `'axis_dimensions'` = `1` or `2` denoting whether the value provided to `axis = ` indicated one or two dimensional plots - `'axis_index_of_tally_array'` = integer specifying what axis of `tally_data` should be accessed for the quantity provided to `axis = ` - `'axis1_label'` = the string of the horizontal axis label of the plots used in the .eps files - `'value_label'` = the string of the vertical axis label of the 1D plots used in the .eps files - `'axis2_label'` = the string of the vertical axis label of the 2D plots used in the .eps files Note that for some tallies there may be additional special entries (like for [T-Point]) or that some of the key names may differ slightly from those stated here (such as for [T-Deposit2]). If the `include_phitsout_in_metadata` setting is enabled, `parse_phitsout_file()` will be called to provide a dictionary containing metadata about the PHITS run itself—by default also including the PHITS run's input echo—added to this metadata dictionary under the `'phitsout'` key. See the `parse_phitsout_file()` function's description for more details about the structure of this dictionary. ----- **The `'tally_dataframe'` Pandas DataFrame** The `tally_dataframe` Pandas dataframe output functions as normal. The dataframe contains a number of rows equal to the number of Values in the NumPy array (i.e., the product of the lengths of the first nine dimensions of the ten-dimensional array) and extra columns for each array dimension of length greater than one (e.g., if the tally includes an energy and/or angle mesh, columns will be present stating the energy/angle bin of each row). A dictionary containing supplemental information that is common to all rows of the dataframe is included and can be accessed with `tally_dataframe.attrs`. ----- ----- Exceptions: **Unsupported tallies and DCHAIN** The following tallies are NOT supported by this function: - [T-WWG], [T-WWBG], and [T-Volume] (due to being "helper" tallies for generating text sections meant for reinsertion into a PHITS input file) - [T-Gshow], [T-Rshow], [T-3Dshow], and [T-4Dtrack] (due to being visualization tallies meant for ANGEL/PHIG-3D) - [T-Userdefined] (due to having no standard format) - [T-Dchain]‡ ‡If provided with the output file of [T-Dchain] (the input file for the DCHAIN code) or the `*.act` main output file produced by the DCHAIN code, this function will attempt to import the [DCHAIN Tools module](https://github.com/Lindt8/DCHAIN-Tools) and process the found DCHAIN output files, returning the output dictionary object and (optionally) saving it to a pickle file. Otherwise, for [T-Dchain] or [T-Yield] with `axis = dchain`, you can manually use the separate suite of parsing functions included in the [DCHAIN Tools](https://github.com/Lindt8/DCHAIN-Tools) module. ----- **[T-Cross] special case** The [T-Cross] tally is unique (scoring across region boundaries rather than within regions), creating some additional challenges. In the `mesh = reg` case, much is the same except each region number is composed of the `r-from` and `r-to` values, e.g. `'100 - 101'`. For `xyz` and `r-z` meshes, an additional parameter is at play: `enclos`. By default, `enclos=0`. In the event `enclos=1` is set, the total number of geometric regions is still either `nx*ny*nz` or `nr*nz` for `xyz` and `r-z` meshes, respectively. For `enclos=0` in the `mesh = xyz` case, the length of the z dimension (`iz` index) is instead equal to `nzsurf`, which is simply one greater than `nz` (# regions = `nx*ny*(nz+1)`). For `enclos=0` in the `mesh = r-z` case, this is much more complicated as PHITS will output every combination of `nr*nzsurf` AND `nrsurf*nz`, noting `nzsurf=nz+1` and `nrsurf=nr+1` (or `nrsurf=nr` if the first radius bin edge is `r=0.0`). The solution implemented here is to, for only this circumstance (in only the `enclos=0 mesh=r-z` case), set the length of the `ir` and `iz` dimensions to `nrsurf` and `nzsurf`, respectively, and also to expand the length of the final dimension of `tally_data` from 3 to 6 (or from 2 to 4 if `calculate_absolute_errors=False`), where: - `ierr = 0/1/2` refer to the combinations of `nr` and `nzsurf` (or `0/1` if `calculate_absolute_errors=False`) - `ierr = 3/4/5` refer to the combinations of `nrsurf` and `nz` (or `2/3` if `calculate_absolute_errors=False`) In this case, the Pandas dataframe, if enabled, will contain 3 (or 2) extra columns `value2` and `rel.err.2` [and `abs.err.2`], which correspond to the combinations of `nrsurf` and `nz` (while the original columns without the "2" refer to values for combinations of and `nr` and `nzsurf`). ----- **[T-Yield] special case** [T-Yield] is also a bit exceptional. When setting the `axis` parameter equal to `charge`, `mass`, or `chart`, the `ic` dimension of `tally_data` is used for each entry of charge (proton number, Z), mass (A), or isotope/isomer, respectively. In the case of `axis = charge` or `axis = mass`, the value of `ic` refers to the actual charge/proton number Z or mass number A when accessing `tally_data`; for instance, `tally_data[:,:,:,:,:,:,:,:,28,:]` references results from nuclei with Z=28 if `axis = charge` or A=28 if `axis = mass`. The length of the `ic` dimension is initialized as 130 or 320 but is later reduced to only just include the highest charge or mass value. In the case of `axis = chart`, the length of the `ic` dimension is initially set equal to the `mxnuclei` parameter in the [T-Yield] tally. If `mxnuclei = 0` is set, then the length of the `ic` dimension is initially set to 10,000. This `ic` dimension length is later reduced to the total number of unique nuclides found in the output. Owing to the huge number of possible nuclides, a list of found nuclides with nonzero yield is assembled and added to `tally_metadata` under the keys `nuclide_ZZZAAAM_list` and `nuclide_isomer_list`, i.e. `tally_metadata['nuclide_ZZZAAAM_list']` and `tally_metadata['nuclide_isomer_list']`. These lists should be referenced to see what nuclide each of index `ic` refers to. The entries of the ZZZAAAM list are intergers calculated with the formula 10000*Z + 10*A + M, where M is the metastable state of the isomer (0 = ground state, 1 = 1st metastable/isomeric state, etc.). The entries of the isomer list are these same nuclides in the same order but written as plaintext strings, e.g. `'Al-28'` and `'Xe-133m1'`. The lists are ordered in the same order nuclides are encountered while parsing the output file. Thus, to sensibly access the yield of a specific nuclide, one must first find its index `ic` in one of the two metadata lists of ZZZAAAM values or isomer names and then use that to access `tally_data`. For example, to get the yield results of production of carbon-14 (C-14), one would use the following code: `ic = tally_metadata['nuclide_ZZZAAAM_list'].index(60140)` OR `ic = tally_metadata['nuclide_isomer_list'].index('C-14')` then `my_yield_values = tally_data[:,:,:,:,:,:,:,:,ic,:]` ''' ''' The old [T-Cross] mesh=r-z enclos=0 solution is written below: The solution implemented here uses `ir` to iterate `nr`, `iy` to iterate `nrsurf`, `iz` to iterate `nz`, and `ic` to iterate `nzsurf`. Since only `rsurf*z [iy,iz]` and `r*zsurf [ir,ic]` pairs exist, when one pair is being written, the other will be `[-1,-1]`, thus the lengths of these dimensions for the array are increased by an extra 1 to prevent an overlap in the data written. ''' tally_output_filepath = Path(tally_output_filepath) pickle_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle') picklexz_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle.xz') if prefer_reading_existing_pickle and (os.path.isfile(pickle_filepath) or os.path.isfile(picklexz_filepath)): import pickle if os.path.isfile(pickle_filepath): print('Reading found pickle file: ', pickle_filepath) with open(pickle_filepath, 'rb') as handle: tally_output = pickle.load(handle) else: print('Reading found pickle file: ', picklexz_filepath) import lzma with lzma.open(picklexz_filepath, 'rb') as handle: tally_output = pickle.load(handle) if autoplot_tally_output: if tally_output['tally_dataframe'] is not None: plot_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pdf') if not plot_filepath.is_file(): # only make plot if file doesn't already exist from inspect import signature max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7 tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1]) if tot_num_values > max_num_values_to_plot: print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values, ' elements), deemed too large for automatic plotting (default max of', max_num_values_to_plot, 'elements).') else: autoplot_tally_results(tally_output, output_filename=plot_filepath, plot_errorbars=calculate_absolute_errors, additional_save_extensions=['.png']) return tally_output # main toggled settings #calculate_absolute_errors = True construct_Pandas_frame_from_array = make_PandasDF #process_all_tally_out_files_in_directory = False save_pickle_files_of_output = save_output_pickle # save metadata, array, and Pandas frame in a pickled dictionary object if construct_Pandas_frame_from_array: import pandas as pd # Check if is _err or _dmp file (or normal value file) is_val_file = False is_err_file = False is_dmp_file = False if tally_output_filepath.stem[-4:] == '_err': is_err_file = True elif tally_output_filepath.stem[-4:] == '_dmp': is_dmp_file = True else: is_val_file = True if is_dmp_file: print('ERROR: The provided file is a "dump" output file. Use the function titled "parse_tally_dump_file" to process it instead.') return None if is_err_file: print('WARNING: Provided file contains just relative uncertainties.',str(tally_output_filepath)) potential_val_file = Path(tally_output_filepath.parent, tally_output_filepath.stem.replace('_err','') + tally_output_filepath.suffix) if potential_val_file.is_file(): print('\t Instead, both it and the file with tally values will be parsed.') potential_err_file = tally_output_filepath tally_output_filepath = potential_val_file is_val_file = True is_err_file = False else: print('\t The corresponding file with tally values could not be found, so only these uncertainties will be parsed.') # Split content of output file into header and content if in_debug_mode: print("\nSplitting output into header and content... ({:0.2f} seconds elapsed)".format(time.time() - start)) tally_header, tally_content = split_into_header_and_content(tally_output_filepath) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) # print(len(tally_content)) # Check if *_err file exists potential_err_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '_err' + tally_output_filepath.suffix) is_err_in_separate_file = potential_err_file.is_file() # for some tallies/meshes, uncertainties are stored in a separate identically-formatted file # Extract tally metadata if in_debug_mode: print("\nExtracting tally metadata... ({:0.2f} seconds elapsed)".format(time.time() - start)) tally_metadata = parse_tally_header(tally_header, tally_content) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) if in_debug_mode: pprint.pp(dict(tally_metadata)) # Check if tally_type is among those supported. unsupported_tally_types = ['[T-WWG]', '[T-WWBG]', '[T-Volume]', '[T-Userdefined]', '[T-Gshow]', '[T-Rshow]', '[T-3Dshow]', '[T-4Dtrack]', '[T-Dchain]', 'UNKNOWN'] if tally_metadata['tally_type'] in unsupported_tally_types: print('ERROR! tally type',tally_metadata['tally_type'],'is not supported by this function!') if tally_metadata['tally_type'] == '[T-Dchain]' or tally_output_filepath.suffix == '.act': print('Instead, the DCHAIN Tools module is used to process the DCHAIN output files with the same basename of the provided file.') dchain_tools_url = 'github.com/Lindt8/DCHAIN-Tools' dchain_tools_go_to_github_str = 'The DCHAIN Tools module ( '+dchain_tools_url+' ) is capable of parsing all DCHAIN-related output.' if tally_output_filepath.suffix != '.act': act_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.act') if not act_filepath.is_file(): # DCHAIN output is not present in directory print('Failed to find the main DCHAIN *.act output file:',act_filepath) print('Aborting this process...') return None try: from dchain_tools import process_dchain_simulation_output except: try: from PHITS_tools.dchain_tools import process_dchain_simulation_output except: print('Failed to import the DCHAIN Tools module; to parse DCHAIN output via PHITS Tools, please install DCHAIN Tools and configure it in your Python environment') return None simulation_folder_path = str(Path(tally_output_filepath.parent)) + '\\' simulation_basename = str(tally_output_filepath.stem) dchain_output = process_dchain_simulation_output(simulation_folder_path,simulation_basename,process_DCS_file=True) if save_output_pickle: import pickle path_to_pickle_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle') if in_debug_mode: print("\nWriting output to pickle file... ({:0.2f} seconds elapsed)".format(time.time() - start)) with open(path_to_pickle_file, 'wb') as handle: pickle.dump(dchain_output, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', path_to_pickle_file, '\n') if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) return dchain_output else: return None if (tally_metadata['tally_type'] == '[T-Yield]' or tally_metadata['tally_type'] == '[T-Track]') and tally_metadata['axis'] == 'dchain': dchain_tools_url = 'github.com/Lindt8/DCHAIN-Tools' print('This function does not support [T-Yield]/[T-Track] with setting "axis = dchain".') print('However, the DCHAIN Tools module (', dchain_tools_url, ') is capable of parsing all DCHAIN-related output.') return None # Initialize tally data array with zeros tally_data = initialize_tally_array(tally_metadata, include_abs_err=calculate_absolute_errors) # Parse tally data if is_val_file: err_mode = False else: # if is_err_file err_mode = True if in_debug_mode: print("\nParsing tally data... ({:0.2f} seconds elapsed)".format(time.time() - start)) if tally_metadata['tally_type']=='[T-Yield]' and tally_metadata['axis'] in ['chart','charge','mass']: # need to update metadata too tally_data, tally_metadata = parse_tally_content(tally_data, tally_metadata, tally_content, is_err_in_separate_file, err_mode=err_mode) else: tally_data = parse_tally_content(tally_data, tally_metadata, tally_content, is_err_in_separate_file, err_mode=err_mode) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) err_data_found = True if tally_metadata['axis_dimensions'] == 2 and tally_metadata['2D-type'] != 4: if is_err_file: err_data_found = False elif is_err_in_separate_file: err_tally_header, err_tally_content = split_into_header_and_content(potential_err_file) if in_debug_mode: print("\nParsing tally error... ({:0.2f} seconds elapsed)".format(time.time() - start)) if tally_metadata['tally_type'] == '[T-Yield]' and tally_metadata['axis'] in ['chart','charge','mass']: # need to update metadata too tally_data, tally_metadata = parse_tally_content(tally_data, tally_metadata, err_tally_content, is_err_in_separate_file,err_mode=True) else: tally_data = parse_tally_content(tally_data, tally_metadata, err_tally_content, is_err_in_separate_file, err_mode=True) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) else: print('WARNING: A separate file ending in "_err" containing uncertainties should exist but was not found.') err_data_found = False if calculate_absolute_errors: if err_data_found: if in_debug_mode: print("\nCalculating absolute errors... ({:0.2f} seconds elapsed)".format(time.time() - start)) tally_data = calculate_tally_absolute_errors(tally_data) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) elif is_err_file: print('WARNING: Absolute errors not calculated since the main tally values file was not found.') else: print('WARNING: Absolute errors not calculated since the _err file was not found.') # Generate Pandas dataframe of tally results if construct_Pandas_frame_from_array: if in_debug_mode: print("\nConstructing Pandas dataframe... ({:0.2f} seconds elapsed)".format(time.time() - start)) tally_Pandas_df = build_tally_Pandas_dataframe(tally_data, tally_metadata) if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) else: tally_Pandas_df = None phitsout_dict = {} include_input_echo = True if include_phitsout_in_metadata is None: include_input_echo = False include_phitsout_in_metadata = True if type(include_phitsout_in_metadata) == bool: # True or False if include_phitsout_in_metadata: # True provided, need to search for phits.out phitsout_file = Path(tally_output_filepath.parent, 'phits.out') if phitsout_file.exists(): phitsout_dict = parse_phitsout_file(phitsout_file, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo) elif isinstance(include_phitsout_in_metadata, dict): # dictionary provided for phitsout phitsout_dict = include_phitsout_in_metadata else: # assume path string/object pointing to phits.out is provided phitsout_dict = parse_phitsout_file(include_phitsout_in_metadata, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma) if phitsout_dict != {}: tally_metadata['phitsout'] = phitsout_dict tally_output = { 'tally_data': tally_data, 'tally_metadata': tally_metadata, 'tally_dataframe': tally_Pandas_df, } if save_output_pickle: import pickle if compress_pickle_with_lzma: import lzma compression_file_extension = '.xz' else: compression_file_extension = '' path_to_pickle_file = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pickle' + compression_file_extension) if in_debug_mode: print("\nWriting output to pickle file... ({:0.2f} seconds elapsed)".format(time.time() - start)) if compress_pickle_with_lzma: with lzma.open(path_to_pickle_file, 'wb') as handle: pickle.dump(tally_output, handle, protocol=4) # protocol=4 needed to pickle an object w/ a Pandas DF else: with open(path_to_pickle_file, 'wb') as handle: pickle.dump(tally_output, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', path_to_pickle_file, '\n') if in_debug_mode: print("\tComplete! ({:0.2f} seconds elapsed)".format(time.time() - start)) if autoplot_tally_output: if not make_PandasDF: print('Plotting via "autoplot_tally_output=True" requires also setting "make_PandasDF=True".') else: from inspect import signature max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7 tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1]) if tot_num_values > max_num_values_to_plot: print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values, ' elements), deemed too large for automatic plotting (default max of',max_num_values_to_plot,'elements).') else: plot_filepath = Path(tally_output_filepath.parent, tally_output_filepath.stem + '.pdf') autoplot_tally_results(tally_output, output_filename=plot_filepath, plot_errorbars=calculate_absolute_errors, additional_save_extensions=['.png']) return tally_output
def parse_tally_dump_file(path_to_dump_file, dump_data_number=None, dump_data_sequence=None, return_directional_info=False, use_degrees=False, max_entries_read=None, return_namedtuple_list=True, return_Pandas_dataframe=True, save_namedtuple_list=False, save_Pandas_dataframe=False, compress_pickles_with_lzma=True, prefer_reading_existing_pickle=False, split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0)
-
Description
Parses the dump file of a [T-Cross], [T-Product], or [T-Time] tally generated by PHITS, in ASCII or binary format.
Dependencies
import numpy as np
from collections import namedtuple
from scipy.io import FortranFile
import pandas as pd
(ifreturn_Pandas_dataframe = True
)
Inputs
(required)
path_to_dump_file
= string or Path object denoting the path to the dump tally output file to be parseddump_data_number
= integer number of data per row in dump file, binary if >0 and ASCII if <0. This should match the value followingdump=
in the tally creating the dump file. (D=None
) If not specified, thesearch_for_dump_parameters()
function will attempt to find it automatically.dump_data_sequence
= string or list of integers with the same number of entries asdump_data_number
, mapping each column in the dump file to their physical quantities. (D=None
) This should match the line following thedump=
line in the tally creating the dump file. See PHITS manual section "6.7.22 dump parameter" for further explanations of these values. If not specified, thesearch_for_dump_parameters()
function will attempt to find it automatically.
Inputs
(optional)
return_directional_info
= (D=False
) Boolean designating whether extra directional information should be calculated and returned; these include: radial distancer
from the origin in cm, radial distancerho
from the z-axis in cm, polar angletheta
between the direction vector and z-axis in radians [0,pi] (or degrees), and azimuthal anglephi
of the direction vector in radians [-pi,pi] (or degrees). Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file.use_degrees
= (D=False
) Boolean designating whether anglestheta
andphi
are returned in units of degrees. Default setting is to return angles in radians.max_entries_read
= (D=None
) integer number specifying the maximum number of entries/records of the dump file to be read. By default, all records in the dump file are read.return_namedtuple_list
= (D=True
) Boolean designating whetherdump_data_list
is returned.return_Pandas_dataframe
= (D=True
) Boolean designating whetherdump_data_frame
is returned.prefer_reading_existing_pickle
= (D=False
) A Boolean determining what this function does if the pickle file(s) this function seeks to generate already exist. IfFalse
(default behavior), this function will parse the PHITS dump files as usual and overwrite the existing pickle file(s). IfTrue
, this function will instead simply just read the existing found pickle file(s) and return the stored contents. Note that it can also be used for creating LZMA compressed/decompressed versions of the pickle file(s) if the file is set to be saved and the found existing file's compression (or lack thereof) contrasts with the current setting ofcompress_pickles_with_lzma
.compress_pickles_with_lzma
= (D=True
) Boolean designating whether the pickle files to be saved of the namedtuple list (ifsave_namedtuple_list = True
) and/or the Pandas DataFrame (ifsave_Pandas_dataframe = True
) will be compressed with LZMA compression (included within the baseline Python standard library); if so, the file extension of the saved pickle file(s) will be'.pickle.xz'
instead of just'.pickle'
. A *.pickle.xz file can then be opened (after importingpickle
andlzma
) as:with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)
. While compression will notably slow down the file-saving process, owing to the often large size of PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred.save_namedtuple_list
= (D=False
) Boolean designating whetherdump_data_list
is saved to a pickle file, which will by default be compressed with LZMA (built-in with Python; see thecompress_pickles_with_lzma
option). For complicated reasons†, the namedtuple list is converted to a NumPy recarray before being saved, which is nearly functionally identical to the list of namedtuples. When viewing individual entries, the field names are not printed (but can be accessed withdump_data_list.dtype.names
), but you can access the fields in exactly the same atribute access way as namedtuples (e.g.,dump_data_list[0].time
to view the time column value for the first entry in the list of events) as well as in a dictionary-like way (e.g.,dump_data_list[0]['time']
for that same time value).
†Pickle could not save the list of namedtuples without error; the dill library could but adding the extra external dependency was undesirable. Furthermore, the time performance of saving the NumPy recarray was substantially superior, approximately a factor of 2, relative to just using dill to save the list of namedtuples, also using the LZMA compression in both cases.save_Pandas_dataframe
= (D=False
) Boolean designating whetherdump_data_frame
is saved to a pickle file (via the Pandas.to_pickle()
method), which will by default be compressed with LZMA (built-in with Python; see thecompress_pickles_with_lzma
option). This pickle can then be accessed via thepandas.read_pickle(path_to_DF_pickle)
function.split_binary_dumps_over_X_GB
= (D=20
GB) an integer/float specifying the number of gigabytes over which a binary dump file will be split into chunks of this size for processing‡.merge_split_dump_handling
= (D=0
) For instances where binary dump files are to be split, described above forsplit_binary_dumps_over_X_GB
and the the note below‡, input one of the following numbers to determine what will happen to the intermediate pickle files created from each split chunk:0
= Merge the intermediate pickle files (viamerge_dump_file_pickles()
), then, if successful, delete them.1
= Do not merge the intermediate pickle files.2
= Merge the intermediate pickle files but do not delete them afterward.
Outputs
dump_data_list
= List of length equal to the number of records contained in the file. Each entry in the list is a namedtuple containing all of the physical information in the dump file for a given particle event, in the same order as specified indump_data_sequence
and using the same naming conventions for keys as described in the PHITS manual section "6.7.22 dump parameter" (kf
,x
,y
,z
,u
,v
,w
,e
,wt
,time
,c1
,c2
,c3
,sx
,sy
,sz
,name
,nocas
,nobch
,no
). Ifreturn_directional_info = True
,r
,rho
,theta
, andphi
are appended to the end of this namedtuple, in that order.dump_data_frame
= A Pandas dataframe created fromdump_data_list
with columns for each physical quantity and rows for each record included in the dump file.
Notes
‡For extremely large dump files, having sufficient available memory (RAM) can become an issue. This function has a built-in feature for automatically splitting binary dump files over a certain size (20 GB by default) into smaller chunks to be processed individually and then merged at the end. You can control when/if this splitting is triggered and how the resulting files are merged/handled with the
split_binary_dumps_over_X_GB
andmerge_split_dump_handling
inputs to this function. If running PHITS simulations yielding absolutely massive ASCII dump files, consider setting the tally'sdump
parameter to a positive number to write binary (much more space efficient) dump files instead, unless you have a good reason for needing them as ASCII. For more information on converting an existing ASCII dump file to a binary one, see the documentation within the following directory of your PHITS installation: /phits/utility/dump-a/Expand source code
def parse_tally_dump_file(path_to_dump_file, dump_data_number=None , dump_data_sequence=None, return_directional_info=False, use_degrees=False,max_entries_read=None,return_namedtuple_list=True, return_Pandas_dataframe=True, save_namedtuple_list=False, save_Pandas_dataframe=False, compress_pickles_with_lzma=True, prefer_reading_existing_pickle=False, split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0): ''' Description: Parses the dump file of a [T-Cross], [T-Product], or [T-Time] tally generated by PHITS, in ASCII or binary format. Dependencies: - `import numpy as np` - `from collections import namedtuple` - `from scipy.io import FortranFile` - `import pandas as pd` (if `return_Pandas_dataframe = True`) Inputs: (required) - **`path_to_dump_file`** = string or Path object denoting the path to the dump tally output file to be parsed - **`dump_data_number`** = integer number of data per row in dump file, binary if >0 and ASCII if <0. This should match the value following `dump=` in the tally creating the dump file. (D=`None`) If not specified, the `search_for_dump_parameters()` function will attempt to find it automatically. - **`dump_data_sequence`** = string or list of integers with the same number of entries as `dump_data_number`, mapping each column in the dump file to their physical quantities. (D=`None`) This should match the line following the `dump=` line in the tally creating the dump file. See PHITS manual section "6.7.22 dump parameter" for further explanations of these values. If not specified, the `search_for_dump_parameters()` function will attempt to find it automatically. Inputs: (optional) - **`return_directional_info`** = (D=`False`) Boolean designating whether extra directional information should be calculated and returned; these include: radial distance `r` from the origin in cm, radial distance `rho` from the z-axis in cm, polar angle `theta` between the direction vector and z-axis in radians [0,pi] (or degrees), and azimuthal angle `phi` of the direction vector in radians [-pi,pi] (or degrees). Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file. - **`use_degrees`** = (D=`False`) Boolean designating whether angles `theta` and `phi` are returned in units of degrees. Default setting is to return angles in radians. - **`max_entries_read`** = (D=`None`) integer number specifying the maximum number of entries/records of the dump file to be read. By default, all records in the dump file are read. - **`return_namedtuple_list`** = (D=`True`) Boolean designating whether `dump_data_list` is returned. - **`return_Pandas_dataframe`** = (D=`True`) Boolean designating whether `dump_data_frame` is returned. - **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the pickle file(s) this function seeks to generate already exist. If `False` (default behavior), this function will parse the PHITS dump files as usual and overwrite the existing pickle file(s). If `True`, this function will instead simply just read the existing found pickle file(s) and return the stored contents. Note that it can also be used for creating LZMA compressed/decompressed versions of the pickle file(s) if the file is set to be saved and the found existing file's compression (or lack thereof) contrasts with the current setting of `compress_pickles_with_lzma`. - **`compress_pickles_with_lzma`** = (D=`True`) Boolean designating whether the pickle files to be saved of the namedtuple list (if `save_namedtuple_list = True`) and/or the Pandas DataFrame (if `save_Pandas_dataframe = True`) will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included within the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the file extension of the saved pickle file(s) will be `'.pickle.xz'` instead of just `'.pickle'`. A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as: `with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)`. While compression will notably slow down the file-saving process, owing to the often large size of PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred. - **`save_namedtuple_list`** = (D=`False`) Boolean designating whether `dump_data_list` is saved to a pickle file, which will by default be compressed with LZMA (built-in with Python; see the `compress_pickles_with_lzma` option). For complicated reasons†, the namedtuple list is converted to a [NumPy recarray](https://numpy.org/doc/stable/reference/generated/numpy.recarray.html) before being saved, which is nearly functionally identical to the list of namedtuples. When viewing individual entries, the field names are not printed (but can be accessed with `dump_data_list.dtype.names`), but you can access the fields in exactly the same atribute access way as namedtuples (e.g., `dump_data_list[0].time` to view the time column value for the first entry in the list of events) as well as in a dictionary-like way (e.g., `dump_data_list[0]['time']` for that same time value). †_Pickle could not save the list of namedtuples without error; the dill library could but adding the extra external dependency was undesirable. Furthermore, the time performance of saving the NumPy recarray was substantially superior, approximately a factor of 2, relative to just using dill to save the list of namedtuples, also using the LZMA compression in both cases._ - **`save_Pandas_dataframe`** = (D=`False`) Boolean designating whether `dump_data_frame` is saved to a pickle file (via the Pandas `.to_pickle()` method), which will by default be compressed with LZMA (built-in with Python; see the `compress_pickles_with_lzma` option). This pickle can then be accessed via the [`pandas.read_pickle(path_to_DF_pickle)`](https://pandas.pydata.org/docs/reference/api/pandas.read_pickle.html) function. - **`split_binary_dumps_over_X_GB`** = (D=`20` GB) an integer/float specifying the number of gigabytes over which a _binary_ dump file will be split into chunks of this size for processing‡. - **`merge_split_dump_handling`** = (D=`0`) For instances where binary dump files are to be split, described above for `split_binary_dumps_over_X_GB` and the the note below‡, input one of the following numbers to determine what will happen to the intermediate pickle files created from each split chunk: - `0` = Merge the intermediate pickle files (via `merge_dump_file_pickles()`), then, if successful, delete them. - `1` = Do not merge the intermediate pickle files. - `2` = Merge the intermediate pickle files but do not delete them afterward. Outputs: - **`dump_data_list`** = List of length equal to the number of records contained in the file. Each entry in the list is a namedtuple containing all of the physical information in the dump file for a given particle event, in the same order as specified in `dump_data_sequence` and using the same naming conventions for keys as described in the PHITS manual section "6.7.22 dump parameter" (`kf`, `x`, `y`, `z`, `u`, `v`, `w`, `e`, `wt`, `time`, `c1`, `c2`, `c3`, `sx`, `sy`, `sz`, `name`, `nocas`, `nobch`, `no`). If `return_directional_info = True`, `r`, `rho`, `theta`, and `phi` are appended to the end of this namedtuple, in that order. - **`dump_data_frame`** = A Pandas dataframe created from `dump_data_list` with columns for each physical quantity and rows for each record included in the dump file. Notes: ‡For extremely large dump files, having sufficient available memory (RAM) can become an issue. This function has a built-in feature for automatically splitting _binary_ dump files over a certain size (20 GB by default) into smaller chunks to be processed individually and then merged at the end. You can control when/if this splitting is triggered and how the resulting files are merged/handled with the `split_binary_dumps_over_X_GB` and `merge_split_dump_handling` inputs to this function. If running PHITS simulations yielding absolutely massive ASCII dump files, consider setting the tally's `dump` parameter to a positive number to write _binary_ (much more space efficient) dump files instead, unless you have a good reason for needing them as ASCII. For more information on converting an existing ASCII dump file to a binary one, see the documentation within the following directory of your PHITS installation: /phits/utility/dump-a/ ''' # The below variables handle if/when absolutely massive binary dump files are forcibly split to not exceed RAM limits. # This only applies to dump files stored in binary. force_split_handling_of_massive_dumps = True # If False, this function will attempt to ingest the whole dump file, regardless of how big. max_bytes_to_handle_per_dump_part = split_binary_dumps_over_X_GB * 1073741824 # There are 1073741824 bytes in 1 GB. # max_bytes_to_handle_per_dump_part sets how much of the dump file is processed at a time, the default above is 20 GB. merge_split_parts_at_end = True # determines if split pickles will be merged at the end delete_merged_parts_at_end = True # if merging, determines if component parts will be deleted or not. if merge_split_dump_handling == 1: merge_split_parts_at_end = False if merge_split_dump_handling == 2: delete_merged_parts_at_end = False # Initializing this as False split_dump = False # This will be set to True if the dump file's size is found to exceed max_bytes_to_handle_per_dump_part from collections import namedtuple #from typing import NamedTuple from scipy.io import FortranFile if return_Pandas_dataframe or save_Pandas_dataframe: import pandas as pd if save_Pandas_dataframe or save_namedtuple_list or prefer_reading_existing_pickle: import pickle import lzma if not return_namedtuple_list and not return_Pandas_dataframe and not save_namedtuple_list and not save_Pandas_dataframe: raise ValueError('ERROR: All "return_namedtuple_list", "return_Pandas_dataframe", "save_namedtuple_list", and "save_Pandas_dataframe" are False. Enable at least one to use this function.') if dump_data_number == None or dump_data_sequence == None: dump_data_number, dump_data_sequence = search_for_dump_parameters(path_to_dump_file) if dump_data_number == None or dump_data_sequence == None: raise ValueError("Please manually specify 'dump_data_number' and 'dump_data_sequence'; these were not inputted and could not be automatically found from an origin tally standard output file.") #return None if isinstance(dump_data_sequence, str): dump_data_sequence = dump_data_sequence.split() dump_data_sequence = [int(i) for i in dump_data_sequence] dump_file_is_binary = True if (dump_data_number > 0) else False # if not binary, file will be ASCII data_values_per_line = abs(dump_data_number) if data_values_per_line != len(dump_data_sequence): raise ValueError('ERROR: Number of values in "dump_data_sequence" is not equal to "dump_data_number"') path_to_dump_file = Path(path_to_dump_file) dump_file_suffixes = path_to_dump_file.suffixes MPI_subdump_num_str = '' if len(dump_file_suffixes) > 1 and is_number(dump_file_suffixes[-1][1:]) and '_dmp' in path_to_dump_file.stem: # MPI dump found MPI_subdump_num_str = dump_file_suffixes[-1] # [1:] if compress_pickles_with_lzma: compression_file_extension = '.xz' else: compression_file_extension = '' pandas_df_pickle_filepath = Path(path_to_dump_file.parent, path_to_dump_file.stem + MPI_subdump_num_str + '_Pandas_df.pickle' + compression_file_extension) recarray_pickle_filepath = Path(path_to_dump_file.parent, path_to_dump_file.stem + MPI_subdump_num_str + '_namedtuple_list.pickle' + compression_file_extension) if prefer_reading_existing_pickle: df_exists = pandas_df_pickle_filepath.is_file() read_df_path = pandas_df_pickle_filepath if not df_exists: if compress_pickles_with_lzma: tmp_path = Path(pandas_df_pickle_filepath.parent, pandas_df_pickle_filepath.name.replace('.xz','')) else: tmp_path = Path(pandas_df_pickle_filepath.parent, pandas_df_pickle_filepath.name + '.xz') if tmp_path.is_file(): df_exists = True read_df_path = tmp_path ra_exists = recarray_pickle_filepath.is_file() read_ra_path = recarray_pickle_filepath read_ra_uses_lzma = compress_pickles_with_lzma if not ra_exists: if compress_pickles_with_lzma: tmp_path = Path(recarray_pickle_filepath.parent, recarray_pickle_filepath.name.replace('.xz', '')) else: tmp_path = Path(recarray_pickle_filepath.parent, recarray_pickle_filepath.name + '.xz') if tmp_path.is_file(): ra_exists = True read_ra_path = tmp_path read_ra_uses_lzma = not compress_pickles_with_lzma # Perform any desired compression/decompression of pickles if ra_exists: if compress_pickles_with_lzma and read_ra_path.name[-3:]!='.xz' and save_namedtuple_list: # save a compressed version with open(read_ra_path, 'rb') as file: records_list = pickle.load(file) with lzma.open(recarray_pickle_filepath, 'wb') as handle: pickle.dump(records_list, handle, protocol=pickle.HIGHEST_PROTOCOL) if not compress_pickles_with_lzma and read_ra_path.name[-3:]=='.xz' and save_namedtuple_list: # save an uncompressed version with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file) with open(recarray_pickle_filepath, 'wb') as handle: pickle.dump(records_list, handle, protocol=pickle.HIGHEST_PROTOCOL) save_namedtuple_list = False if df_exists: if (compress_pickles_with_lzma and read_df_path.name[-3:]!='.xz' and save_Pandas_dataframe) or \ (not compress_pickles_with_lzma and read_df_path.name[-3:]=='.xz' and save_Pandas_dataframe): # save a compressed or uncompressed version records_df = pd.read_pickle(read_df_path) records_df.to_pickle(pandas_df_pickle_filepath) save_Pandas_dataframe = False # Determine if function can end here or if the dump file needs to be read and reprocessed if (ra_exists and return_namedtuple_list and not save_namedtuple_list) and (df_exists and return_Pandas_dataframe and not save_Pandas_dataframe): if read_ra_uses_lzma: with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file) else: with open(read_ra_path, 'rb') as file: records_list = pickle.load(file) records_df = pd.read_pickle(read_df_path) return records_list, records_df elif (ra_exists and return_namedtuple_list and not save_namedtuple_list): if read_ra_uses_lzma: with lzma.open(read_ra_path, 'rb') as file: records_list = pickle.load(file) else: with open(read_ra_path, 'rb') as file: records_list = pickle.load(file) return records_list elif (df_exists and return_Pandas_dataframe and not save_Pandas_dataframe): records_df = pd.read_pickle(read_df_path) return records_df elif not return_namedtuple_list and not return_Pandas_dataframe and not save_namedtuple_list and not save_Pandas_dataframe: # nothing more to be done in the function, everything to be saved already exists return None # Generate NamedTuple for storing record information # See PHITS manual section "6.7.22 dump parameter" for descriptions of these values dump_quantities = ['kf', 'x', 'y', 'z', 'u', 'v', 'w', 'e', 'wt', 'time', 'c1', 'c2', 'c3', 'sx', 'sy', 'sz', 'name', 'nocas', 'nobch', 'no'] dump_col_dtypes = [int, float, float, float, float, float, float, float, float, float, int, int, int, float, float, float, int, int, int, int] ordered_record_entries_list = [dump_quantities[i - 1] for i in dump_data_sequence] rawRecord = namedtuple('rawRecord', ordered_record_entries_list) if return_directional_info: ordered_record_entries_list += ['r', 'rho', 'theta', 'phi'] angle_units_mult = 1 if use_degrees: angle_units_mult = 180 / np.pi Record = namedtuple('Record', ordered_record_entries_list) records_list = [] if dump_file_is_binary: # Read binary dump file; extract each record (particle) file_size_bytes = os.path.getsize(path_to_dump_file) record_size_bytes = (data_values_per_line + 1) * 8 # each record has 8 bytes per data value plus an 8-byte record end num_records = int(file_size_bytes / record_size_bytes) if max_entries_read != None: if max_entries_read < num_records: num_records = max_entries_read bytes_to_read = num_records*record_size_bytes current_record_count = 0 if force_split_handling_of_massive_dumps and bytes_to_read > max_bytes_to_handle_per_dump_part: #num_total_parts = np.ceil(bytes_to_read/max_bytes_to_handle_per_dump_part) max_records_per_part = np.floor(max_bytes_to_handle_per_dump_part/record_size_bytes) num_total_parts = np.ceil(num_records/max_records_per_part) remaining_total_records_to_read = num_records remaining_records_to_read_in_this_part = max_records_per_part num_current_part = 1 current_bytes_read = 0 split_dump = True paths_to_split_recarrays = [] paths_to_split_pandas_dfs = [] paths_to_split_fake_dump_files = [] if not save_namedtuple_list and not save_Pandas_dataframe: raise ValueError('ERROR: "save_namedtuple_list" and "save_Pandas_dataframe" are False. Enable at least one to parse a dump file to be split.') print('WARNING: Dump file will be split and then remerged to not exceed memory limits.') #if return_namedtuple_list or return_Pandas_dataframe: # print('WARNING: When splitting a huge dump, "return_namedtuple_list" and "return_Pandas_dataframe" are set to False.') # return_namedtuple_list, return_Pandas_dataframe = False, False with FortranFile(path_to_dump_file, 'r') as f: while current_record_count < num_records: current_record_count += 1 remaining_records_to_read_in_this_part -= 1 raw_values = f.read_reals(float) rawrecord = rawRecord(*raw_values) if return_directional_info: # calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis) r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2) rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2) dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w] theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult record = Record(*raw_values, r, rho, theta, phi) records_list.append(record) else: records_list.append(rawrecord) if remaining_records_to_read_in_this_part<=0: # time to write dump part paths_to_split_fake_dump_files.append(Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part, num_total_parts) + '.out')) num_records = num_records - current_record_count remaining_total_records_to_read -= current_record_count remaining_records_to_read_in_this_part = min(remaining_total_records_to_read,max_records_per_part) current_record_count = 0 if save_namedtuple_list: pickle_path = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part,num_total_parts) + '_namedtuple_list.pickle' + compression_file_extension) paths_to_split_recarrays.append(pickle_path) record_type = [(ordered_record_entries_list[i], dump_col_dtypes[i]) for i in range(len(ordered_record_entries_list))] records_np_array = np.array(records_list, dtype=record_type) records_np_array = records_np_array.view(np.recarray) if compress_pickles_with_lzma: with lzma.open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) else: with open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) del records_np_array if save_Pandas_dataframe: records_df = pd.DataFrame(records_list, columns=Record._fields) path_to_dump_file = Path(path_to_dump_file) pickle_path = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_part-{:g}of{:g}'.format(num_current_part, num_total_parts) + '_Pandas_df.pickle' + compression_file_extension) paths_to_split_pandas_dfs.append(pickle_path) records_df.to_pickle(pickle_path) del records_df num_current_part += 1 records_list = [] else: # Normal flow, if not split dump if return_directional_info: with FortranFile(path_to_dump_file, 'r') as f: while current_record_count < num_records: current_record_count += 1 raw_values = f.read_reals(float) rawrecord = rawRecord(*raw_values) # calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis) r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2) rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2) dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w] theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult record = Record(*raw_values, r, rho, theta, phi) records_list.append(record) else: # just return data in dump file with FortranFile(path_to_dump_file, 'r') as f: while current_record_count < num_records: current_record_count += 1 raw_values = f.read_reals(float) record = Record(*raw_values) records_list.append(record) else: # file is ASCII if max_entries_read == None: max_entries_read = np.inf if return_directional_info: with open(path_to_dump_file, 'r') as f: current_record_count = 0 for line in f: current_record_count += 1 if current_record_count > max_entries_read: break line_str_values = line.replace('D', 'E').split() raw_values = [float(i) for i in line_str_values] rawrecord = rawRecord(*raw_values) # calculate r, rho, theta (w.r.t. z-axis), and phi (w.r.t. x axis) r = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2 + rawrecord.z ** 2) rho = np.sqrt(rawrecord.x ** 2 + rawrecord.y ** 2) dir_vector = [rawrecord.u, rawrecord.v, rawrecord.w] theta = np.arccos(np.clip(np.dot(dir_vector, [0, 0, 1]), -1.0, 1.0)) * angle_units_mult phi = np.arctan2(rawrecord.y, rawrecord.x) * angle_units_mult record = Record(*raw_values, r, rho, theta, phi) records_list.append(record) else: # just return data in dump file with open(path_to_dump_file, 'r') as f: current_record_count = 0 for line in f: current_record_count += 1 if current_record_count > max_entries_read: break line_str_values = line.replace('D', 'E').split() raw_values = [float(i) for i in line_str_values] record = Record(*raw_values) records_list.append(record) #print(record) if split_dump: # Now attempt to merge dumps back together if merge_split_parts_at_end: merged_dump_base_filepath = Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str) merge_dump_file_pickles(paths_to_split_fake_dump_files, merged_dump_base_filepath=merged_dump_base_filepath, delete_pre_merge_pickles=delete_merged_parts_at_end, compress_pickles_with_lzma=compress_pickles_with_lzma) if return_namedtuple_list: path_to_recarray_pickle_file = recarray_pickle_filepath if compress_pickles_with_lzma: with lzma.open(path_to_recarray_pickle_file, 'rb') as file: records_list = pickle.load(file) else: with open(path_to_recarray_pickle_file, 'rb') as file: records_list = pickle.load(file) if return_Pandas_dataframe: records_df = pd.read_pickle(Path(path_to_dump_file.parent,path_to_dump_file.stem + MPI_subdump_num_str + '_Pandas_df.pickle')) else: # Normal (non-split) dump saving if save_namedtuple_list: path_to_dump_file = Path(path_to_dump_file) pickle_path = recarray_pickle_filepath #num_records = len(records_list) record_type = [(ordered_record_entries_list[i], dump_col_dtypes[i]) for i in range(len(ordered_record_entries_list))] records_np_array = np.array(records_list, dtype=record_type) records_np_array = records_np_array.view(np.recarray) if compress_pickles_with_lzma: with lzma.open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) else: with open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', pickle_path, '\n') del records_np_array # In the event this is huge in memory, best to delete it here before the Pandas DF is made if return_Pandas_dataframe or save_Pandas_dataframe: # Make Pandas dataframe from list of records records_df = pd.DataFrame(records_list, columns=Record._fields) if save_Pandas_dataframe: pickle_path = pandas_df_pickle_filepath records_df.to_pickle(pandas_df_pickle_filepath) #with open(pickle_path, 'wb') as handle: # pickle.dump(records_df, handle, protocol=pickle.HIGHEST_PROTOCOL) # print('Pickle file written:', pickle_path, '\n') print('Pickle file written:', pandas_df_pickle_filepath, '\n') if return_namedtuple_list and return_Pandas_dataframe: return records_list, records_df elif return_namedtuple_list: return records_list elif return_Pandas_dataframe: return records_df else: return None
def parse_all_tally_output_in_dir(tally_output_dirpath, output_file_suffix=None, output_file_prefix='', output_file_required_string='', include_subdirectories=False, return_tally_output=False, make_PandasDF=True, calculate_absolute_errors=True, save_output_pickle=True, include_phitsout_in_metadata=False, prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False, merge_tally_outputs=False, save_pickle_of_merged_tally_outputs=None, include_dump_files=False, dump_data_number=None, dump_data_sequence=None, dump_return_directional_info=False, dump_use_degrees=False, dump_max_entries_read=None, dump_save_namedtuple_list=True, dump_save_Pandas_dataframe=True, dump_merge_MPI_subdumps=True, dump_delete_MPI_subdumps_post_merge=True, split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0, autoplot_tally_output=False, autoplot_all_tally_output_in_dir=False)
-
Description
Parse all standard PHITS tally output files in a directory [DIRECTORY mode], returning either a list of dictionaries containing tally metadata and an array of values from each tally output (and optionally this data inside of a Pandas dataframe too) or a list of filepaths to pickle files containing these dictionaries, as created with the
parse_tally_output_file()
function. Alternatively, if provided a PHITS input file or its produced "phits.out" file instead of a directory [INPUT_FILE mode], this function will instead scan the provided file for all tally output that should have been produced by the PHITS run and parse all of those files instead.This function allows selective processing of files in the directory by specification of strings which must appear at the start, end, and/or anywhere within each filename. Even if a file satisfies all of these naming criteria, the function will also use
determine_PHITS_output_file_type()
to check if it is a valid tally output file or phits.out file (meaning, it will skip files like batch.out). It will also skip over "_err" uncertainty files as these are automatically found by theparse_tally_output_file()
function after it processes that tally's main output file.This function will mainly process standard tally output files, but it can optionally process tally "dump" files too, though it can only save the dump outputs to its pickle files and not return the (quite large) dump data objects. The filenames of saved dump data will not be included in the returned list.
Dependencies
import numpy as np
import pandas as pd
(ifmake_PandasDF = True
)import seaborn as sns
(ifautoplot_tally_output=True
orautoplot_all_tally_output_in_dir=True
)from munch import Munch
(will still run if package not found)from collections import namedtuple
(if processing tally dump files)from scipy.io import FortranFile
(if processing tally dump files)
Inputs
(required)
tally_output_dirpath
= Path (string orpathlib.Path()
object) to the tally output directory to be searched and parsed or to a PHITS input file or its "phits.out" file (file(6)
in the [Parameters] section of a PHITS input file) whose produced outputs are to be parsed. If provided a directory, this function will operate in [DIRECTORY mode]; otherwise, if provided a PHITS input or phits.out file, it will operate in [INPUT_FILE mode]. Both of these modes are nearly identical in their function, but their differences are highlighted here in this function's documentation.
Inputs
(optional)
output_file_suffix
= ([DIRECTORY mode] D='.out'
/ [INPUT_FILE mode] D=''
) A string specifying what characters processed filenames (including the file extension) must end in to be included. This condition is not enforced if set to an empty string''
. Note that if the filename contains "_dmp" and has a final extension of the form ".###" (where "###" is an arbitrarily long numeric string of digits 0-9), as is the case for dump files from PHITS ran in MPI parallelization, this ".###" final extension is pretended not to exist when enforcing thisoutput_file_suffix
parameter, allowing these dump files to be found and processed the same regardless of whether MPI was used or not when running PHITS.output_file_prefix
= (D=''
) A string specifying what characters processed filenames (including the file extension) must begin with to be included. This condition is not enforced if set to an empty string''
.output_file_required_string
= (D=''
) A string which must be present anywhere within processed filenames (including the file extension) to be included. This condition is not enforced if set to an empty string''
.include_subdirectories
= (D=False
) A Boolean determining whether this function searches and processes all included tally output files in this directory AND deeper subdirectories if set toTrue
or only the files directly within the provided directorytally_output_dirpath
if set toFalse
return_tally_output
= (D=False
) A Boolean determining whether this function returns (ifTrue
) a list oftally_output
dictionaries or (ifFalse
) just a list of filepaths to the pickle files containing these dictionariesinclude_dump_files
= (D=False
) A Boolean determining whether dump files will be processed too or skipped. Settings to be applied to all encountered dump files can be specified per the optional inputs detailed below which are simply passed to theparse_tally_dump_file()
function. Note that parametersreturn_namedtuple_list
andreturn_Pandas_dataframe
will always beFalse
when dump files are processed in a directory with this function; instead,save_namedtuple_list
andsave_Pandas_dataframe
are by default set toTrue
when parsing dump files in a directory with this function. (Be warned, if the dump file is large, the produced files from parsing them will be too.)prefer_reading_existing_pickle
= (D=False
) A Boolean determining what this function does if the pickle file this function seeks to generate already exists. IfFalse
(default behavior), this function will parse the PHITS output files as usual and overwrite the existing pickle file. IfTrue
, this function will instead simply just read the existing found pickle file, extracting its storedtally_output
contents (and, if for dump file pickles, they'll simply be skipped and not reprocessed).merge_tally_outputs
= (D=False
) A Boolean determining whether this function will merge all of the producedtally_output
dictionaries (and "phitsout" dictionaries ifinclude_phitsout_in_metadata=True
) into a single larger dictionary object. The output filenames are used as the keys in this dictionary. In [DIRECTORY mode], this is the relative filepath (from the provided dirctory) of each output file, converted to a string; in [INPUT_FILE mode], this is the string provided to each tally'sfile
parameter in the PHITS input. This dictionary will respect thesave_pickle_of_merged_tally_outputs
andcompress_pickle_with_lzma
input arguments for saving and compression, with path/name "tally_output_dirpath
/ALL_TALLY_OUTPUTS.pickle[.xz]" by default. If in [INPUT_FILE mode], the default filename of this file will be prefixed by either the PHITS input filename or "phitsout_", depending on which type of file is provided to this function. If a string is provided instead of a Boolean, this setting will be set toTrue
and the string will be used as the saved pickle file's filename, with the file name and path as "tally_output_dirpath
/merge_tally_outputs
.pickle[.xz]". Ifreturn_tally_output
is set toTrue
, this single dictionary will be returned instead of a list of producedtally_output
dictionaries.save_pickle_of_merged_tally_outputs
= (D=None
; requiresmerge_tally_outputs=True
) Boolean denoting whether the merged dictionary containing all PHITS tally output created whenmerge_tally_outputs=True
should be saved as a pickle file, allowing independent control of saving this pickle file versus those of the constituent tallies. If left at its default value ofNone
, this option will be set to match thesave_output_pickle
(D=True
) option. If set toTrue
andreturn_tally_output=False
andsave_output_pickle=False
, this function will return the path to this saved pickle file.autoplot_all_tally_output_in_dir
= (D=False
; requiresmake_PandasDF=True
) Boolean denoting, for all tally outputs included in this function's call, whether the tallies' outputs will be automatically plotted and saved to a single PDF, with path/name "tally_output_dirpath
/ALL_TALLY_OUTPUTS_PLOTTED.pdf" by default, using theautoplot_tally_results()
function. If a string is provided instead of a Boolean, this setting will be set toTrue
and the string will be used as the plot PDF's filename, with the file name and path as "tally_output_dirpath
/autoplot_all_tally_output_in_dir
.pdf". If in [INPUT_FILE mode], the default filename of this PDF will be prefixed by either the PHITS input filename or "phitsout_", depending on which is provided to this function.
Inputs
(optional, the same as in and directly passed to the
parse_tally_output_file()
function)make_PandasDF
= (D=True
) A Boolean determining whether a Pandas dataframe of the tally data array will be madecalculate_absolute_errors
= (D=True
) A Boolean determining whether the absolute uncertainty of each tally output value is to be calculated (simply as the product of the value and relative error); ifFalse
, the final dimension oftally_data
,ierr
, will be of length-2 rather than length-3save_output_pickle
= (D=True
) A Boolean determining whether thetally_output
dictionary object is saved as a pickle file; ifTrue
, the file will be saved with the same path and name as the provided PHITS tally output file but with the .pickle extension.include_phitsout_in_metadata
= (D=False
) A Boolean determining whether the "phits.out" file (file(6)
in the [Parameters] section of a PHITS input file) in the same directory astally_output_dirpath
, if found, should be processed viaparse_phitsout_file()
and have its informational dictionary about the PHITS run added to the'tally_metadata'
dictionary under the key'phitsout'
.- This only works if a single "phits.out"-type file is found; if multiple are encountered (as determined by
determine_PHITS_output_file_type()
), they will still be processed withparse_phitsout_file()
(ifsave_output_pickle=True
ormerge_tally_outputs=True
) but not added to any tally output metadata dictionaries. In this circumstance and ifmerge_tally_outputs=True
, the multiple phits.out dictionaries will be added to the merged dictionary. - If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided
dictionary will be taken as that to be added as the
'phitsout'
key in the'tally_metadata'
dictionary. - Otherwise, if a string or
pathlib.Path()
object is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its informational dictionary added to the'tally_metadata'
dictionary. - Note that this is automatically set to
True
if in [INPUT_FILE mode] (iftally_output_dirpath
is a file, rather than a directory, and deemed to be a PHITS input file or "phits.out"-type file, as determined bydetermine_PHITS_output_file_type()
); to override this automatic behavior (i.e., to behave as if set toFalse
even when provided a PHITS input or phits.out file), setinclude_phitsout_in_metadata = {}
. - Setting
include_phitsout_in_metadata = None
functions the same asinclude_phitsout_in_metadata = True
but setsinclude_input_echo = False
in theparse_phitsout_file()
call.
- This only works if a single "phits.out"-type file is found; if multiple are encountered (as determined by
compress_pickle_with_lzma
= (D=False
; requiressave_output_pickle=True
) Boolean designating whether the pickle file to be saved will be compressed with LZMA compression (included in the baseline Python standard library); if so, the pickle file's extension will be'.pickle.xz'
instead of just'.pickle'
. A *.pickle.xz file can then be opened (after importingpickle
andlzma
) as:
with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)
.
For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for output from massive tallies.autoplot_tally_output
= (D=False
; requiresmake_PandasDF=True
) Boolean denoting whether, for each tally, the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path astally_output_filepath
but ending in.pdf
/.png
) using theautoplot_tally_results()
function.
Inputs
(optional, the same as in and directly passed to the
parse_tally_dump_file()
function)dump_data_number
= (D=None
) integer number of data per row in dump file, binary if >0 and ASCII if <0. This should match the value followingdump=
in the tally creating the dump file. If not specified, the search_for_dump_parameters() function will attempt to find it automatically.dump_data_sequence
= (D=None
) string or list of integers with the same number of entries asdump_data_number
, mapping each column in the dump file to their physical quantities. This should match the line following thedump=
line in the tally creating the dump file. See PHITS manual section "6.7.22 dump parameter" for further explanations of these values. If not specified, the search_for_dump_parameters() function will attempt to find it automatically.dump_return_directional_info
= (D=False
) Boolean designating whether extra directional information should be calculated and returned; these include: radial distancer
from the origin in cm, radial distancerho
from the z-axis in cm, polar angletheta
between the direction vector and z-axis in radians [0,pi] (or degrees), and azimuthal anglephi
of the direction vector in radians [-pi,pi] (or degrees). Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file.dump_use_degrees
= (D=False
) Boolean designating whether anglestheta
andphi
are returned in units of degrees. Default setting is to return angles in radians.dump_max_entries_read
= (D=None
) integer number specifying the maximum number of entries/records of the dump file to be read. By default, all records in the dump file are read.dump_save_namedtuple_list
= (D=True
) Boolean designating whetherdump_data_list
is saved to a pickle file (for complicated reasons, the namedtuple list is converted to a NumPy recarray before being saved, which is nearly functionally identical to the list of namedtuples. For more info and the full explanation, see the documentation for theparse_tally_dump_file()
function'ssave_namedtuple_list
argument, which is what this argument is passed to anyways).dump_save_Pandas_dataframe
= (D=True
) Boolean designating whetherdump_data_frame
is saved to a pickle file (via Pandas .to_pickle()).split_binary_dumps_over_X_GB
= (D=20
GB) an integer/float specifying the number of gigabytes over which a binary dump file will be split into chunks of this size for processing.merge_split_dump_handling
= (D=0
) For instances where binary dump files are to be split, as determined by thesplit_binary_dumps_over_X_GB
setting, input one of the following numbers to determine what will happen to the intermediate pickle files created from each split chunk:0
= Merge the intermediate pickle files (viamerge_dump_file_pickles()
), then, if successful, delete them.1
= Do not merge the intermediate pickle files.2
= Merge the intermediate pickle files but do not delete them afterward.
dump_merge_MPI_subdumps
= (D=True
) Boolean designating whether the pickled namedtuple lists and/or Pandas DataFrames for all "sub-dumps" from an MPI run of PHITS should be merged into single namedtuple list and/or Pandas DataFrame pickle file(s) (withmerge_dump_file_pickles()
). When a dump file is written in an MPI execution of PHITS, it is split into N "sub dump" files, one per MPI process, and given an extra final extension of the form ".###" (where "###" is an arbitrarily long numeric string of digits 0-9 designating the MPI process). This option is only active when such files are encountered.dump_delete_MPI_subdumps_post_merge
= (D=True
) Requiresdump_merge_MPI_subdumps=True
, Boolean designating whether the numerous "sub dump" pickle files merged whendump_merge_MPI_subdumps=True
(and MPI dump files are found) are deleted after the merged version combining all of them is saved.
Output
tally_output_list
= a list oftally_output
dictionary objects (ifreturn_tally_output=True
) with the below keys and values / a list of file paths to pickle files containingtally_output
dictionary objects:'tally_data'
= a 10-dimensional NumPy array containing all tally results, explained in more detail below'tally_metadata'
= a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units'tally_dataframe'
= (optionally included if settingmake_PandasDF = True
) a Pandas dataframe version oftally_data
- This list of
tally_output
dictionary objects will be returned as a larger merged dictionary/Munch† object, keyed using the tally output file names, instead ifmerge_tally_outputs=True
andreturn_tally_output=True
.
Ifmerge_tally_outputs=True
,return_tally_output=False
,save_output_pickle=False
, andsave_pickle_of_merged_tally_outputs=True
, the path to this saved merged tally output dictionary pickle file will be returned instead.
†If you have the Munch package installed, the
tally_metadata
dictionary and merged tally output dictionary will instead be Munch objects, which are identical to a dictionary but additionally allows attribute-style access (e.g.,tally_metadata.mesh
instead of onlytally_metadata['mesh']
). If you do not have Munch, then these will just be standard dictionary objects. If, for whatever reason, you have Munch installed but do not wish for these to be Munch objects, then in the first line of code for both this and theparse_tally_header()
function, setprefer_to_munch_merged_dict = False
andprefer_to_munch_meta_dict = False
, respectively.Expand source code
def parse_all_tally_output_in_dir(tally_output_dirpath, output_file_suffix = None, output_file_prefix = '', output_file_required_string='', include_subdirectories=False, return_tally_output=False, make_PandasDF=True, calculate_absolute_errors=True, save_output_pickle=True, include_phitsout_in_metadata=False, prefer_reading_existing_pickle=False, compress_pickle_with_lzma=False, merge_tally_outputs=False, save_pickle_of_merged_tally_outputs=None, include_dump_files=False, dump_data_number=None , dump_data_sequence=None, dump_return_directional_info=False, dump_use_degrees=False, dump_max_entries_read=None, dump_save_namedtuple_list=True, dump_save_Pandas_dataframe=True, dump_merge_MPI_subdumps=True, dump_delete_MPI_subdumps_post_merge=True, split_binary_dumps_over_X_GB=20, merge_split_dump_handling=0, autoplot_tally_output=False, autoplot_all_tally_output_in_dir=False ): ''' Description: Parse all standard PHITS tally output files in a directory *[DIRECTORY mode]*, returning either a list of dictionaries containing tally metadata and an array of values from each tally output (and optionally this data inside of a Pandas dataframe too) or a list of filepaths to pickle files containing these dictionaries, as created with the `parse_tally_output_file()` function. Alternatively, if provided a PHITS input file or its produced "phits.out" file instead of a directory *[INPUT_FILE mode]*, this function will instead scan the provided file for all tally output that should have been produced by the PHITS run and parse all of those files instead. This function allows selective processing of files in the directory by specification of strings which must appear at the start, end, and/or anywhere within each filename. Even if a file satisfies all of these naming criteria, the function will also use `determine_PHITS_output_file_type()` to check if it is a valid tally output file or phits.out file (meaning, it will skip files like batch.out). It will also skip over "_err" uncertainty files as these are automatically found by the `parse_tally_output_file()` function after it processes that tally's main output file. This function will mainly process standard tally output files, but it can optionally process tally "dump" files too, though it can only save the dump outputs to its pickle files and not return the (quite large) dump data objects. The filenames of saved dump data will not be included in the returned list. Dependencies: - `import numpy as np` - `import pandas as pd` (if `make_PandasDF = True`) - `import seaborn as sns` (if `autoplot_tally_output=True` or `autoplot_all_tally_output_in_dir=True`) - `from munch import Munch` (will still run if package not found) - `from collections import namedtuple` (if processing tally dump files) - `from scipy.io import FortranFile` (if processing tally dump files) Inputs: (required) - **`tally_output_dirpath`** = Path (string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object) to the tally output directory to be searched and parsed or to a PHITS input file or its "phits.out" file (`file(6)` in the [Parameters] section of a PHITS input file) whose produced outputs are to be parsed. If provided a directory, this function will operate in *[DIRECTORY mode]*; otherwise, if provided a PHITS input or phits.out file, it will operate in *[INPUT_FILE mode]*. Both of these modes are nearly identical in their function, but their differences are highlighted here in this function's documentation. Inputs: (optional) - **`output_file_suffix`** = (*[DIRECTORY mode]* D=`'.out'` / *[INPUT_FILE mode]* D=`''`) A string specifying what characters processed filenames (including the file extension) must end in to be included. This condition is not enforced if set to an empty string `''`. Note that if the filename contains "_dmp" and has a final extension of the form ".###" (where "###" is an arbitrarily long numeric string of digits 0-9), as is the case for dump files from PHITS ran in MPI parallelization, this ".###" final extension is pretended not to exist when enforcing this `output_file_suffix` parameter, allowing these dump files to be found and processed the same regardless of whether MPI was used or not when running PHITS. - **`output_file_prefix`** = (D=`''`) A string specifying what characters processed filenames (including the file extension) must begin with to be included. This condition is not enforced if set to an empty string `''`. - **`output_file_required_string`** = (D=`''`) A string which must be present anywhere within processed filenames (including the file extension) to be included. This condition is not enforced if set to an empty string `''`. - **`include_subdirectories`** = (D=`False`) A Boolean determining whether this function searches and processes all included tally output files in this directory AND deeper subdirectories if set to `True` or only the files directly within the provided directory `tally_output_dirpath` if set to `False` - **`return_tally_output`** = (D=`False`) A Boolean determining whether this function returns (if `True`) a list of `tally_output` dictionaries or (if `False`) just a list of filepaths to the pickle files containing these dictionaries - **`include_dump_files`** = (D=`False`) A Boolean determining whether dump files will be processed too or skipped. Settings to be applied to all encountered dump files can be specified per the optional inputs detailed below which are simply passed to the `parse_tally_dump_file()` function. Note that parameters `return_namedtuple_list` and `return_Pandas_dataframe` will always be `False` when dump files are processed in a directory with this function; instead, `save_namedtuple_list` and `save_Pandas_dataframe` are by default set to `True` when parsing dump files in a directory with this function. (Be warned, if the dump file is large, the produced files from parsing them will be too.) - **`prefer_reading_existing_pickle`** = (D=`False`) A Boolean determining what this function does if the pickle file this function seeks to generate already exists. If `False` (default behavior), this function will parse the PHITS output files as usual and overwrite the existing pickle file. If `True`, this function will instead simply just read the existing found pickle file, extracting its stored `tally_output` contents (and, if for dump file pickles, they'll simply be skipped and not reprocessed). - **`merge_tally_outputs`** = (D=`False`) A Boolean determining whether this function will merge all of the produced `tally_output` dictionaries (and "phitsout" dictionaries if `include_phitsout_in_metadata=True`) into a single larger dictionary object. The output filenames are used as the keys in this dictionary. In *[DIRECTORY mode]*, this is the relative filepath (from the provided dirctory) of each output file, converted to a string; in *[INPUT_FILE mode]*, this is the string provided to each tally's `file` parameter in the PHITS input. This dictionary will respect the `save_pickle_of_merged_tally_outputs` and `compress_pickle_with_lzma` input arguments for saving and compression, with path/name "`tally_output_dirpath`/ALL_TALLY_OUTPUTS.pickle[.xz]" by default. If in *[INPUT_FILE mode]*, the default filename of this file will be prefixed by either the PHITS input filename or "phitsout_", depending on which type of file is provided to this function. If a string is provided instead of a Boolean, this setting will be set to `True` and the string will be used as the saved pickle file's filename, with the file name and path as "`tally_output_dirpath`/`merge_tally_outputs`.pickle[.xz]". If `return_tally_output` is set to `True`, this single dictionary will be returned instead of a list of produced `tally_output` dictionaries. - **`save_pickle_of_merged_tally_outputs`** = (D=`None`; requires `merge_tally_outputs=True`) Boolean denoting whether the merged dictionary containing all PHITS tally output created when `merge_tally_outputs=True` should be saved as a pickle file, allowing independent control of saving this pickle file versus those of the constituent tallies. If left at its default value of `None`, this option will be set to match the `save_output_pickle` (D=`True`) option. If set to `True` and `return_tally_output=False` and `save_output_pickle=False`, this function will return the path to this saved pickle file. - **`autoplot_all_tally_output_in_dir`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting, for all tally outputs included in this function's call, whether the tallies' outputs will be automatically plotted and saved to a single PDF, with path/name "`tally_output_dirpath`/ALL_TALLY_OUTPUTS_PLOTTED.pdf" by default, using the `autoplot_tally_results()` function. If a string is provided instead of a Boolean, this setting will be set to `True` and the string will be used as the plot PDF's filename, with the file name and path as "`tally_output_dirpath`/`autoplot_all_tally_output_in_dir`.pdf". If in *[INPUT_FILE mode]*, the default filename of this PDF will be prefixed by either the PHITS input filename or "phitsout_", depending on which is provided to this function. Inputs: (optional, the same as in and directly passed to the `parse_tally_output_file()` function) - **`make_PandasDF`** = (D=`True`) A Boolean determining whether a Pandas dataframe of the tally data array will be made - **`calculate_absolute_errors`** = (D=`True`) A Boolean determining whether the absolute uncertainty of each tally output value is to be calculated (simply as the product of the value and relative error); if `False`, the final dimension of `tally_data`, `ierr`, will be of length-2 rather than length-3 - **`save_output_pickle`** = (D=`True`) A Boolean determining whether the `tally_output` dictionary object is saved as a pickle file; if `True`, the file will be saved with the same path and name as the provided PHITS tally output file but with the .pickle extension. - **`include_phitsout_in_metadata`** = (D=`False`) A Boolean determining whether the "phits.out" file (`file(6)` in the [Parameters] section of a PHITS input file) in the same directory as `tally_output_dirpath`, if found, should be processed via `parse_phitsout_file()` and have its informational dictionary about the PHITS run added to the `'tally_metadata'` dictionary under the key `'phitsout'`. - This only works if a single "phits.out"-type file is found; if multiple are encountered (as determined by `determine_PHITS_output_file_type()`), they will still be processed with `parse_phitsout_file()` (if `save_output_pickle=True` or `merge_tally_outputs=True`) but not added to any tally output metadata dictionaries. In this circumstance and if `merge_tally_outputs=True`, the multiple phits.out dictionaries will be added to the merged dictionary. - If, instead of a Boolean, a dictionary-type object is provided, no search will be conducted and the provided dictionary will be taken as that to be added as the `'phitsout'` key in the `'tally_metadata'` dictionary. - Otherwise, if a string or [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object is provided, this will be taken as the path to the "phits.out"-type file to be processed and have its informational dictionary added to the `'tally_metadata'` dictionary. - Note that this is automatically set to `True` if in *[INPUT_FILE mode]* (if `tally_output_dirpath` is a file, rather than a directory, and deemed to be a PHITS input file or "phits.out"-type file, as determined by `determine_PHITS_output_file_type()`); to override this automatic behavior (i.e., to behave as if set to `False` even when provided a PHITS input or phits.out file), set `include_phitsout_in_metadata = {}`. - Setting `include_phitsout_in_metadata = None` functions the same as `include_phitsout_in_metadata = True` but sets `include_input_echo = False` in the `parse_phitsout_file()` call. - **`compress_pickle_with_lzma`** = (D=`False`; requires `save_output_pickle=True`) Boolean designating whether the pickle file to be saved will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included in the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`. A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as: `with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`. For most "normal" tallies, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for output from massive tallies. - **`autoplot_tally_output`** = (D=`False`; requires `make_PandasDF=True`) Boolean denoting whether, for each tally, the tally's output will be automatically plotted and saved to a PDF and PNG (of the same name/path as `tally_output_filepath` but ending in `.pdf`/`.png`) using the `autoplot_tally_results()` function. Inputs: (optional, the same as in and directly passed to the `parse_tally_dump_file()` function) - **`dump_data_number`** = (D=`None`) integer number of data per row in dump file, binary if >0 and ASCII if <0. This should match the value following `dump=` in the tally creating the dump file. If not specified, the search_for_dump_parameters() function will attempt to find it automatically. - **`dump_data_sequence`** = (D=`None`) string or list of integers with the same number of entries as `dump_data_number`, mapping each column in the dump file to their physical quantities. This should match the line following the `dump=` line in the tally creating the dump file. See PHITS manual section "6.7.22 dump parameter" for further explanations of these values. If not specified, the search_for_dump_parameters() function will attempt to find it automatically. - **`dump_return_directional_info`** = (D=`False`) Boolean designating whether extra directional information should be calculated and returned; these include: radial distance `r` from the origin in cm, radial distance `rho` from the z-axis in cm, polar angle `theta` between the direction vector and z-axis in radians [0,pi] (or degrees), and azimuthal angle `phi` of the direction vector in radians [-pi,pi] (or degrees). Note: This option requires all position and direction values [x,y,z,u,v,w] to be included in the dump file. - **`dump_use_degrees`** = (D=`False`) Boolean designating whether angles `theta` and `phi` are returned in units of degrees. Default setting is to return angles in radians. - **`dump_max_entries_read`** = (D=`None`) integer number specifying the maximum number of entries/records of the dump file to be read. By default, all records in the dump file are read. - **`dump_save_namedtuple_list`** = (D=`True`) Boolean designating whether `dump_data_list` is saved to a pickle file (for complicated reasons, the namedtuple list is converted to a [NumPy recarray](https://numpy.org/doc/stable/reference/generated/numpy.recarray.html) before being saved, which is nearly functionally identical to the list of namedtuples. For more info and the full explanation, see the documentation for the `parse_tally_dump_file()` function's `save_namedtuple_list` argument, which is what this argument is passed to anyways). - **`dump_save_Pandas_dataframe`** = (D=`True`) Boolean designating whether `dump_data_frame` is saved to a pickle file (via Pandas .to_pickle()). - **`split_binary_dumps_over_X_GB`** = (D=`20` GB) an integer/float specifying the number of gigabytes over which a _binary_ dump file will be split into chunks of this size for processing. - **`merge_split_dump_handling`** = (D=`0`) For instances where binary dump files are to be split, as determined by the `split_binary_dumps_over_X_GB` setting, input one of the following numbers to determine what will happen to the intermediate pickle files created from each split chunk: - `0` = Merge the intermediate pickle files (via `merge_dump_file_pickles()`), then, if successful, delete them. - `1` = Do not merge the intermediate pickle files. - `2` = Merge the intermediate pickle files but do not delete them afterward. - **`dump_merge_MPI_subdumps`** = (D=`True`) Boolean designating whether the pickled namedtuple lists and/or Pandas DataFrames for all "sub-dumps" from an MPI run of PHITS should be merged into single namedtuple list and/or Pandas DataFrame pickle file(s) (with `merge_dump_file_pickles()`). When a dump file is written in an MPI execution of PHITS, it is split into N "sub dump" files, one per MPI process, and given an extra final extension of the form ".###" (where "###" is an arbitrarily long numeric string of digits 0-9 designating the MPI process). This option is only active when such files are encountered. - **`dump_delete_MPI_subdumps_post_merge`** = (D=`True`) Requires `dump_merge_MPI_subdumps=True`, Boolean designating whether the numerous "sub dump" pickle files merged when `dump_merge_MPI_subdumps=True` (and MPI dump files are found) are deleted after the merged version combining all of them is saved. Output: - **`tally_output_list`** = a list of `tally_output` dictionary objects (if `return_tally_output=True`) with the below keys and values / a list of file paths to pickle files containing `tally_output` dictionary objects: - `'tally_data'` = a 10-dimensional NumPy array containing all tally results, explained in more detail below - `'tally_metadata'` = a dictionary/Munch† object with various data extracted from the tally output file, such as axis binning and units - `'tally_dataframe'` = (optionally included if setting `make_PandasDF = True`) a Pandas dataframe version of `tally_data` - This list of `tally_output` dictionary objects will be returned as a larger merged dictionary/Munch† object, keyed using the tally output file names, instead if `merge_tally_outputs=True` and `return_tally_output=True`. If `merge_tally_outputs=True`, `return_tally_output=False`, `save_output_pickle=False`, and `save_pickle_of_merged_tally_outputs=True`, the path to this saved merged tally output dictionary pickle file will be returned instead. †_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `tally_metadata` dictionary and merged tally output dictionary will instead be Munch objects, which are identical to a dictionary but additionally allows attribute-style access (e.g., `tally_metadata.mesh` instead of only `tally_metadata['mesh']`). If you do not have Munch, then these will just be standard dictionary objects. If, for whatever reason, you have Munch installed but do not wish for these to be Munch objects, then in the first line of code for both this and the `parse_tally_header()` function, set `prefer_to_munch_merged_dict = False` and `prefer_to_munch_meta_dict = False`, respectively._ ''' prefer_to_munch_merged_dict = True import os from inspect import signature if prefer_to_munch_merged_dict: try: from munch import Munch merged_outputs_dict = Munch({}) except: merged_outputs_dict = {} else: merged_outputs_dict = {} if save_pickle_of_merged_tally_outputs is None: save_pickle_of_merged_tally_outputs = save_output_pickle operating_in_directory_mode = True # otherwise, if False, assume INPUT_FILE mode is_file6_phitsout_file, is_PHITS_input_file = False, False include_THIS_phitsout_dict_in_metadata = False include_input_echo = True if include_phitsout_in_metadata is None: include_input_echo = False if not os.path.isdir(tally_output_dirpath): #print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath) PHITS_file_type = determine_PHITS_output_file_type(tally_output_dirpath) if PHITS_file_type['is_file6_phitsout_file'] or PHITS_file_type['is_PHITS_input_file']: is_file6_phitsout_file, is_PHITS_input_file = PHITS_file_type['is_file6_phitsout_file'], PHITS_file_type['is_PHITS_input_file'] file_type_str = 'PHITS input' if is_PHITS_input_file else 'phits.out' #print('However, it is a valid '+file_type_str+' file, thus the tally outputs from this PHITS run will be processed.') operating_in_directory_mode = False original_input_path = tally_output_dirpath head, tail = os.path.split(tally_output_dirpath) tally_output_dirpath = head elif os.path.isfile(tally_output_dirpath): print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath) head, tail = os.path.split(tally_output_dirpath) tally_output_dirpath = head print('However, it is a valid path to a file; thus, its parent directory will be used:',tally_output_dirpath) else: print('The provided path to "tally_output_dir" is not a directory:', tally_output_dirpath) print('Nor is it a valid path to a file. ERROR! Aborting...') return None if output_file_suffix is None: if operating_in_directory_mode: output_file_suffix = '.out' else: output_file_suffix = '' if is_file6_phitsout_file or is_PHITS_input_file: # Only list out files produced by this PHITS run if is_PHITS_input_file: files_dict = extract_tally_outputs_from_phits_input(original_input_path) phitsout_dict = parse_phitsout_file(files_dict['phitsout'], save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo) if not (isinstance(include_phitsout_in_metadata, dict) and include_phitsout_in_metadata=={}): include_THIS_phitsout_dict_in_metadata = True if files_dict['active_infl_found']: print('An active "infl:{}" section was found in the PHITS input; therefore its phits.out file will be used for obtaining the complete list of generated output files.') files_dict = phitsout_dict['produced_files'] else: phitsout_file = Path(original_input_path) phitsout_dict = parse_phitsout_file(phitsout_file, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, include_input_echo=include_input_echo) if not (isinstance(include_phitsout_in_metadata, dict) and include_phitsout_in_metadata=={}): include_THIS_phitsout_dict_in_metadata = True files_dict = phitsout_dict['produced_files'] files_in_dir = files_dict['standard_output'] if include_dump_files: files_in_dir += files_dict['dump_output'] elif include_subdirectories: # Get paths to all files in this dir and subdirs files_in_dir = [] for path, subdirs, files in os.walk(tally_output_dirpath): for name in files: files_in_dir.append(os.path.join(path, name)) else: # Just get paths to files in this dir files_in_dir = [os.path.join(tally_output_dirpath, f) for f in os.listdir(tally_output_dirpath) if os.path.isfile(os.path.join(tally_output_dirpath, f))] if is_PHITS_input_file: autoplot_dir_filename = Path(original_input_path).stem + "_ALL_TALLY_OUTPUTS_PLOTTED.pdf" merged_output_pickle_filename = Path(original_input_path).stem + "_ALL_TALLY_OUTPUTS.pickle" elif is_file6_phitsout_file: autoplot_dir_filename = "phitsout_ALL_TALLY_OUTPUTS_PLOTTED.pdf" merged_output_pickle_filename = "phitsout_ALL_TALLY_OUTPUTS.pickle" else: autoplot_dir_filename = "ALL_TALLY_OUTPUTS_PLOTTED.pdf" merged_output_pickle_filename = "ALL_TALLY_OUTPUTS.pickle" if isinstance(autoplot_all_tally_output_in_dir, str): autoplot_dir_filename = autoplot_all_tally_output_in_dir + '.pdf' autoplot_all_tally_output_in_dir = True if isinstance(merge_tally_outputs, str): merged_output_pickle_filename = merge_tally_outputs + '.pickle' merge_tally_outputs = True # Determine which files should be parsed filepaths_to_process = [] dump_filepaths_to_process = [] phitsout_files_to_process = [] len_suffix = len(output_file_suffix) len_prefix = len(output_file_prefix) len_reqstr = len(output_file_required_string) MPI_subdump_files_found = [] MPI_subdump_filestems_found = [] for f in files_in_dir: head, tail = os.path.split(f) file_suffixes = tail.split('.')[1:] if len(file_suffixes) >= 1 and is_number(file_suffixes[-1][1:]) and '_dmp' in tail: # MPI dump found MPI_subdump_files_found.append(f) tail = tail[:-1 * (1+len(file_suffixes[-1]))] if Path(head,tail) not in MPI_subdump_filestems_found: MPI_subdump_filestems_found.append(Path(head,tail)) if len_suffix > 0 and tail[-len_suffix:] != output_file_suffix: continue if len_prefix > 0 and tail[:len_prefix] != output_file_prefix: continue if len_reqstr > 0 and output_file_required_string not in tail: continue if tail[(-4-len_suffix):] == '_err' + output_file_suffix: continue this_file_type = determine_PHITS_output_file_type(f) if this_file_type['is_standard_tally_output']: filepaths_to_process.append(f) elif include_dump_files and tail[(-4 - len_suffix):] == '_dmp' + output_file_suffix and ( this_file_type['is_binary_tally_dump'] or this_file_type['is_binary_tally_dump']): dump_filepaths_to_process.append(f) elif this_file_type['is_file6_phitsout_file']: phitsout_files_to_process.append(f) else: continue # with open(f) as ff: # try: # first_line = ff.readline().strip() # except: # triggered if encountering binary / non ASCII or UTF-8 file # if include_dump_files and tail[(-4-len_suffix):] == '_dmp' + output_file_suffix: # dump_filepaths_to_process.append(f) # continue # if len(first_line) == 0: continue # if first_line[0] != '[' : # if '_________________________________________________________' in first_line: # phits.out file # phitsout_files_to_process.append(f) # if include_dump_files and tail[(-4-len_suffix):] == '_dmp' + output_file_suffix: # dump_filepaths_to_process.append(f) # continue # filepaths_to_process.append(f) # If there is only one phits.out file, we can safely assume it belongs to all other processed output if len(phitsout_files_to_process) == 1 and (save_output_pickle or (include_phitsout_in_metadata is None or (type(include_phitsout_in_metadata) == bool and include_phitsout_in_metadata))): phitsout_dict = parse_phitsout_file(phitsout_files_to_process[0], include_input_echo=include_input_echo, save_phitsout_pickle=save_output_pickle) if include_phitsout_in_metadata is None or (type(include_phitsout_in_metadata) == bool and include_phitsout_in_metadata): include_THIS_phitsout_dict_in_metadata = True # if merge_tally_outputs: # key = str(f.relative_to(tally_output_dirpath)) # merged_outputs_dict[key] = phitsout_dict elif len(phitsout_files_to_process) > 1 and (save_output_pickle or merge_tally_outputs): # Otherwise, if more, only process if pickles are to be saved for f in phitsout_files_to_process: temp_phitsout_dict = parse_phitsout_file(f, save_phitsout_pickle=save_output_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma) if merge_tally_outputs: key = str(Path(f).relative_to(tally_output_dirpath)) merged_outputs_dict[key] = temp_phitsout_dict if include_THIS_phitsout_dict_in_metadata: # provide a phitsout dictionary created in THIS function to parse_tally_output_file, otherwise pass provided include_phitsout_in_metadata argument include_phitsout_in_metadata = phitsout_dict tally_output_pickle_path_list = [] tally_output_list = [] tally_include_in_plotting_list = [] max_num_values_to_plot = signature(autoplot_tally_results).parameters['max_num_values_to_plot'].default # 1e7 for f in filepaths_to_process: f = Path(f) xz_text = '' if compress_pickle_with_lzma: xz_text = '.xz' path_to_pickle_file = Path(f.parent, f.stem + '.pickle' + xz_text) tally_output = parse_tally_output_file(f, make_PandasDF=make_PandasDF, calculate_absolute_errors=calculate_absolute_errors, save_output_pickle=save_output_pickle, include_phitsout_in_metadata=include_phitsout_in_metadata, prefer_reading_existing_pickle=prefer_reading_existing_pickle, compress_pickle_with_lzma=compress_pickle_with_lzma, autoplot_tally_output=autoplot_tally_output) if tally_output is not None: tally_output_pickle_path_list.append(path_to_pickle_file) tot_num_values = np.prod(np.shape(tally_output['tally_data'])[:-1]) if tot_num_values > max_num_values_to_plot: tally_include_in_plotting_list.append(False) if autoplot_all_tally_output_in_dir and not autoplot_tally_output: # only print this message if not already printed print('WARNING: Tally output for ', tally_output['tally_metadata']['file'], ' is VERY LARGE (', tot_num_values, ' elements), deemed too large for automatic plotting (default max of', max_num_values_to_plot, 'elements).') else: tally_include_in_plotting_list.append(True) if return_tally_output or (merge_tally_outputs and not save_output_pickle and autoplot_all_tally_output_in_dir): tally_output_list.append(tally_output) if merge_tally_outputs: if operating_in_directory_mode: key = str(Path(f).relative_to(tally_output_dirpath)) else: key = tally_output['tally_metadata']['file'] merged_outputs_dict[key] = tally_output if include_dump_files: for f in dump_filepaths_to_process: f = Path(f) parse_tally_dump_file(f, dump_data_number=dump_data_number, dump_data_sequence=dump_data_number, return_directional_info=dump_return_directional_info, use_degrees=dump_use_degrees, max_entries_read=dump_max_entries_read, return_namedtuple_list=False, return_Pandas_dataframe=False, save_namedtuple_list=dump_save_namedtuple_list, save_Pandas_dataframe=dump_save_Pandas_dataframe, prefer_reading_existing_pickle=prefer_reading_existing_pickle, split_binary_dumps_over_X_GB=split_binary_dumps_over_X_GB, merge_split_dump_handling=merge_split_dump_handling) if dump_merge_MPI_subdumps: for id in MPI_subdump_filestems_found: # identified independent tallies with dumps dumps_to_merge = [] id_head, id_tail = os.path.split(id) for f in MPI_subdump_files_found: head, tail = os.path.split(f) tail = tail[:-1 * (1 + len(tail.split('.')[-1]))] if tail == id_tail: dumps_to_merge.append(f) merged_fp = Path(Path(id_head),id_tail) merge_succeeded = merge_dump_file_pickles(dumps_to_merge, merged_dump_base_filepath=merged_fp, delete_pre_merge_pickles=dump_delete_MPI_subdumps_post_merge) if merge_tally_outputs and save_pickle_of_merged_tally_outputs: # created dictionary of merged tally outputs import pickle, lzma merged_pickle_path = Path(tally_output_dirpath, merged_output_pickle_filename) if compress_pickle_with_lzma: merged_pickle_path = Path(merged_pickle_path.parent, merged_pickle_path.name + '.xz') with lzma.open(merged_pickle_path, 'wb') as handle: pickle.dump(merged_outputs_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) else: with open(merged_pickle_path, 'wb') as handle: pickle.dump(merged_outputs_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', merged_pickle_path, '\n') if not return_tally_output: del merged_outputs_dict # In the event this is huge in memory, best to delete it here before the Pandas DF is made if autoplot_all_tally_output_in_dir: if not make_PandasDF: print('Plotting via "autoplot_tally_output=True" or "autoplot_all_tally_output_in_dir=True" requires also setting "make_PandasDF=True".') else: print('Compiling PDF of plots...') plot_filepath = Path(tally_output_dirpath, autoplot_dir_filename) plot_list = [] for i in range(len(tally_include_in_plotting_list)): if tally_include_in_plotting_list[i]: if return_tally_output or (merge_tally_outputs and not save_output_pickle): plot_list.append(tally_output_list[i]) else: plot_list.append(tally_output_pickle_path_list[i]) autoplot_tally_results(plot_list, plot_errorbars=calculate_absolute_errors, output_filename=plot_filepath) if return_tally_output: if merge_tally_outputs: return merged_outputs_dict else: return tally_output_list elif merge_tally_outputs and save_pickle_of_merged_tally_outputs and not save_output_pickle: return merged_pickle_path else: return tally_output_pickle_path_list
def parse_phitsout_file(phitsout_filepath, include_input_echo=True, save_phitsout_pickle=False, compress_pickle_with_lzma=False)
-
Description
Extracts useful information from the "phits.out" file (
file(6)
in the PHITS [Parameters] section) into a dictionary object, including the PHITS version number, time data (start/stop time, CPU time, MPI info, etc.), event and particle details, papers to cite, and more. Sections taking the approximate form of a table are converted to Pandas DataFrames before being stored in the dictionary object.Dependencies
import pandas as pd
from munch import Munch
(will still run if package not found)
Inputs
phitsout_filepath
= string or Path object denoting the path to the "phits.out" file to be parsedinclude_input_echo
= (optional, D=True
) Boolean indicating whether the "Input Echo" block of text should be included in the output dictionary.save_phitsout_pickle
= (optional, D=False
) Boolean indicating whether a pickle file of the returnedphitsout_dict
dictionary should be saved, named "phits_out.pickle[.xz]" and in the same directory asphitsout_filepath
. More specifically, the filename will be that of the "phits.out"-type file (file(6)
) with "_out.pickle[.xz]" appended to it.compress_pickle_with_lzma
= (optional, D=False
; requiressave_phitsout_pickle=True
) Boolean designating whether the pickle file to be saved will be compressed with LZMA compression (included in the baseline Python standard library); if so, the pickle file's extension will be'.pickle.xz'
instead of just'.pickle'
. A *.pickle.xz file can then be opened (after importingpickle
andlzma
) as:
with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)
.
For most "normal" PHITS runs, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for instances with very large geometry, materials, tally, etc. sections.
Outputs
phitsout_dict
= a dictionary/Munch† object with various information/data extracted from the "phits.out" file. It is organized with the following keys:'job'
: a dictionary of information about the run (PHITS version, start/end time, CPU time, MPI info, etc.)'summary'
: a dictionary of the various text tables in the "Summary for the end of job" section of the phits.out file. The sections clearly formatted as tables are generally converted into Pandas DataFrames.'memory'
: a string of the section of text in phits.out containing memory usage information'batch'
: a string of the section of text in phits.out containing information on each batch'input_echo'
: a string of the "Input Echo" text section of phits.out'produced_files'
: a dictionary of the output files produced by this PHITS run's tallies, catergorized with keys'standard_output'
(list ofpathlib.Path()
objects),'dump_output'
(list ofpathlib.Path()
objects), and'phitsout'
(a singlepathlib.Path()
object); this is thefiles_dict
output returned by theextract_tally_outputs_from_phits_input()
function.'citation_request'
: a string of the "Citation Request" text section of phits.out listing additional papers beyond the main PHITS reference paper that should be cited in any publications using results from this simulation owing to using dependent models, codes, etc.'extra_lines'
: a list of strings of any lines parsed by this function not classified as belonging to any other section (and clearly containing information; i.e., not just blank or delimiter lines)
To conveniently view the contents of
phitsout_dict
, one can import the built-in pprint libraryimport pprint
and then usepprint.pp(dict(phitsout_dict))
.†If you have the Munch package installed, the
phitsout_dict
dictionary will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access. If you do not have Munch, thenphitsout_dict
will just be a standard dictionary object. If, for whatever reason, you have Munch installed but do not wish forphitsout_dict
to be a Munch object, then in the first line of code for theparse_phitsout_file()
function, setprefer_to_munch_meta_dict = False
.Expand source code
def parse_phitsout_file(phitsout_filepath, include_input_echo=True, save_phitsout_pickle=False, compress_pickle_with_lzma=False): ''' Description: Extracts useful information from the "phits.out" file (`file(6)` in the PHITS [Parameters] section) into a dictionary object, including the PHITS version number, time data (start/stop time, CPU time, MPI info, etc.), event and particle details, papers to cite, and more. Sections taking the approximate form of a table are converted to Pandas DataFrames before being stored in the dictionary object. Dependencies: - `import pandas as pd` - `from munch import Munch` (will still run if package not found) Inputs: - `phitsout_filepath` = string or Path object denoting the path to the "phits.out" file to be parsed - `include_input_echo` = (optional, D=`True`) Boolean indicating whether the "Input Echo" block of text should be included in the output dictionary. - `save_phitsout_pickle` = (optional, D=`False`) Boolean indicating whether a pickle file of the returned `phitsout_dict` dictionary should be saved, named "phits_out.pickle[.xz]" and in the same directory as `phitsout_filepath`. More specifically, the filename will be that of the "phits.out"-type file (`file(6)`) with "_out.pickle[.xz]" appended to it. - `compress_pickle_with_lzma` = (optional, D=`False`; requires `save_phitsout_pickle=True`) Boolean designating whether the pickle file to be saved will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included in the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the pickle file's extension will be `'.pickle.xz'` instead of just `'.pickle'`. A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as: `with lzma.open(path_to_picklexz_file, 'rb') as file: tally_output = pickle.load(file)`. For most "normal" PHITS runs, the pickle file sizes are likely to be small enough to not warrant compression, but LZMA compression can reduce the file size by several orders of magnitude, great for instances with very large geometry, materials, tally, etc. sections. Outputs: - `phitsout_dict` = a dictionary/Munch† object with various information/data extracted from the "phits.out" file. It is organized with the following keys: - `'job'` : a dictionary of information about the run (PHITS version, start/end time, CPU time, MPI info, etc.) - `'summary'` : a dictionary of the various text tables in the "Summary for the end of job" section of the phits.out file. The sections clearly formatted as tables are generally converted into Pandas DataFrames. - `'memory'` : a string of the section of text in phits.out containing memory usage information - `'batch'` : a string of the section of text in phits.out containing information on each batch - `'input_echo'` : a string of the "Input Echo" text section of phits.out - `'produced_files'` : a dictionary of the output files produced by this PHITS run's tallies, catergorized with keys `'standard_output'` (list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects), `'dump_output'` (list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects), and `'phitsout'` (a single [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object); this is the `files_dict` output returned by the `extract_tally_outputs_from_phits_input()` function. - `'citation_request'` : a string of the "Citation Request" text section of phits.out listing additional papers beyond the main PHITS reference paper that should be cited in any publications using results from this simulation owing to using dependent models, codes, etc. - `'extra_lines'` : a list of strings of any lines parsed by this function not classified as belonging to any other section (and clearly containing information; i.e., not just blank or delimiter lines) To conveniently view the contents of `phitsout_dict`, one can import the built-in pprint library `import pprint` and then use `pprint.pp(dict(phitsout_dict))`. †_If you have the [Munch package](https://github.com/Infinidat/munch) installed, the `phitsout_dict` dictionary will instead be a Munch object, which is identical to a dictionary but additionally allows attribute-style access. If you do not have Munch, then `phitsout_dict` will just be a standard dictionary object. If, for whatever reason, you have Munch installed but do not wish for `phitsout_dict` to be a Munch object, then in the first line of code for the `parse_phitsout_file()` function, set `prefer_to_munch_meta_dict = False`._ ''' prefer_to_munch_meta_dict = True import datetime import io import pandas as pd phitsout_filepath = Path(phitsout_filepath) starting_dict = {'job':{},'summary':{},'memory':{},'batch':{}} if prefer_to_munch_meta_dict: try: from munch import Munch meta = Munch(starting_dict) except: meta = starting_dict else: meta = starting_dict in_header = True in_input_echo = False in_footer = False header_lines = [] input_echo = '' footer_lines = [] # First, extract text from phits.out by section with open(phitsout_filepath, mode='r', encoding="utf-8") as f: for line in f: if ">>> Input Echo >>>=============================================================" in line: in_header = False in_input_echo = True continue if "[END] of Input Echo <<<========================================================" in line: in_input_echo = False in_footer = True continue if in_header: header_lines.append(line) elif in_input_echo: input_echo += line elif in_footer: footer_lines.append(line) # Extract info from header for li, line in enumerate(header_lines): if 'Version =' in line: key, value = extract_data_from_header_line(line.replace('|','').strip()) meta['job']['PHITS_version'] = value elif '[ Job Title ]' in line: meta['job']['title'] = header_lines[li+2].replace('|','').strip() elif 'Starting Date' in line: key, date = extract_data_from_header_line(line.replace('|', '').strip()) year, month, day = [int(i) for i in date.split('-')] key, time = extract_data_from_header_line(header_lines[li+1].replace('|', '').strip()) hour, minute, second = [int(i) for i in time.replace('h ',':').replace('m ',':').split(':')] meta['job']['datetime_start'] = datetime.datetime(year, month, day, hour, minute, second) meta['job']['datetime_start_ISO_str'] = meta['job']['datetime_start'].isoformat() elif 'Total PE =' in line: key, pe = extract_data_from_header_line(line.replace('|', '').strip()) meta['job']['MPI_total_num_PE'] = pe meta['job']['MPI_executable_num_PE'] = pe - 1 # input echo if include_input_echo: meta['input_echo'] = input_echo # produced files meta['produced_files'] = extract_tally_outputs_from_phits_input({'sim_base_dir_path':phitsout_filepath.parent, 'input_echo':input_echo}, use_path_and_string_mode=True) # Extract info from footer extra_lines = [] # extra footer lines not added to any specific dictionary entry skip_lines = 0 memory_report = '' batch_report = '' citation_request = '' line_of_dashes = '-------------------------------------------------------------------------------' for li, line in enumerate(footer_lines): if skip_lines>0: skip_lines -= 1 continue if '<<< Report of' in line and 'memory' in line: for eli in range(5): memory_report += footer_lines[li + eli] skip_lines = 4 elif line[:4] == 'bat[': for eli in range(5): batch_report += footer_lines[li + eli] skip_lines = 4 elif line[:-1] == line_of_dashes and footer_lines[li + 2][:-1] == line_of_dashes: # encountered table in_special_events_table, in_special_MPI_table = False, False if "CPU time and number of event called in PHITS" in footer_lines[li + 1][:-1]: in_special_events_table = True # get CPU time total_cpu_time_sec = float(footer_lines[li + 5][:-1].split('=')[1].strip()) meta['job']['CPU_time_sec_int'] = total_cpu_time_sec meta['job']['CPU_time_datetime_object'] = datetime.timedelta(seconds=total_cpu_time_sec) meta['job']['CPU_time_str'] = str(meta['job']['CPU_time_datetime_object']) elif "Final Parallel Status:" in footer_lines[li + 1][:-1]: in_special_MPI_table = True table_text = '' table_title = footer_lines[li + 1][:-1] column_header_line = table_title nsl = 0 # number of lines to skip in table content table_keyname = table_title.strip().replace(' ','_') if "number of analyz call vs ncol" in table_title: column_header_line = footer_lines[li + 3][:-1] # + ' description' nsl = 1 table_keyname = 'analyz_calls_per_ncol' elif "List of transport particles" in table_title: column_header_line = footer_lines[li + 3][:-1] nsl = 1 table_keyname = 'transport_particles' elif "CPU time and number of event called in PHITS" in table_title: column_header_line = ' event count' nsl = 6 table_keyname = 'events_and_models' reassign_key_to_first_col_name = False if column_header_line == table_title: reassign_key_to_first_col_name = True column_header_line = column_header_line.replace('. p','._p') column_header_line = column_header_line.replace('source:','') col_headers_with_spaces = ['weight per source', 'average weight', 'total source'] for x in col_headers_with_spaces: column_header_line = column_header_line.replace(x,x.replace(' ','_')) line_len = len(line) tesli = 3 + nsl eli = 0 next_line = footer_lines[li + tesli + eli + 1] while len(next_line)>1 or eli==0: this_line = footer_lines[li + tesli + eli] next_line = footer_lines[li + tesli + eli + 1] if len(this_line)>1: if in_special_events_table: this_line = this_line.replace('=',' ') this_line = this_line.replace('photonucl lib', 'photonucl_lib') this_line = this_line.replace('frag data', 'frag_data') table_text += this_line #.rstrip() + '\n' #.replace(':','') eli += 1 if in_special_events_table: if len(next_line)<2 and '>>>' not in next_line and '---' not in next_line and '===' not in next_line: this_line = footer_lines[li + tesli + eli] next_line = footer_lines[li + tesli + eli + 1] eli += 1 if '>>>' in next_line or '---' in next_line or '===' in next_line: eli -= 1 break if in_special_MPI_table: # also grab final MPI stats eli += 1 while len(footer_lines[li + tesli + eli]) > 1: key, value = extract_data_from_header_line(footer_lines[li + tesli + eli]) key = 'MPI_' + key.strip().replace(' ','_').replace('time','time_sec') meta['job'][key] = value eli += 1 skip_lines = tesli + eli if '=' not in table_text: table_text = column_header_line + '\n' + table_text table_df = pd.read_csv(io.StringIO(table_text.replace(',','.')), comment=':', sep='\s+', on_bad_lines='skip') if ':' in table_text: descriptions = [x.split(':', 1)[-1] for x in table_text.split('\n')[1:-1]] table_df['description'] = descriptions if reassign_key_to_first_col_name: if 'source: maxcas' in table_title: table_keyname = 'source' else: table_keyname = table_df.columns[0] #print(table_text) #print(table_df.to_string()) meta['summary'][table_keyname + '_df'] = table_df meta['summary'][table_keyname + '_text'] = table_text else: table_text = column_header_line + '\n' + table_text meta['summary'][table_keyname + '_text'] = table_text else: if 'job termination date' in line: date = line.split(':')[1].strip() year, month, day = [int(i) for i in date.split('/')] time = footer_lines[li + 1].split(':',1)[1].strip() hour, minute, second = [int(i) for i in time.split(':')] meta['job']['datetime_end'] = datetime.datetime(year, month, day, hour, minute, second) meta['job']['datetime_end_ISO_str'] = meta['job']['datetime_start'].isoformat() skip_lines = 1 elif '>>> Citation Request >>>' in line: citation_request += line in_citation_request = True eli = 1 while in_citation_request: this_line = footer_lines[li + eli] if this_line.strip() == line_of_dashes or this_line.strip() == 'END': break citation_request += this_line eli += 1 skip_lines = eli - 1 meta['citation_request'] = citation_request elif 'initial random seed:' in line: meta['job']['initial_random_seed_bitrseed'] = footer_lines[li + 1].replace('bitrseed =','').strip() meta['job']['next_initial_random_seed_bitrseed'] = footer_lines[li + 3].replace('bitrseed =', '').strip() skip_lines = 4 else: extra_lines.append(line) meta['memory']['report_text'] = memory_report meta['batch']['report_text'] = batch_report # Clean up extra lines extra_lines_cleaned = [] for line in extra_lines: if len(line.strip()) == 0: continue if line.strip() == line_of_dashes: continue if 'Summary for the end of job' in line: continue if line.strip() == 'END': continue extra_lines_cleaned.append(line) meta['extra_lines'] = extra_lines_cleaned phitsout_dict = meta if save_phitsout_pickle: import pickle, lzma pickle_path = Path(phitsout_filepath.parent, phitsout_filepath.stem + '_out.pickle') if compress_pickle_with_lzma: pickle_path = Path(pickle_path.parent, pickle_path.name + '.xz') with lzma.open(pickle_path, 'wb') as handle: pickle.dump(phitsout_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) else: with open(pickle_path, 'wb') as handle: pickle.dump(phitsout_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', pickle_path, '\n') return phitsout_dict
def tally(data, bin_edges=[], min_bin_left_edge=None, max_bin_right_edge=None, nbins=None, bin_width=None, divide_by_bin_width=False, normalization=None, scaling_factor=1, place_overflow_at_ends=True, return_uncertainties=False, return_event_indices_histogram=False)
-
Description
Tally number of incidences of values falling within a desired binning structure
Inputs
data
= list of values to be tallied/histogrammedbin_edges
= list of N+1 bin edge values for a tally of N binsmin_bin_left_edge
= left/minimum edge value of the first binmax_bin_right_edge
= right/maximum edge value of the last binnbins
= number of equally-sized bins to be created frommin_bin_left_edge
tomax_bin_right_edge
bin_width
= constant width of bins to be created frommin_bin_left_edge
tomax_bin_right_edge
divide_by_bin_width
= Boolean denoting whether final bin values are divided by their bin widths (D=False
)normalization
= determine how the resulting histogram is normalized (D=None
), options are:[None, 'unity-sum', 'unity-max-val']
. IfNone
, no additional normalization is done. Ifunity-sum
, the data is normalized such that its sum will be 1. Ifunity-max-val
, the data is normalized such that the maximum value is 1. The operation occurs after any bin width normalization fromdivide_by_bin_width
but before any scaling fromscaling_factor
.scaling_factor
= value which all final bins are multiplied/scaled by (D=1
)place_overflow_at_ends
= handling of values outside of binning range (D=True
); ifTrue
extreme values are tallied in the first/last bin, ifFalse
extreme values are discardedreturn_uncertainties
= Boolean denoting if should return an extra N-length list whose elements are the statistical uncertainties (square root w/ normalizations) of the tally bins (D=False
)return_event_indices_histogram
= Boolean denoting if should return an extra N-length list whose elements are each a list of the event indices corresponding to each bin (D=False
)
Notes
Regarding the binning structure, this function only needs to be provided
bin_edges
directly (takes priority) or the information needed to calculatebin_edges
, that is:min_bin_left_edge
andmax_bin_right_edge
and eithernbins
orbin_width
. (Priority is given tonbins
if both are provided.)Outputs
tallied_hist
= N-length list of tallied databin_edges
= list of N+1 bin edge values for a tally of N binstallied_hist_err
= (optional) N-length list of statistical uncertainties of tallied datatallied_event_indicies
= (optional) N-length list of, for each bin, a list of the event indices populating it
Expand source code
def tally(data, bin_edges=[], min_bin_left_edge=None, max_bin_right_edge=None, nbins=None, bin_width=None, divide_by_bin_width=False, normalization=None, scaling_factor=1, place_overflow_at_ends=True, return_uncertainties=False, return_event_indices_histogram=False): ''' Description: Tally number of incidences of values falling within a desired binning structure Inputs: - `data` = list of values to be tallied/histogrammed - `bin_edges` = list of N+1 bin edge values for a tally of N bins - `min_bin_left_edge` = left/minimum edge value of the first bin - `max_bin_right_edge` = right/maximum edge value of the last bin - `nbins` = number of equally-sized bins to be created from `min_bin_left_edge` to `max_bin_right_edge` - `bin_width` = constant width of bins to be created from `min_bin_left_edge` to `max_bin_right_edge` - `divide_by_bin_width` = Boolean denoting whether final bin values are divided by their bin widths (D=`False`) - `normalization` = determine how the resulting histogram is normalized (D=`None`), options are: `[None, 'unity-sum', 'unity-max-val']`. If `None`, no additional normalization is done. If `unity-sum`, the data is normalized such that its sum will be 1. If `unity-max-val`, the data is normalized such that the maximum value is 1. The operation occurs after any bin width normalization from `divide_by_bin_width` but before any scaling from `scaling_factor`. - `scaling_factor` = value which all final bins are multiplied/scaled by (D=`1`) - `place_overflow_at_ends` = handling of values outside of binning range (D=`True`); if `True` extreme values are tallied in the first/last bin, if `False` extreme values are discarded - `return_uncertainties` = Boolean denoting if should return an extra N-length list whose elements are the statistical uncertainties (square root w/ normalizations) of the tally bins (D=`False`) - `return_event_indices_histogram` = Boolean denoting if should return an extra N-length list whose elements are each a list of the event indices corresponding to each bin (D=`False`) Notes: Regarding the binning structure, this function only needs to be provided `bin_edges` directly (takes priority) or the information needed to calculate `bin_edges`, that is: `min_bin_left_edge` and `max_bin_right_edge` and either `nbins` or `bin_width`. (Priority is given to `nbins` if both are provided.) Outputs: - `tallied_hist` = N-length list of tallied data - `bin_edges` = list of N+1 bin edge values for a tally of N bins - `tallied_hist_err` = (optional) N-length list of statistical uncertainties of tallied data - `tallied_event_indicies` = (optional) N-length list of, for each bin, a list of the event indices populating it ''' normalization_valid_entries = [None, 'unity-sum', 'unity-max-val'] if normalization not in normalization_valid_entries: print("Entered normalization option of ",normalization," is not a valid option; please select from the following: [None, 'unity-sum', 'unity-max-val']".format()) if len(bin_edges)!=0: bin_edges = np.array(bin_edges) else: if nbins != None: bin_edges = np.linspace(min_bin_left_edge,max_bin_right_edge,num=nbins+1) else: bin_edges = np.arange(min_bin_left_edge,max_bin_right_edge+bin_width,step=bin_width) nbins = len(bin_edges) - 1 if return_event_indices_histogram: tallied_event_indicies = [] tallied_hist = np.zeros(nbins) for i in range(nbins): tallied_event_indicies.append([]) # events must be histogrammed manually for i, val in enumerate(data): if val < bin_edges[0]: if place_overflow_at_ends: tallied_hist[0] += 1 tallied_event_indicies[0].append(i) continue if val > bin_edges[-1]: if place_overflow_at_ends: tallied_hist[-1] += 1 tallied_event_indicies[-1].append(i) continue for j, be in enumerate(bin_edges): if be > val: # found right edge of bin containing val tallied_hist[j-1] += 1 tallied_event_indicies[j-1].append(i) break else: tallied_hist, bins = np.histogram(data,bins=bin_edges) if return_uncertainties: tallied_hist_err = np.sqrt(tallied_hist) if divide_by_bin_width: tallied_hist_err = tallied_hist_err/(bin_edges[1:]-bin_edges[:-1]) if normalization=='unity-sum': tallied_hist_err = tallied_hist_err/np.sum(tallied_hist) if normalization=='unity-max-val': tallied_hist_err = tallied_hist_err/np.max(tallied_hist) if scaling_factor != 1: tallied_hist_err = tallied_hist_err*scaling_factor if divide_by_bin_width: tallied_hist = tallied_hist/(bin_edges[1:]-bin_edges[:-1]) if normalization=='unity-sum': tallied_hist = tallied_hist/np.sum(tallied_hist) if normalization=='unity-max-val': tallied_hist = tallied_hist/np.max(tallied_hist) if scaling_factor != 1: tallied_hist = tallied_hist*scaling_factor if return_event_indices_histogram: if return_uncertainties: return tallied_hist,bin_edges,tallied_hist_err,tallied_event_indicies else: return tallied_hist,bin_edges,tallied_event_indicies else: if return_uncertainties: return tallied_hist,bin_edges,tallied_hist_err else: return tallied_hist,bin_edges
def rebinner(output_xbins, input_xbins, input_ybins)
-
Description
The purpose of this function is to rebin a set of y values corresponding to a set of x bins to a new set of x bins. The function seeks to be as generalized as possible, meaning bin sizes do not need to be consistent nor do the new bin edges necessarily need to line up exactly with the old bin edges.
Dependencies
import numpy as np
Inputs
output_xbins
= output array containing bounds of x bins of length N; first entry is leftmost bin boundaryinput_xbins
= input array containing bounds of x bins of length M; first entry is leftmost bin boundaryinput_ybins
= input array containing y values of length M-1
Outputs
output_ybins
= output array containing y values of length N-1
Expand source code
def rebinner(output_xbins,input_xbins,input_ybins): """ Description: The purpose of this function is to rebin a set of y values corresponding to a set of x bins to a new set of x bins. The function seeks to be as generalized as possible, meaning bin sizes do not need to be consistent nor do the new bin edges necessarily need to line up exactly with the old bin edges. Dependencies: `import numpy as np` Inputs: - `output_xbins` = output array containing bounds of x bins of length N; first entry is leftmost bin boundary - `input_xbins` = input array containing bounds of x bins of length M; first entry is leftmost bin boundary - `input_ybins` = input array containing y values of length M-1 Outputs: - `output_ybins` = output array containing y values of length N-1 """ N = len(output_xbins) M = len(input_xbins) output_ybins = np.zeros(N-1) for i in range(0,N-1): # For each output bin lxo = output_xbins[i] # lower x value of output bin uxo = output_xbins[i+1] # upper x value of output bin dxo = uxo - lxo # width of current x output bin # Scan input x bins to see if any fit in this output bin for j in range(0,M-1): lxi = input_xbins[j] # lower x value of input bin uxi = input_xbins[j+1] # upper x value of input bin dxi = uxi - lxi # width of current x input bin if uxi<lxo or lxi>uxo: # no bins are aligned continue elif lxi >= lxo and lxi < uxo: # start of an input bin occurs in this output bin if lxi >= lxo and uxi <= uxo: # input bin completely encompassed by output bin output_ybins[i] = output_ybins[i] + input_ybins[j] else: # input bin spans over at least one output bin # count fraction in current output x bin f_in_dxo = (uxo-lxi)/dxi output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j] elif lxi < lxo and uxi > uxo: # output bin is completely encompassed by input bin f_in_dxo = (uxo-lxo)/dxi output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j] elif lxi < lxo and uxi > lxo and uxi <= uxo: # tail of input bin is located in this output bin f_in_dxo = (uxi-lxo)/dxi output_ybins[i] = output_ybins[i] + f_in_dxo*input_ybins[j] return output_ybins
def autoplot_tally_results(tally_output_list, plot_errorbars=True, output_filename='results.pdf', additional_save_extensions=[], show_plots=False, return_fg_list=False, max_num_values_to_plot=10000000.0, rasterizesize_threshold=50000.0, rasterize_dpi=300)
-
Description
Generates visualizations/plots of the data in the output Pandas DataFrames from the
parse_tally_output_file()
function in an automated fashion. Note that this function only seeks to accomplish exactly this. It is not a function for generating customized plots; it exists to automate creating visualizations of PHITS output using sets of predetermined rules and settings, principally for initial checking of results. Generally, it does not respect plotting-relevant settings provided to PHITS tallies (e.g.,samepage
,axis
,angel
, etc.), though it will use thetitle
parameter and the plot axis titles for ANGEL included in the .out files, which are influenced by some tally parameters, such asunit
andy-txt
. This function seeks to compile plots of results from one or multiple tallies into a single PDF file (and individual files in other image formats). The seaborn package's relplot function is used for generating these plots. This function is primarily intended to be called byparse_tally_output_file()
andparse_all_tally_output_in_dir()
. However, if you wish to make modifications to the automatically generated figures, you can use thereturn_fg_list=True
setting and apply your desired modifications to the returned FacetGrid objects. (This is demonstrated in the example distributed with PHITS Tools.)Dependencies
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm, SymLogNorm, Normalize
from matplotlib.backends.backend_pdf import PdfPages
Inputs
tally_output_list
= thetally_output
output from theparse_tally_output_file()
function, a string/Path object pointing to the pickle file of such output, or a list of such outputs or pickle filepaths.plot_errorbars
= (optional, D=True
, requirescalculate_absolute_errors=True
to have been set in theparse_tally_output_file()
call producing thetally_output
) Boolean determining if errorbars will be displayed in plots. Note that owing to shortcomings in the seaborn package's handling of error bars (i.e., not supporting externally calculated error bars) that a workaround has instead been implemented but is only functional for "line"-type and "2D"-type plots.output_filename
= (optional, D=results.pdf
) String or Path object designating the name/path where the PDF of plots will be saved.additional_save_extensions
= (optional, D=[]
) a list of strings of file extensions, e.g.,['.png', '.svg']
compatible with matplotlib.savefig to also save the plots as. (options include '.eps', '.jpg', '.jpeg', '.pdf', '.pgf', '.png', '.ps', '.raw', '.rgba', '.svg', '.svgz', '.tif', '.tiff', and '.webp'; seeplt.gcf().canvas.get_supported_filetypes()
)show_plots
= (optional, D=False
) Boolean denoting whether this function will make aplt.show()
call immediately before thereturn
statement.return_fg_list
= (optional, D=False
) Boolean denoting whether a list of the generated seaborn FacetGrid objects returned by the sns.relplot() calls should be returned by this function, allowing modification of the automatically generated plots. (Strictly speaking, the objects are copies of the FacetGrid objects made using the built-in copy.deepcopy() function.) IfFalse
, this function returnsNone
.max_num_values_to_plot
= (optional, D=1e7
) integer denoting the maximum number of data points to be plotted by this function for a single tally output, which, when exceeded, will cause the function to skip creating the plot†. The number of data points to be plotted is calculated as the product of the axis lengths of thetally_output['tally_data']
Numpy array (excluding the final axis for values/errors).rasterizesize_threshold
= (optional, D=5e4
) integer denoting the maximum number of data points to be plotted by this function for a single tally output before settingrasterized=True
to thesns.relplot()
calls generating plots‡. The number of data points to be plotted is calculated as the product of the axis lengths of thetally_output['tally_data']
Numpy array (excluding the final axis for values/errors).rasterize_dpi
= (optional, D=300
) integer denoting the DPI to be used in thesavefig()
calls when therasterizesize_threshold
is exceeded or whenadditional_save_extensions
is used to save a plot in a non-vectorized format.
Outputs
None
ifreturn_fg_list=False
(default) orfg_list
ifreturn_fg_list=True
; see description of thereturn_fg_list
input argument- (and the saved file(s) of plot(s) specified by
output_filename
)
Notes
All plots generated by this function will be one of three types:
- 'scatter' = a scatterplot (error bars not available)
- 'line' = a line plot (error bars available)
- 'pseudo 2D' = a 2D color image plot (relative errors displayed as an additional 2D plot)
How the plot type is chosen and structured is dependent on the number of plotting axes found and their types. The number of plotting axes is taken as the count of axes in the
tally_output['tally_data']
Numpy array of length greater than 1 (excluding the final axis for values/errors). Each axis is categorized as being either 'numerical' (energy, time, angle, radius, x, etc.) or 'categorical' (particle, region number, etc.) in nature. If all plotting axes are categorical in nature, then the plot type will be of 'scatter' type. Otherwise, the longest numerical axis is taken to be the horizontal plotting axis on either the 'line' or 'pseudo 2D' plot. If the next longest numerical axis has length >6 (or, in the event the total number of plot axes is 4 or greater, catergorical axes are also considered in this step), the plot type will be 'pseudo 2D' with that axis taken as the vertical axis; otherwise, the plot type will be 'line' with'value'
as the vertical axis.The seaborn relplot
hue
,style
,row
,col
, andsize
variables, loosely in that order, are assigned to the next longest axes in order of descending length (except for the 'pseudo 2D' plots, wherehue
is used for'value'
andstyle
andsize
are unused). For the 'scatter' and 'line' plots,hue
andstyle
are typically assigned to the same axis for better visual distinction.The 'pseudo 2D' plots are called "pseudo" here as they are still made with the seaborn replot function, which actually does not support making this type of plot. The 2D plots are achieved by using the matplotlib.markers "verts" functionality to make markers of the correct aspect ratio to tile together nicely when scaled up (or down) sufficiently in size
s
to form the illusion of a 2D colormap plot, hence the "pseudo" in the name.†This function also calculates the total number of values (bins) in the tally output to be plotted, the product of the axis lengths of the
tally_output['tally_data']
Numpy array (excluding the final axis for values/errors). If this value exceeds 10 million (controlled by themax_num_values_to_plot
input in this function), this function will skip attempting to make the plot. This limit is set to avoid potential crashes from insufficient memory in scenarios where these plots are unlikely to actually be desired anyways.‡Furthermore, if the total number of values exceeds 50 thousand (controlled by the
rasterizesize_threshold
input in this function), rasterization will be employed in the plotting area (passingrasterized = True
to thesns.relplot()
call) using a default DPI of 300 (controlled by therasterize_dpi
input in this function).Expand source code
def autoplot_tally_results(tally_output_list,plot_errorbars=True,output_filename='results.pdf', additional_save_extensions=[],show_plots=False,return_fg_list=False, max_num_values_to_plot=1e7,rasterizesize_threshold=5e4,rasterize_dpi=300): ''' Description: Generates visualizations/plots of the data in the output Pandas DataFrames from the `parse_tally_output_file()` function in an automated fashion. Note that this function only seeks to accomplish exactly this. It is not a function for generating customized plots; it exists to automate creating visualizations of PHITS output using sets of predetermined rules and settings, principally for initial checking of results. Generally, it does not respect plotting-relevant settings provided to PHITS tallies (e.g., `samepage`, `axis`, `angel`, etc.), though it will use the `title` parameter and the plot axis titles for ANGEL included in the .out files, which are influenced by some tally parameters, such as `unit` and `y-txt`. This function seeks to compile plots of results from one or multiple tallies into a single PDF file (and individual files in other image formats). The [seaborn](https://seaborn.pydata.org/) package's [relplot](https://seaborn.pydata.org/generated/seaborn.relplot.html) function is used for generating these plots. This function is primarily intended to be called by `parse_tally_output_file()` and `parse_all_tally_output_in_dir()`. However, if you wish to make modifications to the automatically generated figures, you can use the `return_fg_list=True` setting and apply your desired modifications to the returned FacetGrid objects. (This is demonstrated in the [example](https://github.com/Lindt8/PHITS-Tools/tree/main/example) distributed with PHITS Tools.) Dependencies: - `import seaborn as sns` - `import pandas as pd` - `import matplotlib.pyplot as plt` - `from matplotlib.colors import LogNorm, SymLogNorm, Normalize` - `from matplotlib.backends.backend_pdf import PdfPages` Inputs: - `tally_output_list` = the `tally_output` output from the `parse_tally_output_file()` function, a string/Path object pointing to the pickle file of such output, or a list of such outputs or pickle filepaths. - `plot_errorbars` = (optional, D=`True`, requires `calculate_absolute_errors=True` to have been set in the `parse_tally_output_file()` call producing the `tally_output`) Boolean determining if errorbars will be displayed in plots. Note that owing to shortcomings in the [seaborn](https://seaborn.pydata.org/) package's handling of error bars (i.e., not supporting externally calculated error bars) that a workaround has instead been implemented but is only functional for "line"-type and "2D"-type plots. - `output_filename` = (optional, D=`results.pdf`) String or Path object designating the name/path where the PDF of plots will be saved. - `additional_save_extensions` = (optional, D=`[]`) a list of strings of file extensions, e.g., `['.png', '.svg']` compatible with [matplotlib.savefig](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html) to also save the plots as. (options include '.eps', '.jpg', '.jpeg', '.pdf', '.pgf', '.png', '.ps', '.raw', '.rgba', '.svg', '.svgz', '.tif', '.tiff', and '.webp'; see `plt.gcf().canvas.get_supported_filetypes()`) - `show_plots` = (optional, D=`False`) Boolean denoting whether this function will make a `plt.show()` call immediately before the `return` statement. - `return_fg_list` = (optional, D=`False`) Boolean denoting whether a list of the generated seaborn FacetGrid objects returned by the sns.relplot() calls should be returned by this function, allowing modification of the automatically generated plots. (Strictly speaking, the objects are copies of the FacetGrid objects made using the built-in [copy.deepcopy()](https://docs.python.org/3/library/copy.html#copy.deepcopy) function.) If `False`, this function returns `None`. - `max_num_values_to_plot` = (optional, D=`1e7`) integer denoting the maximum number of data points to be plotted by this function for a single tally output, which, when exceeded, will cause the function to skip creating the plot†. The number of data points to be plotted is calculated as the product of the axis lengths of the `tally_output['tally_data']` Numpy array (excluding the final axis for values/errors). - `rasterizesize_threshold` = (optional, D=`5e4`) integer denoting the maximum number of data points to be plotted by this function for a single tally output before setting `rasterized=True` to the `sns.relplot()` calls generating plots‡. The number of data points to be plotted is calculated as the product of the axis lengths of the `tally_output['tally_data']` Numpy array (excluding the final axis for values/errors). - `rasterize_dpi` = (optional, D=`300`) integer denoting the DPI to be used in the `savefig()` calls when the `rasterizesize_threshold` is exceeded or when `additional_save_extensions` is used to save a plot in a non-vectorized format. Outputs: - `None` if `return_fg_list=False` (default) or `fg_list` if `return_fg_list=True`; see description of the `return_fg_list` input argument - (and the saved file(s) of plot(s) specified by `output_filename`) -------- Notes: All plots generated by this function will be one of three types: - 'scatter' = a scatterplot (error bars not available) - 'line' = a line plot (error bars available) - 'pseudo 2D' = a 2D color image plot (relative errors displayed as an additional 2D plot) How the plot type is chosen and structured is dependent on the number of plotting axes found and their types. The number of plotting axes is taken as the count of axes in the `tally_output['tally_data']` Numpy array of length greater than 1 (excluding the final axis for values/errors). Each axis is categorized as being either 'numerical' (energy, time, angle, radius, x, etc.) or 'categorical' (particle, region number, etc.) in nature. If all plotting axes are categorical in nature, then the plot type will be of 'scatter' type. Otherwise, the longest numerical axis is taken to be the horizontal plotting axis on either the 'line' or 'pseudo 2D' plot. If the next longest numerical axis has length >6 (or, in the event the total number of plot axes is 4 or greater, catergorical axes are also considered in this step), the plot type will be 'pseudo 2D' with that axis taken as the vertical axis; otherwise, the plot type will be 'line' with `'value'` as the vertical axis. The seaborn relplot `hue`, `style`, `row`, `col`, and `size` variables, loosely in that order, are assigned to the next longest axes in order of descending length (except for the 'pseudo 2D' plots, where `hue` is used for `'value'` and `style` and `size` are unused). For the 'scatter' and 'line' plots, `hue` and `style` are typically assigned to the same axis for better visual distinction. The 'pseudo 2D' plots are called "pseudo" here as they are still made with the seaborn replot function, which actually does not support making this type of plot. The 2D plots are achieved by using the [matplotlib.markers "verts"](https://matplotlib.org/stable/api/markers_api.html) functionality to make markers of the correct aspect ratio to tile together nicely when scaled up (or down) sufficiently in size `s` to form the illusion of a 2D colormap plot, hence the "pseudo" in the name. †This function also calculates the total number of values (bins) in the tally output to be plotted, the product of the axis lengths of the `tally_output['tally_data']` Numpy array (excluding the final axis for values/errors). If this value exceeds 10 million (controlled by the `max_num_values_to_plot` input in this function), this function will skip attempting to make the plot. This limit is set to avoid potential crashes from insufficient memory in scenarios where these plots are unlikely to actually be desired anyways. ‡Furthermore, if the total number of values exceeds 50 thousand (controlled by the `rasterizesize_threshold` input in this function), rasterization will be employed in the plotting area (passing `rasterized = True` to the `sns.relplot()` call) using a default DPI of 300 (controlled by the `rasterize_dpi` input in this function). ''' ''' Behavior of this function within PHITS Tools: - The option to execute this function on each produced output should be available for both `parse_tally_output_file()` and `parse_all_tally_output_in_dir()` (one PDF file per tally output file) - Also for `parse_all_tally_output_in_dir()` should be an option to execute this function on ALL tally outputs handled by the dir parsing function (combining all plots into a single PDF) - By default, both of these options should be disabled (set False) by default when PHITS Tools is used as an imported module. - For GUI/CLI usage, checkboxes/flags for these three options should appear - For GUI/CLI usage of `parse_tally_output_file()`, use of this function should be enabled/disabled by default. - For GUI/CLI usage of `parse_all_tally_output_in_dir()`, use of this function should be enabled/disabled by default only for combining all output in a directory; making individual PDFs of each handled tally output should be disabled/disabled by default. ''' import seaborn as sns import matplotlib.pyplot as plt from matplotlib.colors import LogNorm, SymLogNorm, Normalize from matplotlib.backends.backend_pdf import PdfPages import pickle, lzma import datetime import copy #max_num_values_to_plot = 1e7 # if the number of data points to plot exceeds this, plotting is skipped for that tally #rasterizesize_threshold = 5e4 # use "rasterize" option if the number of data points to be plotted exceeds this #rasterize_dpi = 300 # if using rasterization, use this DPI fig_aspect_ratio = 1.414 # width / height fig_height = 4 # inches figi = 10000 # figure number fg_list = [] # list of generated Seaborn FacetGrid objects returned by the sns.relplot() calls output_filename = Path(output_filename) # Convert provided input to a list of `tally_output` dictionary objects if isinstance(tally_output_list, dict): # single tally output, tally output object tally_output_list = [tally_output_list] elif not isinstance(tally_output_list, list): # single tally output, pickle file of tally output object p = Path(tally_output_list) tally_output = pickle.load(lzma.open(p, 'rb') if p.name[-3:] == '.xz' else open(p, 'rb')) tally_output_list = [tally_output] else: # list of tally output objects and/or pickles for i, to in enumerate(tally_output_list): # open any pickle files provided if not isinstance(to, dict): if to is None: print("WARNING: 'None' tally output encountered in tally_output_list in autoplot_tally_results()!") continue p = Path(to) tally_output_list[i] = pickle.load(lzma.open(p, 'rb') if p.name[-3:] == '.xz' else open(p, 'rb')) plotting_multiple_tallies = len(tally_output_list) > 1 def are_bins_linearly_spaced(bin_mids): ''' Return True/False designating whether bin centers appear to be linearly spaced or not ''' diff = np.diff(bin_mids).round(decimals=8) vals, counts = np.unique(diff, return_counts=True) counts, vals = zip(*sorted(zip(counts, vals), reverse=True)) if 0 in vals: izero = vals.index(0) counts, vals = list(counts), list(vals) counts.pop(izero) vals.pop(izero) if len(vals) <= 1: # all diff values are the same bins_are_lin_spaced = True else: if counts[0]/sum(counts) > 0.5: # most frequent difference is more than half of all differences bins_are_lin_spaced = True else: bins_are_lin_spaced = False return bins_are_lin_spaced tex_replace_strs = {'^2':'$^2$', '^3':'$^3$', r'\sigma':r'$\sigma$', r'\Omega':r'$\Omega$'} with PdfPages(output_filename) as pdf: for toi, tally_output in enumerate(tally_output_list): tally_data, tally_metadata, tally_dataframe = [tally_output[k] for k in tally_output.keys()] # determine number of plots necessary ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tally_data) if ierr_max==2 or ierr_max==4: plot_errorbars = False special_tcross_case = False if ierr_max>3: special_tcross_case = True if special_tcross_case: df_cols = tally_dataframe.columns.values.tolist() #print(df_cols) df1 = tally_dataframe.copy().drop(['r_surf', 'z_mid', 'value2', 'rel.err.2', 'abs.err.2'], axis=1) df2 = tally_dataframe.copy().drop(['r_mid', 'z_surf', 'value', 'rel.err.', 'abs.err.'], axis=1).rename(columns={'value2':'value', 'rel.err.2':'rel.err.', 'abs.err.2':'abs.err.'}) #print(df1.columns.values.tolist()) #print(df2.columns.values.tolist()) tally_df_list = [df1, df2] extra_df_fname_text = ['_rsurf-zmid', '_rmid-zsurf'] else: tally_df_list = [tally_dataframe] extra_df_fname_text = [''] for tdfi, tally_df in enumerate(tally_df_list): df_cols = tally_df.columns.values.tolist() array_axes_lens = [ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max] tot_plot_axes = sum(1 for i in array_axes_lens if i > 1) tot_num_values = np.prod(array_axes_lens) if tot_num_values > max_num_values_to_plot: print('WARNING: Tally output for ',tally_metadata['file'],' is VERY LARGE (',tot_num_values,' elements), deemed too large for automatic plotting.') if return_fg_list: fg_list.append(None) continue if tot_num_values > rasterizesize_threshold: use_rasterization = True else: use_rasterization = False if tot_plot_axes==0: # case where the tally is only scoring a single value fig = plt.figure(figi,(fig_aspect_ratio*fig_height,fig_height)) plt.errorbar(1,tally_data[0,0,0,0,0,0,0,0,0,0],yerr=tally_data[0,0,0,0,0,0,0,0,0,2],marker='o') plt.title(tally_metadata['tally_type'] + ', ' + tally_metadata['file'] + '\n' + tally_metadata['title']) plt.ylabel('value') plt.tight_layout() # add PHITS Tools info fontdict = {'color': '#666666', 'weight': 'normal', 'size': 8, 'style': 'italic'} fig.text(0.005, 0.005, 'Figure generated by PHITS Tools, github.com/Lindt8/PHITS-Tools', fontdict=fontdict, ha='left', va='bottom', url='https://github.com/Lindt8/PHITS-Tools') pdf.savefig() if return_fg_list: fg_list.append(copy.deepcopy(fig)) if not show_plots: plt.close() figi += 1 continue # We can divide axes into two categories: # - categorical: 'reg', 'tet', 'point#', 'ring#', 'particle','nuclide', 'ZZZAAAM' # - numerical: 'r_mid', 'r_surf', 'x_mid', 'y_mid', 'z_surf', 'z_mid', 'e_mid', 't_mid', 'a_mid', 'LET_mid', 'ic/Z/charge', 'ic/A/mass' plot_axes = [] plot_axes_lens = [] plot_axes_ivars = [] num_axes = [] cat_axes = [] # assign all possible dimensions a "priority" score to compare for tiebreakers (dimensions of equal length) ax_priority_vals = {'reg': 100, 'tet': 100, 'point#': 101, 'ring#': 101, 'r_mid': 100, 'r_surf': 101, 'x_mid': 100, 'y_mid': 90, 'z_mid': 80, 'z_surf': 81, 'e_mid': 110, 'e1_mid': 111, 't_mid': 70, 'a_mid': 60, 'LET_mid': 109, 'particle': 95, 'nuclide': 20, 'ZZZAAAM': 19, 'ic/Z/charge': 30, 'ic/A/mass': 30, '#Interactions': 10, 'e2_mid': 109 } cat_ax_to_index = {'reg': 'ir', 'tet': 'ir', 'point#': 'ir', 'ring#': 'ir', 'particle': 'ip', 'nuclide': 'ic', 'ZZZAAAM': 'ic' } # For the sake of making the plot labels nicer, also determine appropriate label for each axis a_units = 'Angle [degrees]' if 'a-type' in tally_metadata: if tally_metadata['a-type']>0: a_units = 'cos(Angle)' elif tally_metadata['axis'] == 'rad': a_units = 'Angle [radians]' value_label_from_phits = 'value' if tally_metadata['axis_dimensions'] == 1: if tally_metadata['value_label'] != '': value_label_from_phits = tally_metadata['value_label'] elif tally_metadata['axis2_label'] != '': value_label_from_phits = tally_metadata['axis2_label'] else: if tally_metadata['value_label'] != '': value_label_from_phits = tally_metadata['value_label'] if value_label_from_phits != 'value': for itx in tex_replace_strs: value_label_from_phits = r'{}'.format(value_label_from_phits.replace(itx,tex_replace_strs[itx])) ax_labels = {'reg':'Region #', 'tet':'Tet #', 'point#':'Point det #', 'ring#':'Ring det #', 'r_mid':'r [cm]', 'r_surf':'r_surface [cm]', 'x_mid':'x [cm]', 'y_mid':'y [cm]', 'z_mid':'z [cm]', 'z_surf':'z_surface [cm]', 'e_mid':'Energy [MeV]', 'e1_mid':'Energy 1 [MeV]', 't_mid':'Time [ns]', 'a_mid':a_units, 'LET_mid':r'LET [keV/$\mu$m]', 'particle':'Particle', 'nuclide':'Nuclide', 'ZZZAAAM':'ZZZAAAM', 'ic/Z/charge':'Z (proton #)', 'ic/A/mass':'A (mass #)', '#Interactions':'Number of interactions', 'e2_mid':'Energy 2 [MeV]', 'value':value_label_from_phits, 'value2':value_label_from_phits, 'rel.err.':'relative error', 'rel.err.2':'relative error' } # now determine what axes are getting plotted for i, leni in enumerate(array_axes_lens): if leni==1: continue if i==0: # 'reg', 'tet', 'point#', 'ring#', 'r_mid', 'r_surf', 'x_mid' if any(a in ['reg', 'point#', 'ring#'] for a in df_cols): # categorical col = [a for a in ['reg', 'point#', 'ring#'] if a in df_cols][0] cat_axes.append(col) else: # numerical col = [a for a in ['r_mid', 'r_surf', 'x_mid', 'tet'] if a in df_cols][0] num_axes.append(col) if col=='tet': # convert tet number from string to float tally_df['tet'] = tally_df['tet'].astype(float) elif i==1: # 'y_mid' col = 'y_mid' num_axes.append(col) elif i==2: # 'z_mid', 'z_surf' col = [a for a in ['z_mid', 'z_surf'] if a in df_cols][0] num_axes.append(col) elif i==3: # 'e_mid' col = [a for a in ['e_mid', 'e1_mid'] if a in df_cols][0] num_axes.append(col) elif i==4: # 't_mid' col = 't_mid' num_axes.append(col) elif i==5: # 'a_mid' col = 'a_mid' num_axes.append(col) elif i==6: # 'LET_mid' col = 'LET_mid' num_axes.append(col) elif i==7: # 'particle' col = 'particle' cat_axes.append(col) elif i==8: # 'nuclide', 'ZZZAAAM', 'ic/Z/charge', 'ic/A/mass', 'act' if any(a in ['nuclide', 'ZZZAAAM'] for a in df_cols): # categorical col = [a for a in ['nuclide', 'ZZZAAAM'] if a in df_cols][0] cat_axes.append(col) else: # numerical col = [a for a in ['ic/Z/charge', 'ic/A/mass', '#Interactions', 'e2_mid'] if a in df_cols][0] num_axes.append(col) plot_axes.append(col) plot_axes_lens.append(leni) # sort the lists in order of descending lengths, with "higher priority" axes coming first when equal in length plot_axes_sorted = [] plot_axes_lens_sorted = [] num_axes_sorted = [] num_lens_sorted = [] cat_axes_sorted = [] cat_lens_sorted = [] ax_props = [(plot_axes[i],plot_axes_lens[i],ax_priority_vals[plot_axes[i]]) for i in range(len(plot_axes))] ax_props.sort(key=lambda x: (-x[1], -x[2])) # sort first by length then priority, both in descending order for i,tup in enumerate(ax_props): plot_axes_sorted.append(tup[0]) plot_axes_lens_sorted.append(tup[1]) if tup[0] in cat_axes: cat_axes_sorted.append(tup[0]) cat_lens_sorted.append(tup[1]) else: num_axes_sorted.append(tup[0]) num_lens_sorted.append(tup[1]) cat = cat_axes_sorted num = num_axes_sorted # Now determine how each axis will be represented in the plot ''' How we represent the data will depend on: - how many axes there are to represent - the specific combination of A cat and B num - whether the longest axes are cat or num For the total number axes variables (r, y, z, e, t, a, l, p, and c) with length > 1: [unless stated otherwise, y='value' for all options] [charts become difficult to read if length of axis passed to hue and (especially) style are too long; therefore, maxlen = 6 is set, meaning if this length is exceeded for an axis bound for hue/style, instead it will be split in row/col and/or cause a shift from a 1D line plot to a 2D scatter plot.] - 0 : set x to 1, y to 'value' - 1 : 1 cat + 0 num : scatter(line?) : x = i_cat1, hue=style=cat1 - 1 : 0 cat + 1 num : line : x = num1 - 2 : 2 cat + 0 num : scatter(line?) : x = i_cat1, hue=cat1, style=cat2 - 2 : 1 cat + 1 num : line : x = num1, hue=style=cat1 - 2 : 0 cat + 2 num : line : x = num1, hue=style=num2 (suitable if len(num2) <= maxlen) OR : scatter : x = num1, y = num2, hue='value' (increase marker size for pseudo 2D plot) - 3 : 3 cat + 0 num : scatter(line?) : x = i_cat1, hue=cat1, style=cat2, row=cat3 (note: this scenario is extremely unlikely to happen) - 3 : 2 cat + 1 num : line : x = num1, hue=cat1, style=cat2 - 3 : 1 cat + 2 num : line : x = num1, hue=num2, row=cat1 (suitable if len(num2) <= maxlen) OR : scatter : x = num1, y = num2, hue='value', row=cat1 - 3 : 0 cat + 3 num : line : x = num1, hue=num2, row=num3 (suitable if len(num2) <= maxlen) OR : scatter : x = num1, y = num2, hue='value', row=num3 - 4 : 3 cat + 1 num : line : x = num1, hue=cat1, style=cat2, row=cat3 - 4 : 2 cat + 2 num : line : x = num1, hue=num2, style=cat1, row=cat2 (suitable if len(num2) <= maxlen and len(cat1) <= maxlen) OR : line : x = num1, hue=num2, row=cat1, col=cat2 (if len(num2) <= maxlen but len(cat1) > maxlen) OR : scatter : x = num1, y = num2, hue='value', row=cat1, col=cat2 - 4 : 1 cat + 3 num : line : x = num1, hue=num2, style=cat1, row=num3 (suitable if len(num2) <= maxlen and len(cat1) <= maxlen) OR : line : x = num1, hue=num2, row=num3, col=cat1 (if len(num2) <= maxlen but len(cat1) > maxlen) OR : scatter : x = num1, y = num2, hue='value', row=num3, col=cat1 - 4 : 0 cat + 4 num : line : x = num1, hue=num2, style=num3, row=num4 (suitable if len(num2) <= maxlen and len(num3) <= maxlen) OR : line : x = num1, hue=num2, row=num3, col=num4 (if len(num2) <= maxlen but len(num3) > maxlen) OR : scatter : x = num1, y = num2, hue='value', row=num3, col=num4 - 5 : any 5 cat/num : line : x = num1, hue=num2, style=num5, row=num3, col=num4 - 6 : any 6 cat/num : line : x = num1, hue=num2, style=num5, size=num6, row=num3, col=num4 - 7+ : no plot will be generated, output will be skipped. If you have a tally this complex, you already have usage ideas. ''' maxlen = 6 # if an axis length exceeds this, it may need to be reassigned to row/col or the plot type changed maxlines = 16 # will modify plotting strategy if combining two axes on a line plot results in more lines than this plot_kind = 'line' # 'line' or 'scatter' y_var = 'value' x_var = None hue_var = None style_var = None size_var = None row_var = None col_var = None pseudo_2d_plot = False xvar_original_label = None yvar_original_label = 'value' if tot_plot_axes==0: pass elif tot_plot_axes==1: if len(cat)==1: # 1 cat + 0 num plot_kind = 'scatter' x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) hue_var = cat[0] style_var = cat[0] else: # 0 cat + 1 num plot_kind = 'line' x_var = num[0] elif tot_plot_axes==2: if len(cat)==2: # 2 cat + 0 num plot_kind = 'scatter' x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) hue_var = cat[0] style_var = cat[1] elif len(cat)==1: # 1 cat + 1 num plot_kind = 'line' x_var = num[0] hue_var = cat[0] style_var = cat[0] elif len(num)==2: # 0 cat + 2 num if num_lens_sorted[1] <= maxlen: plot_kind = 'line' x_var = num[0] hue_var = num[1] style_var = num[1] else: pseudo_2d_plot = True plot_kind = 'scatter' x_var = num[0] y_var = num[1] hue_var = 'value' elif tot_plot_axes==3: if len(cat) == 3: # 3 cat + 0 num plot_kind = 'scatter' x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) hue_var = cat[0] style_var = cat[1] row_var = cat[2] elif len(cat) == 2: # 2 cat + 1 num plot_kind = 'line' x_var = num[0] hue_var = cat[0] style_var = cat[1] elif len(cat) == 1: # 1 cat + 2 num if num_lens_sorted[1] <= maxlen: plot_kind = 'line' x_var = num[0] hue_var = num[1] style_var = num[1] row_var = cat[0] else: pseudo_2d_plot = True plot_kind = 'scatter' x_var = num[0] y_var = num[1] hue_var = 'value' row_var = cat[0] elif len(num) == 3: # 0 cat + 3 num if num_lens_sorted[1] <= maxlen: plot_kind = 'line' x_var = num[0] hue_var = num[1] style_var = num[1] row_var = num[2] else: pseudo_2d_plot = True plot_kind = 'scatter' x_var = num[0] y_var = num[1] hue_var = 'value' row_var = num[2] ''' if plot_axes_sorted[0] in num: x_var = plot_axes_sorted[0] else: x_var = plot_axes_sorted[0] #x_var = cat_ax_to_index[cat_axes_sorted[0]] #cat.append(x_var) style_var = plot_axes_sorted[0] if plot_axes_lens_sorted[1] <= maxlen: plot_kind = 'line' hue_var = plot_axes_sorted[1] if plot_axes_lens_sorted[1]*plot_axes_lens_sorted[2] <= maxlines: style_var = plot_axes_sorted[2] else: if style_var==None: style_var = plot_axes_sorted[1] row_var = plot_axes_sorted[2] else: pseudo_2d_plot = True plot_kind = 'scatter' if plot_axes_sorted[1] in num: y_var = plot_axes_sorted[1] else: y_var = cat_ax_to_index[cat_axes_sorted[1]] cat.append(y_var) hue_var = 'value' row_var = plot_axes_sorted[2] ''' elif tot_plot_axes==4: if plot_axes_sorted[0] in num: x_var = plot_axes_sorted[0] else: x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) style_var = plot_axes_sorted[0] if plot_axes_lens_sorted[1] <= maxlen: plot_kind = 'line' hue_var = plot_axes_sorted[1] if style_var==None: style_var = plot_axes_sorted[1] row_var = plot_axes_sorted[2] col_var = plot_axes_sorted[3] else: pseudo_2d_plot = True plot_kind = 'scatter' if plot_axes_sorted[1] in num: y_var = plot_axes_sorted[1] else: y_var = cat_ax_to_index[cat_axes_sorted[1]] yvar_original_label = cat_axes_sorted[1] cat.append(y_var) hue_var = 'value' row_var = plot_axes_sorted[2] col_var = plot_axes_sorted[3] elif tot_plot_axes==5: plot_kind = 'line' if plot_axes_sorted[0] in num: x_var = plot_axes_sorted[0] else: x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) hue_var = plot_axes_sorted[1] row_var = plot_axes_sorted[2] col_var = plot_axes_sorted[3] style_var = plot_axes_sorted[4] elif tot_plot_axes==6: plot_kind = 'line' if plot_axes_sorted[0] in num: x_var = plot_axes_sorted[0] else: x_var = cat_ax_to_index[cat_axes_sorted[0]] xvar_original_label = cat_axes_sorted[0] cat.append(x_var) hue_var = plot_axes_sorted[1] row_var = plot_axes_sorted[2] col_var = plot_axes_sorted[3] style_var = plot_axes_sorted[4] size_var = plot_axes_sorted[5] else: print('Cannot create plot with 7+ variables...') continue ''' listed below are all possible column headers in the dataframe 'ir','reg','reg#', 'tet', 'point#', 'ring#' 'ip', 'particle', 'kf-code' 'ix', 'iy', 'iz', 'x_mid', 'y_mid', 'z_surf', 'z_mid' 'ir', 'r_mid', 'r_surf', 'ie','e_mid', 'it', 't_mid', 'ia', 'a_mid', 'il', 'LET_mid', 'e1_mid' 'ic', 'nuclide', 'ZZZAAAM', 'ic/Z/charge', 'ic/A/mass', '#Interactions', 'e2_mid' 'value', 'rel.err.', 'abs.err.' 'value2', 'rel.err.2', 'abs.err.2' ''' for ierr in range(2): # for pseudo 2d plots, also make a rel.err. plot if ierr==1 and not pseudo_2d_plot: continue extra_fname_text = extra_df_fname_text[tdfi] + ['','_err'][ierr] if plotting_multiple_tallies: print('\tplotting:',tally_metadata['file'], extra_fname_text) if ierr==0: hue_palette_name_str = "mako_r" # "cividis" # "rocket_r" else: hue_palette_name_str = "Reds" # "cividis" # "rocket_r" if hue_var=='value': hue_var = 'rel.err.' elif hue_var=='value2': hue_var = 'rel.err.2' else: continue if x_var in cat: plot_kind='scatter' if plot_kind=='scatter': plot_errorbars = False if plot_errorbars: # The following 3 lines are the "hack" to circumvent seaborn stupidly not supporting custom error values duplicates = 100 df = tally_df.loc[tally_df.index.repeat(duplicates)].copy() df['value'] = np.random.normal(df['value'].values, df['abs.err.'].values) errorbar_arg = 'sd' else: df = tally_df.copy() errorbar_arg = None if ierr==1 and pseudo_2d_plot: df[hue_var] = df[hue_var].replace({'0': '1', 0: 1.0}) # Determine lin/log/symlog usage for axes, with the following rules: # - default to linear # - if axis is already evenly linearly binned, keep it linear # - otherwise, log scale if max/min > maxratio, maxratio = 101 # - if log but dataset contains is not of all same sign , use symlog x_norm, y_norm = 'linear', 'linear' hue_norm, size_norm = None, None maxratio = 101 if x_var not in cat: x_min, x_max = df[x_var].abs().replace(0, np.nan).min(), df[x_var].abs().replace(0, np.nan).max() bins_are_lin_spaced = are_bins_linearly_spaced(df[x_var].to_numpy()) if not bins_are_lin_spaced and x_max/x_min>maxratio: if len(np.sign(df[x_var]).unique())>1: x_norm = 'log' #'symlog' else: x_norm = 'log' if y_var not in cat: y_min, y_max = df[y_var].abs().replace(0, np.nan).min(), df[y_var].abs().replace(0, np.nan).max() bins_are_lin_spaced = are_bins_linearly_spaced(df[y_var].to_numpy()) if not bins_are_lin_spaced and y_max/y_min>maxratio: if len(np.sign(df[y_var]).unique())>1: y_norm = 'log' # 'symlog' else: y_norm = 'log' use_colorbar = False if hue_var != None: if hue_var not in cat: hue_min, hue_max = df[hue_var].abs().replace(0, np.nan).min(), df[hue_var].abs().replace(0, np.nan).max() bins_are_lin_spaced = are_bins_linearly_spaced(df[hue_var].to_numpy()) if not bins_are_lin_spaced and hue_max/hue_min>maxratio: if len(np.sign(df[hue_var]).unique())>1: hue_norm = SymLogNorm(linthresh=hue_min) else: hue_norm = LogNorm() if pseudo_2d_plot: use_colorbar = True if hue_norm == None: hue_norm = Normalize() if ierr == 1: hue_norm = Normalize(vmin=0, vmax=1) cmap = sns.color_palette(hue_palette_name_str, as_cmap=True) sm = plt.cm.ScalarMappable(cmap=cmap, norm=hue_norm) if size_var != None and size_var not in cat: size_min, size_max = df[size_var].abs().replace(0, np.nan).min(), df[size_var].abs().replace(0,np.nan).max() bins_are_lin_spaced = are_bins_linearly_spaced(df[size_var].to_numpy()) if not bins_are_lin_spaced and size_max/size_min>maxratio: if len(np.sign(df[size_var]).unique())>1: size_norm = SymLogNorm(linthresh=size_min) else: size_norm = LogNorm() # Right before plotting, let's rename Pandas DF columns to be nicer on plots y_var_renamed = 'value' x_var_renamed = None hue_var_renamed = None style_var_renamed = None size_var_renamed = None row_var_renamed = None col_var_renamed = None df_renamed = df.rename(columns=ax_labels) if xvar_original_label != None: x_var_new = ax_labels[xvar_original_label] + ' index' df_renamed = df_renamed.rename(columns={x_var:x_var_new}) x_var_renamed = x_var_new else: x_var_renamed = ax_labels[x_var] if yvar_original_label != 'value': y_var_new = ax_labels[yvar_original_label] + ' index' df_renamed = df_renamed.rename(columns={y_var:y_var_new}) y_var_renamed = y_var_new else: y_var_renamed = ax_labels[y_var] if hue_var != None: hue_var_renamed = ax_labels[hue_var] if style_var != None: style_var_renamed = ax_labels[style_var] if size_var != None: size_var_renamed = ax_labels[size_var] if row_var != None: row_var_renamed = ax_labels[row_var] if col_var != None: col_var_renamed = ax_labels[col_var] if in_debug_mode: print('plot_kind=', plot_kind, '\ny_var=', y_var, y_var_renamed, '\nx_var=', x_var, x_var_renamed, '\nhue_var=', hue_var, hue_var_renamed, '\nhue_norm=', hue_norm, '\nstyle_var=', style_var, style_var_renamed, '\nsize_var=', size_var, size_var_renamed, '\nrow_var=', row_var, row_var_renamed, '\ncol_var=', col_var, col_var_renamed, '\npseudo_2d_plot=', pseudo_2d_plot) print(df_renamed.columns.values) if plot_kind == 'line': fg = sns.relplot(data=df_renamed, kind=plot_kind, #num=figi, height=fig_height, aspect=fig_aspect_ratio, x=x_var_renamed, y=y_var_renamed, hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed, hue_norm=hue_norm, size_norm=size_norm, row=row_var_renamed, col=col_var_renamed, errorbar=errorbar_arg, legend='auto', # markers=False, rasterized = use_rasterization, facet_kws={"legend_out": True, } ) else: # scatterplot if pseudo_2d_plot: num_cols = df_renamed[x_var_renamed].nunique() num_rows = df_renamed[y_var_renamed].nunique() # using horizontal direction #fig_size_inch = fig_height * fig_aspect_ratio #margin = 0.05 # 0.12 #n_markers = num_cols #subplot_fraction = 0.7696385 # for a normal plot w/o rows/cols, widths are: fig=624.475, ax=535.807 (pre adjust) / 480.62 (post adjust), leg=58.875 if use_colorbar: leg_arg = False # using vertical direction # these values are decent but could perhaps use better optimization fig_size_inch = fig_height margin = 0.06 # 0.2 n_markers = num_rows subplot_fraction = 1 - 2 * margin ar_ax_vs_fig_correction = 0.95 else: leg_arg = 'auto' # using vertical direction fig_size_inch = fig_height margin = 0.185 # 0.2 n_markers = num_rows subplot_fraction = 1 - 2 * margin ar_ax_vs_fig_correction = 1.3 ar_if_square = num_rows / num_cols mar = fig_aspect_ratio * ar_if_square * ar_ax_vs_fig_correction # marker aspect ratio, width/height verts = [[-mar, -1], [mar, -1], [mar, 1], [-mar, 1], [-mar, -1]] fig_ppi = 72 # 300 is default save dpi, different from draw dpi marker_size = (subplot_fraction * fig_size_inch * fig_ppi / n_markers) ** 2 # Size of the marker, in points^2 s = marker_size # universal marker size for plot if num_rows <= 15: # adjust vertical label spacing in legend, if needed, before plotting sns.set(style=None, rc={'legend.labelspacing': 1.2}) fg = sns.relplot(data=df_renamed, kind=plot_kind, # num=figi, height=fig_height, aspect=fig_aspect_ratio, x=x_var_renamed, y=y_var_renamed, s=s, linewidth=0, #edgecolor=None, hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed, hue_norm=hue_norm, size_norm=size_norm, row=row_var_renamed, col=col_var_renamed, legend=leg_arg, palette=hue_palette_name_str, marker=verts, alpha=1, rasterized = use_rasterization, #facet_kws={"legend_out": True, } ) #fg.set(aspect='equal') #cbar = plt.colorbar(sm, ax=plt.gca()) #cbar = fg.fig.colorbar(sm, ax=fg.axes[:, -1]) for col in range(np.shape(fg.axes[:,:])[1]): for row in range(np.shape(fg.axes[:,:])[0]): cbar = plt.colorbar(sm, ax=fg.axes[row,col]) cbar.set_label(hue_var_renamed) else: fg = sns.relplot(data=df_renamed, kind=plot_kind, # num=figi, height=fig_height, aspect=fig_aspect_ratio, x=x_var_renamed, y=y_var_renamed, hue=hue_var_renamed, style=style_var_renamed, size=size_var_renamed, hue_norm=hue_norm, size_norm=size_norm, row=row_var_renamed, col=col_var_renamed, legend='auto', s=100, alpha=1, rasterized = use_rasterization, facet_kws={"legend_out": True, } ) fig, axes = fg.fig, fg.axes legend_exists = not all(v is None for v in [hue_var, style_var, size_var, col_var, row_var]) if legend_exists and not pseudo_2d_plot: leg = fg._legend leg.set_bbox_to_anchor([1, 0.5]) # Move legend to far right leg._loc = 5 # https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html#matplotlib.axes.Axes.legend # later figure will be rescaled to accommodate the legend title_str = tally_metadata['tally_type'] + ', ' + tally_metadata['file'] + '\n' + tally_metadata['title'] #xlabel_str = tally_metadata['axis1_label'] #ylabel_str = tally_metadata['value_label'] st = fg.fig.suptitle(title_str, fontsize=16, va='top', y=0.995) #fg.fig.get_axes()[0].annotate(title_str, (0.5, 0.95), xycoords='figure fraction', ha='center', fontsize=16) #fg.set_axis_labels(xlabel_str, ylabel_str) for ax in fg.axes.flat: ax.set_xscale(x_norm) ax.set_yscale(y_norm) ax.grid(which='both', linewidth=1, color='#EEEEEE', alpha=0.5) #ax.set_facecolor((0, 0, 0, 0)) #ax.tick_params(labelbottom=True, labelleft=True) #ax.set_xlabel(xlabel_str, visible=True) #ax.set_ylabel(ylabel_str, visible=True) #sns.move_legend(fg, "upper left", bbox_to_anchor=(1, 1)) # Issue 1: tight_layout is needed to not have stuff clipped off edge # Issue 2: tight_layout causes the legend, if placed outside the plot area, to be cut out # Soltion: draw canvas early, figure out their relative widths, then adjust the right edge of the figure plt.tight_layout() # pad=3.0 if legend_exists and not pseudo_2d_plot: fig.canvas.draw() fig_width = fig.get_window_extent().width leg_width = leg.get_window_extent().width #print(fig_width,ax.get_window_extent().width,leg_width) fg.fig.subplots_adjust(right=0.98*(fig_width-leg_width)/fig_width) #print(fg.fig.get_window_extent().width, fg.ax.get_window_extent().width, leg.get_window_extent().width) #if pseudo_2d_plot: # fig.canvas.draw() # fig_width = fig.get_window_extent().width # #cbar_width = fg.fig.get_window_extent().width - fg.axes.get_window_extent().width # #fg.fig.subplots_adjust(right=0.98 * (fig_width - cbar_width) / fig_width) # fg.fig.subplots_adjust(right=0.98 * 0.8) # add PHITS Tools info fontdict = {'color':'#666666', 'weight':'normal', 'size': 8, 'style':'italic'} fig.text(0.005,0.005,'Figure generated by PHITS Tools, github.com/Lindt8/PHITS-Tools',fontdict=fontdict,ha='left', va='bottom', url='https://github.com/Lindt8/PHITS-Tools') if use_rasterization: pdf.savefig(bbox_extra_artists=[st], dpi=rasterize_dpi) else: pdf.savefig(bbox_extra_artists=[st]) if return_fg_list: fg_list.append(copy.deepcopy(fg)) for ext in additional_save_extensions: if '.' not in ext: ext = '.'+ext # assume user provides only the extension in event the . is missing if ext.lower()=='.pdf': ext = '_.pdf' img_save_path = Path(output_filename.parent, output_filename.stem + extra_fname_text + ext) plt.savefig(img_save_path, bbox_extra_artists=[st], dpi=rasterize_dpi) if not show_plots: plt.close(fg.fig) figi += 1 d = pdf.infodict() #d['Title'] = 'Multipage PDF Example' d['Author'] = 'PHITS Tools' d['Subject'] = 'https://github.com/Lindt8/PHITS-Tools/' #d['Keywords'] = 'PdfPages multipage keywords author title subject' d['CreationDate'] = datetime.datetime.today() d['ModDate'] = datetime.datetime.today() print('Plot PDF written:', output_filename, '\n') if show_plots: plt.show() if return_fg_list: return fg_list else: return None
def fetch_MC_material(matid=None, matname=None, matsource=None, concentration_type=None, particle=None)
-
Description
Returns a materials definition string formatted for use in PHITS or MCNP (including a density estimate); most available materials are those found in PNNL-15870 Rev. 1.
Dependencies
- PYTHONPATH environmental variable must be set and one entry must contain the directory which contains the vital "MC_materials/Compiled_MC_materials.pkl" file.
Inputs
(required to enter
matid
ORmatname
, withmatid
taking priority if conflicting)matid
= ID number in the "Compiled_MC_materials" filematname
= exact name of material in "Compiled_MC_materials" filematsource
= exact source of material in "Compiled_MC_materials" file, only used when multiple materials have identical namesconcentration_type
= selection between'weight fraction'
(default if no chemical formula is present in database; e.g., "Incoloy-800") and'atom fraction'
(default if a chemical formula is present; e.g. "Ethane" (Formula = C2H6)) to be returnedparticle
= selection of whether natural ('photons'
, default) or isotopic ('neutrons'
) elements are used Note that if "enriched" or "depleted" appears in the material's name, particle='neutrons'
is set automatically.
Outputs
mat_str
= string containing the material's information, ready to be inserted directly into a PHITS/MCNP input file
Expand source code
def fetch_MC_material(matid=None,matname=None,matsource=None,concentration_type=None,particle=None): ''' Description: Returns a materials definition string formatted for use in PHITS or MCNP (including a density estimate); most available materials are those found in [PNNL-15870 Rev. 1](https://www.osti.gov/biblio/1023125). Dependencies: - PYTHONPATH environmental variable must be set and one entry must contain the directory which contains the vital "MC_materials/Compiled_MC_materials.pkl" file. Inputs: (required to enter `matid` OR `matname`, with `matid` taking priority if conflicting) - `matid` = ID number in the "Compiled_MC_materials" file - `matname` = exact name of material in "Compiled_MC_materials" file - `matsource` = exact source of material in "Compiled_MC_materials" file, only used when multiple materials have identical names - `concentration_type` = selection between `'weight fraction'` (default if no chemical formula is present in database; e.g., "Incoloy-800") and `'atom fraction'` (default if a chemical formula is present; e.g. "Ethane" (Formula = C2H6)) to be returned - `particle` = selection of whether natural (`'photons'`, default) or isotopic (`'neutrons'`) elements are used Note that if "enriched" or "depleted" appears in the material's name, particle=`'neutrons'` is set automatically. Outputs: - `mat_str` = string containing the material's information, ready to be inserted directly into a PHITS/MCNP input file ''' import os import pickle if not matid and not matname: print('Either "matid" or "matname" MUST be defined') return None # First, locate and open materials library try: user_paths = os.environ['PYTHONPATH'].split(os.pathsep) lib_file = None for i in user_paths: if 'phits_tools' in i.lower() or 'phits-tools' in i.lower(): lib_file = i + r"\MC_materials\Compiled_MC_materials" if not lib_file: print('Could not find "PHITS_tools" folder in PYTHONPATH; this folder contains the vital "MC_materials/Compiled_MC_materials.pkl" file.') except KeyError: print('The PYTHONPATH environmental variable must be defined and contain the path to the directory holding "MC_materials/Compiled_MC_materials.pkl"') return None # Load materials library def load_obj(name ): with open(name + '.pkl', 'rb') as f: return pickle.load(f) all_mats_list = load_obj(lib_file) if matid: # use mat ID number mi = int(matid)-1 matname = all_mats_list[mi]['name'] else: # use material name and possibly source too # determine material mi = None # first check for exact matches matching_mi = [] for i in range(len(all_mats_list)): if all_mats_list[i]['name'].lower()==matname.lower(): matching_mi.append(i) if len(matching_mi)==1: mi = matching_mi[0] elif len(matching_mi)>1: print('Found multiple materials with this identical matname value:') for mmi in matching_mi: print('\tmatid={} matname="{}" source="{}"'.format(str(mmi+1),all_mats_list[mmi]['name'],all_mats_list[mmi]['source'])) if all_mats_list[mmi]['source'] and all_mats_list[mmi]['source']==matsource: mi = mmi print('\t\t^ matches inputed "matsource" and will be used') if mi==None: print('Please enter a "matsource" value identical to one of these two (or the matid).') return None else: # Exact material name not found # search for similar entries similar_mi = [] for i in range(len(all_mats_list)): if matname.lower() in all_mats_list[i]['name'].lower(): similar_mi.append(i) if len(similar_mi)==0: print('No materials with that exact name or names containing "matname" were found.') return None elif len(similar_mi)==1: mi = similar_mi[0] print('Found one similar material (matid={} matname="{}" source="{}"); using it.'.format(str(mi+1),all_mats_list[mi]['name'],all_mats_list[mi]['source'])) else: print('Found no material with exact "matname" but {} with similar names:'.format(len(similar_mi))) for smi in similar_mi: print('\tmatid={} matname="{}" source="{}"'.format(str(smi+1),all_mats_list[smi]['name'],all_mats_list[smi]['source'])) print('The first of these will be used. If another material was desired, please enter its "matid" or exact "matname".') mi = similar_mi[0] # Now that material ID has been found, generate text entry mat = all_mats_list[mi] banner_width = 60 cc = '$' # comment character entry_text = '\n'+cc+'*'*banner_width + '\n' entry_text += cc+' {:<3d} : {} \n'.format(mi+1,mat['name']) if mat['source'] and mat['source']!='-': entry_text += cc+' Source = {} \n'.format(mat['source']) if mat['formula'] and mat['formula']!='-': entry_text += cc+' Formula = {} \n'.format(mat['formula']) if mat['molecular weight'] and mat['molecular weight']!='-': entry_text += cc+' Molecular weight (g/mole) = {} \n'.format(mat['molecular weight']) if mat['density'] and mat['density']!='-': entry_text += cc+' Density (g/cm3) = {} \n'.format(mat['density']) if mat['total atom density'] and mat['total atom density']!='-': if isinstance(mat['total atom density'],str): entry_text += cc+' Total atom density (atoms/b-cm) = {} \n'.format(mat['total atom density']) else: entry_text += cc+' Total atom density (atoms/b-cm) = {:<13.4E} \n'.format(mat['total atom density']) if concentration_type==None: # user did not select this, determine which is more appropriate automatically if mat['formula'] and mat['formula']!='-': concentration_type = 'atom fraction' else: concentration_type = 'weight fraction' entry_text += cc+' Composition by {} \n'.format(concentration_type) # Determine if neutron or photon entry will be used neutron_keyword_list = ['depleted','enriched',' heu',' leu','uranium','plutonium','uranyl'] if particle==None: # user did not select this, determine which is more appropriate automatically neutron_kw_found_in_name = False for nki in neutron_keyword_list: if nki in matname.lower(): neutron_kw_found_in_name = True if neutron_kw_found_in_name: particle = 'neutrons' else: particle = 'photons' for j in range(len(mat[particle][concentration_type]['ZA'])): if isinstance(mat[particle][concentration_type]['value'][j],str): entry_format = '{:4} {:>7} {:13} '+cc+' {}' + '\n' else: entry_format = '{:4} {:>7d} {:<13.6f} '+cc+' {}' + '\n' if j==0: mstr = 'M{:<3}'.format(mi+1) else: mstr = ' '*4 ZZZAAA = mat[particle][concentration_type]['ZA'][j] if ZZZAAA == '-': ZZZAAA = mat['photons'][concentration_type]['ZA'][j] Z = int(str(ZZZAAA)[:-3]) A = str(ZZZAAA)[-3:] sym = Element_Z_to_Sym(Z) if A != '000': isotope = sym+'-'+A.lstrip('0') else: isotope = sym entry_text += entry_format.format(mstr,ZZZAAA,mat[particle][concentration_type]['value'][j],isotope) entry_text += cc+'*'*banner_width + '\n' return entry_text
def ICRP116_effective_dose_coeff(E=1.0, particle='photon', geometry='AP', interp_scale='log', interp_type='cubic', extrapolation_on=False)
-
Description
For a given particle at a given energy in a given geometry, returns its effective dose conversion coefficient from ICRP 116
Dependencies
import numpy as np
from scipy.interpolate import CubicSpline, lagrange, interp1d
find()
(function within the "PHITS Tools" package)
Inputs
E
= energy of the particle in MeV (D=1
)particle
= select particle (D='photon'
, options include:['photon', 'electron', 'positron' ,'neutron' ,'proton', 'negmuon', 'posmuon', 'negpion', 'pospion', 'He3ion']
)geometry
= geometric arrangement (D='AP'
, options include:['AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO', 'H*(10)']
('LLAT'
,'RLAT'
,'ROT'
only available for photon, proton, and neutron))- Meanings: AP, antero-posterior; PA, postero-anterior; LLAT, left lateral; RLAT, right lateral; ROT, rotational; ISO, isotropic.
- Note:
'H*(10)'
ambient dose equivalent is available for photons only
interp_scale
= interpolation scale (D='log'
to interpolate on a log scale, options include:['log','lin']
, ICRP 74/116 suggest log-log cubic interpolation)interp_type
= interpolation method (D='cubic'
to interpolate with a cubic spline, options include:['cubic','linear']
, ICRP 74/116 suggest log-log cubic interpolation) technically, any options available for scipy.interpolate.interp1d() can be used:['linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'previous']
-
extrapolation_on
= boolean designating whether values outside of the tabulated energies will be extrapolated (D=False
)if False & E < E_min f(E) = 0 if False & E > E_max f(E) = f(E_max) if True & E < E_min f(E) is linearly interpolated between (0,0) and (E_min,f(E_min)) if True & E > E_max f(E) is extrapolated using the specified interpolation scale and type
Outputs
f
= effective dose conversion coefficient in pSv*cm^2
Expand source code
def ICRP116_effective_dose_coeff(E=1.0,particle='photon',geometry='AP',interp_scale='log',interp_type='cubic',extrapolation_on=False): ''' Description: For a given particle at a given energy in a given geometry, returns its effective dose conversion coefficient from [ICRP 116](https://doi.org/10.1016/j.icrp.2011.10.001) Dependencies: - `import numpy as np` - `from scipy.interpolate import CubicSpline, lagrange, interp1d` - `find` (function within the "PHITS Tools" package) Inputs: - `E` = energy of the particle in MeV (D=`1`) - `particle` = select particle (D=`'photon'`, options include: `['photon', 'electron', 'positron' ,'neutron' ,'proton', 'negmuon', 'posmuon', 'negpion', 'pospion', 'He3ion']`) - `geometry` = geometric arrangement (D=`'AP'`, options include: `['AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO', 'H*(10)']` (`'LLAT'`,`'RLAT'`,`'ROT'` only available for photon, proton, and neutron)) - Meanings: AP, antero-posterior; PA, postero-anterior; LLAT, left lateral; RLAT, right lateral; ROT, rotational; ISO, isotropic. - Note: `'H*(10)'` ambient dose equivalent is available for photons only - `interp_scale` = interpolation scale (D=`'log'` to interpolate on a log scale, options include: `['log','lin']`, ICRP 74/116 suggest log-log cubic interpolation) - `interp_type` = interpolation method (D=`'cubic'` to interpolate with a cubic spline, options include: `['cubic','linear']`, ICRP 74/116 suggest log-log cubic interpolation) technically, any options available for scipy.interpolate.interp1d() can be used: `['linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'previous']` - `extrapolation_on` = boolean designating whether values outside of the tabulated energies will be extrapolated (D=`False`) | | | | -------------------- | --------------------------------------------------------------------- | | if False & E < E_min | f(E) = 0 | | if False & E > E_max | f(E) = f(E_max) | | if True & E < E_min | f(E) is linearly interpolated between (0,0) and (E_min,f(E_min)) | | if True & E > E_max | f(E) is extrapolated using the specified interpolation scale and type | Outputs: - `f` = effective dose conversion coefficient in pSv*cm^2 ''' import numpy as np from scipy.interpolate import CubicSpline, lagrange, interp1d pars_list = ['photon','electron','positron','neutron','proton','negmuon','posmuon','negpion','pospion','He3ion'] geo_list_all = ['AP','PA','LLAT','RLAT','ROT','ISO','H*(10)'] geo_list_short = ['AP','PA','ISO'] if particle not in pars_list or geometry not in geo_list_all: pstr = 'Please select a valid particle and geometry.\n' pstr += "Particle selected = {}, options include: ['photon','electron','positron','neutron','proton','negmuon','posmuon','negpion','pospion','He3ion']".format(particle) pstr += "Geometry selected = {}, options include: ['AP','PA','LLAT','RLAT','ROT','ISO'] ('LLAT','RLAT','ROT' only available for photon, proton, and neutron)" print(pstr) return None if (particle not in ['photon','neutron','proton'] and geometry in ['LLAT','RLAT','ROT']) or (particle!='photon' and geometry=='H*(10)'): if (particle!='photon' and geometry=='H*(10)'): pstr = "geometry = {} is only available for photons\n".format(geometry) else: pstr = "geometry = {} is only available for photon, neutron, and proton\n".format(geometry) pstr += "For selected particle = {}, please choose geometry from ['AP','PA','ISO']".format(particle) print(pstr) return None E_photon = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.511, 0.6, 0.662, 0.8, 1, 1.117, 1.33, 1.5, 2, 3, 4, 5, 6, 6.129, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_photon = [ [0.0685, 0.156, 0.225, 0.313, 0.351, 0.37, 0.39, 0.413, 0.444, 0.519, 0.748, 1, 1.51, 2, 2.47, 2.52, 2.91, 3.17, 3.73, 4.49, 4.9, 5.59, 6.12, 7.48, 9.75, 11.7, 13.4, 15, 15.1, 17.8, 20.5, 26.1, 30.8, 37.9, 43.1, 47.1, 50.1, 54.5, 57.8, 63.3, 67.3, 72.3, 75.5, 77.5, 78.9, 80.5, 81.7, 83.8, 85.2, 86.9, 88.1, 88.9, 89.5, 90.2, 90.7], [0.0184, 0.0155, 0.026, 0.094, 0.161, 0.208, 0.242, 0.271, 0.301, 0.361, 0.541, 0.741, 1.16, 1.57, 1.98, 2.03, 2.38, 2.62, 3.13, 3.83, 4.22, 4.89, 5.39, 6.75, 9.12, 11.2, 13.1, 15, 15.2, 18.6, 22, 30.3, 38.2, 51.4, 62, 70.4, 76.9, 86.6, 93.2, 104, 111, 119, 124, 128, 131, 135, 138, 142, 145, 148, 150, 152, 153, 155, 155], [0.0189, 0.0416, 0.0655, 0.11, 0.14, 0.16, 0.177, 0.194, 0.214, 0.259, 0.395, 0.552, 0.888, 1.24, 1.58, 1.62, 1.93, 2.14, 2.59, 3.23, 3.58, 4.2, 4.68, 5.96, 8.21, 10.2, 12, 13.7, 13.9, 17, 20.1, 27.4, 34.4, 47.4, 59.2, 69.5, 78.3, 92.4, 103, 121, 133, 148, 158, 165, 170, 178, 183, 193, 198, 206, 212, 216, 219, 224, 228], [0.0182, 0.039, 0.0573, 0.0891, 0.114, 0.133, 0.15, 0.167, 0.185, 0.225, 0.348, 0.492, 0.802, 1.13, 1.45, 1.49, 1.78, 1.98, 2.41, 3.03, 3.37, 3.98, 4.45, 5.7, 7.9, 9.86, 11.7, 13.4, 13.6, 16.6, 19.7, 27.1, 34.4, 48.1, 60.9, 72.2, 82, 97.9, 110, 130, 143, 161, 172, 180, 186, 195, 201, 212, 220, 229, 235, 240, 244, 251, 255], [0.0337, 0.0664, 0.0986, 0.158, 0.199, 0.226, 0.248, 0.273, 0.297, 0.355, 0.528, 0.721, 1.12, 1.52, 1.92, 1.96, 2.3, 2.54, 3.04, 3.72, 4.1, 4.75, 5.24, 6.55, 8.84, 10.8, 12.7, 14.4, 14.6, 17.6, 20.6, 27.7, 34.4, 46.1, 56, 64.4, 71.2, 82, 89.7, 102, 111, 121, 128, 133, 136, 142, 145, 152, 156, 161, 165, 168, 170, 172, 175], [0.0288, 0.056, 0.0812, 0.127, 0.158, 0.18, 0.199, 0.218, 0.239, 0.287, 0.429, 0.589, 0.932, 1.28, 1.63, 1.67, 1.97, 2.17, 2.62, 3.25, 3.6, 4.2, 4.66, 5.9, 8.08, 10, 11.8, 13.5, 13.7, 16.6, 19.6, 26.8, 33.8, 46.1, 56.9, 66.2, 74.1, 87.2, 97.5, 116, 130, 147, 159, 168, 174, 185, 193, 208, 218, 232, 243, 251, 258, 268, 276], [0.061, 0.83, 1.05, 0.81, 0.64, 0.55, 0.51, 0.52, 0.53, 0.61, 0.89, 1.20, 1.80, 2.38, 2.93, 2.99, 3.44, 3.73, 4.38, 5.20, 5.60, 6.32, 6.90, 8.60, 11.10, 13.40, 15.50, 17.60, 17.86, 21.60, 25.60, 8.53, 8.29, 8.23, 8.26, 8.64, 8.71, 8.86, 9.00, 9.60, 10.20, 10.73, 11.27, 11.80, 11.78, 11.74, 11.70, 11.60, 11.50, 12.10, 12.70, 13.30, 13.08, 12.64, 12.20] ] E_electron = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_electron = [ [0.0269, 0.0404, 0.0539, 0.081, 0.108, 0.135, 0.163, 0.218, 0.275, 0.418, 0.569, 0.889, 1.24, 1.63, 2.05, 4.04, 7.1, 15, 22.4, 36.1, 48.2, 59.3, 70.6, 97.9, 125, 188, 236, 302, 329, 337, 341, 346, 349, 355, 359, 365, 369, 372, 375, 379, 382, 387, 391, 397, 401, 405, 407, 411, 414], [0.0268, 0.0402, 0.0535, 0.0801, 0.107, 0.133, 0.16, 0.213, 0.267, 0.399, 0.53, 0.787, 1.04, 1.28, 1.5, 1.68, 1.68, 1.62, 1.62, 1.95, 2.62, 3.63, 5.04, 9.46, 18.3, 53.1, 104, 220, 297, 331, 344, 358, 366, 379, 388, 399, 408, 414, 419, 428, 434, 446, 455, 468, 477, 484, 490, 499, 507], [0.0188, 0.0283, 0.0377, 0.0567, 0.0758, 0.0948, 0.114, 0.152, 0.191, 0.291, 0.393, 0.606, 0.832, 1.08, 1.35, 1.97, 2.76, 4.96, 7.24, 11.9, 16.4, 21, 25.5, 35.5, 46.7, 76.9, 106, 164, 212, 249, 275, 309, 331, 363, 383, 410, 430, 445, 457, 478, 495, 525, 549, 583, 608, 628, 646, 675, 699] ] E_positron = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_positron = [ [3.28, 3.29, 3.3, 3.33, 3.36, 3.39, 3.42, 3.47, 3.53, 3.67, 3.84, 4.16, 4.52, 4.9, 5.36, 7.41, 10.5, 18.3, 25.7, 39.1, 51, 61.7, 72.9, 99, 126, 184, 229, 294, 320, 327, 333, 339, 342, 349, 354, 362, 366, 369, 372, 376, 379, 385, 389, 395, 399, 402, 404, 408, 411], [1.62, 1.64, 1.65, 1.68, 1.71, 1.73, 1.76, 1.82, 1.87, 2.01, 2.14, 2.4, 2.65, 2.9, 3.12, 3.32, 3.37, 3.44, 3.59, 4.19, 5.11, 6.31, 8.03, 14, 23.6, 59, 111, 221, 291, 321, 334, 349, 357, 371, 381, 393, 402, 409, 415, 424, 430, 443, 451, 465, 473, 480, 486, 495, 503], [1.39, 1.4, 1.41, 1.43, 1.45, 1.47, 1.49, 1.53, 1.57, 1.67, 1.77, 1.98, 2.21, 2.45, 2.72, 3.38, 4.2, 6.42, 8.7, 13.3, 18, 22.4, 26.9, 36.7, 47.6, 75.5, 104, 162, 209, 243, 268, 302, 323, 356, 377, 405, 425, 440, 453, 474, 491, 522, 545, 580, 605, 627, 645, 674, 699] ] E_neutron = [1.00E-09, 1.00E-08, 2.50E-08, 1.00E-07, 2.00E-07, 5.00E-07, 1.00E-06, 2.00E-06, 5.00E-06, 1.00E-05, 2.00E-05, 5.00E-05, 1.00E-04, 2.00E-04, 5.00E-04, 0.001, 0.002, 0.005, 0.01, 0.02, 0.03, 0.05, 0.07, 0.1, 0.15, 0.2, 0.3, 0.5, 0.7, 0.9, 1, 1.2, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 30, 50, 75, 100, 130, 150, 180, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 5000, 10000] f_neutron = [ [3.09, 3.55, 4, 5.2, 5.87, 6.59, 7.03, 7.39, 7.71, 7.82, 7.84, 7.82, 7.79, 7.73, 7.54, 7.54, 7.61, 7.97, 9.11, 12.2, 15.7, 23, 30.6, 41.9, 60.6, 78.8, 114, 177, 232, 279, 301, 330, 365, 407, 458, 483, 494, 498, 499, 499, 500, 500, 499, 495, 493, 490, 484, 477, 474, 453, 433, 420, 402, 382, 373, 363, 359, 363, 389, 422, 457, 486, 508, 524, 537, 612, 716, 933], [1.85, 2.11, 2.44, 3.25, 3.72, 4.33, 4.73, 5.02, 5.3, 5.44, 5.51, 5.55, 5.57, 5.59, 5.6, 5.6, 5.62, 5.95, 6.81, 8.93, 11.2, 15.7, 20, 25.9, 34.9, 43.1, 58.1, 85.9, 112, 136, 148, 167, 195, 235, 292, 330, 354, 371, 383, 392, 398, 404, 412, 417, 419, 420, 422, 423, 423, 422, 428, 439, 444, 446, 446, 447, 448, 464, 496, 533, 569, 599, 623, 640, 654, 740, 924, 1.17E+03], [1.04, 1.15, 1.32, 1.7, 1.94, 2.21, 2.4, 2.52, 2.64, 2.65, 2.68, 2.66, 2.65, 2.66, 2.62, 2.61, 2.6, 2.74, 3.13, 4.21, 5.4, 7.91, 10.5, 14.4, 20.8, 27.2, 39.7, 63.7, 85.5, 105, 115, 130, 150, 179, 221, 249, 269, 284, 295, 303, 310, 316, 325, 333, 336, 338, 343, 347, 348, 360, 380, 399, 409, 416, 420, 425, 427, 441, 472, 510, 547, 579, 603, 621, 635, 730, 963, 1.23E+03], [0.893, 0.978, 1.12, 1.42, 1.63, 1.86, 2.02, 2.11, 2.21, 2.24, 2.26, 2.24, 2.23, 2.24, 2.21, 2.21, 2.2, 2.33, 2.67, 3.6, 4.62, 6.78, 8.95, 12.3, 17.9, 23.4, 34.2, 54.4, 72.6, 89.3, 97.4, 110, 128, 153, 192, 220, 240, 255, 267, 276, 284, 290, 301, 310, 313, 317, 323, 328, 330, 345, 370, 392, 404, 413, 418, 425, 429, 451, 483, 523, 563, 597, 620, 638, 651, 747, 979, 1.26E+03], [1.7, 2.03, 2.31, 2.98, 3.36, 3.86, 4.17, 4.4, 4.59, 4.68, 4.72, 4.73, 4.72, 4.67, 4.6, 4.58, 4.61, 4.86, 5.57, 7.41, 9.46, 13.7, 18, 24.3, 34.7, 44.7, 63.8, 99.1, 131, 160, 174, 193, 219, 254, 301, 331, 351, 365, 374, 381, 386, 390, 395, 398, 398, 399, 399, 398, 398, 395, 395, 402, 406, 411, 414, 418, 422, 443, 472, 503, 532, 558, 580, 598, 614, 718, 906, 1.14E+03], [1.29, 1.56, 1.76, 2.26, 2.54, 2.92, 3.15, 3.32, 3.47, 3.52, 3.54, 3.55, 3.54, 3.52, 3.47, 3.46, 3.48, 3.66, 4.19, 5.61, 7.18, 10.4, 13.7, 18.6, 26.6, 34.4, 49.4, 77.1, 102, 126, 137, 153, 174, 203, 244, 271, 290, 303, 313, 321, 327, 332, 339, 344, 346, 347, 350, 352, 353, 358, 371, 387, 397, 407, 412, 421, 426, 455, 488, 521, 553, 580, 604, 624, 642, 767, 1.01E+03, 1.32E+03] ] E_proton = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_proton = [ [5.46, 8.2, 10.9, 16.4, 21.9, 27.3, 32.8, 43.7, 54.9, 189, 428, 750, 1.02E+03, 1.18E+03, 1.48E+03, 2.16E+03, 2.51E+03, 2.38E+03, 1.77E+03, 1.38E+03, 1.23E+03, 1.15E+03, 1.16E+03, 1.11E+03, 1.09E+03, 1.15E+03, 1.12E+03, 1.23E+03, 1.27E+03, 1.23E+03, 1.37E+03, 1.45E+03, 1.41E+03], [5.47, 8.21, 10.9, 16.4, 21.9, 27.3, 32.8, 43.7, 54.6, 56.1, 43.6, 36.1, 45.5, 71.5, 156, 560, 1.19E+03, 2.82E+03, 1.93E+03, 1.45E+03, 1.30E+03, 1.24E+03, 1.23E+03, 1.23E+03, 1.23E+03, 1.25E+03, 1.28E+03, 1.34E+03, 1.40E+03, 1.45E+03, 1.53E+03, 1.65E+03, 1.74E+03], [2.81, 4.21, 5.61, 8.43, 11.2, 14, 16.8, 22.4, 28.1, 50.7, 82.8, 180, 290, 379, 500, 799, 994, 1.64E+03, 2.15E+03, 1.44E+03, 1.27E+03, 1.21E+03, 1.20E+03, 1.19E+03, 1.18E+03, 1.21E+03, 1.25E+03, 1.32E+03, 1.31E+03, 1.39E+03, 1.44E+03, 1.56E+03, 1.63E+03], [2.81, 4.2, 5.62, 8.41, 11.2, 14, 16.8, 22.4, 28.1, 48.9, 78.8, 172, 278, 372, 447, 602, 818, 1.46E+03, 2.18E+03, 1.45E+03, 1.28E+03, 1.21E+03, 1.20E+03, 1.20E+03, 1.20E+03, 1.23E+03, 1.25E+03, 1.32E+03, 1.33E+03, 1.41E+03, 1.45E+03, 1.59E+03, 1.67E+03], [4.5, 6.75, 8.98, 13.4, 17.8, 22.1, 26.3, 34.5, 50.1, 93.7, 165, 296, 422, 532, 687, 1.09E+03, 1.44E+03, 2.16E+03, 1.96E+03, 1.44E+03, 1.28E+03, 1.22E+03, 1.22E+03, 1.20E+03, 1.19E+03, 1.23E+03, 1.23E+03, 1.30E+03, 1.29E+03, 1.35E+03, 1.41E+03, 1.49E+03, 1.56E+03], [3.52, 5.28, 7.02, 10.5, 13.9, 17.3, 20.5, 26.8, 45.8, 80.1, 136, 249, 358, 451, 551, 837, 1.13E+03, 1.79E+03, 1.84E+03, 1.42E+03, 1.25E+03, 1.18E+03, 1.17E+03, 1.17E+03, 1.15E+03, 1.21E+03, 1.22E+03, 1.31E+03, 1.40E+03, 1.43E+03, 1.57E+03, 1.71E+03, 1.78E+03] ] E_negmuon = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_negmuon = [ [180, 180, 184, 188, 193, 205, 242, 293, 332, 414, 465, 657, 735, 755, 628, 431, 382, 340, 326, 319, 320, 321, 325, 327, 333, 331, 333, 336, 337, 337, 337, 337, 338], [75.2, 76.8, 78.3, 81.4, 84.8, 87.7, 86.7, 86.8, 88.6, 100, 122, 251, 457, 703, 775, 485, 402, 345, 329, 321, 321, 324, 326, 332, 337, 338, 341, 344, 345, 346, 346, 347, 347], [78.7, 79.5, 80.9, 83.7, 87.1, 91.5, 98.1, 113, 127, 161, 191, 275, 363, 446, 496, 498, 432, 354, 332, 321, 321, 323, 326, 331, 337, 338, 341, 344, 346, 347, 347, 348, 348] ] E_posmuon = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000] f_posmuon = [ [194, 196, 198, 202, 207, 216, 251, 300, 340, 425, 481, 674, 751, 768, 635, 431, 381, 339, 326, 318, 319, 320, 322, 325, 327, 331, 333, 336, 337, 337, 337, 337, 339], [82.6, 84.1, 85.7, 88.9, 92.1, 94.3, 92.5, 92.8, 94.8, 108, 133, 265, 473, 721, 787, 483, 399, 345, 328, 320, 321, 323, 325, 330, 333, 339, 341, 344, 345, 346, 346, 347, 347], [85.2, 86.2, 87.5, 90.3, 93.6, 97.7, 103, 117, 132, 167, 199, 284, 373, 456, 506, 502, 432, 354, 332, 320, 320, 322, 324, 329, 333, 338, 341, 344, 346, 347, 347, 348, 348] ] E_negpion = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 15000, 20000, 30000, 40000, 50000, 60000, 80000, 100000, 150000, 200000] f_negpion = [ [406, 422, 433, 458, 491, 528, 673, 965, 1.09E+03, 1.25E+03, 1.28E+03, 1.77E+03, 1.92E+03, 1.93E+03, 1.68E+03, 1.14E+03, 995, 927, 902, 848, 844, 869, 901, 947, 977, 1.03E+03, 1.05E+03, 1.03E+03, 1.03E+03, 1.06E+03, 1.09E+03, 1.14E+03, 1.17E+03, 1.21E+03, 1.24E+03, 1.30E+03, 1.35E+03, 1.39E+03, 1.42E+03, 1.48E+03, 1.54E+03, 1.67E+03, 1.78E+03], [194, 201, 210, 225, 233, 237, 208, 181, 178, 197, 244, 547, 1.02E+03, 1.70E+03, 1.99E+03, 1.31E+03, 991, 889, 871, 843, 850, 880, 917, 976, 1.02E+03, 1.08E+03, 1.12E+03, 1.11E+03, 1.13E+03, 1.18E+03, 1.22E+03, 1.29E+03, 1.34E+03, 1.41E+03, 1.47E+03, 1.56E+03, 1.63E+03, 1.70E+03, 1.75E+03, 1.86E+03, 1.95E+03, 2.15E+03, 2.33E+03], [176, 189, 198, 215, 232, 251, 271, 317, 361, 439, 508, 676, 868, 1.02E+03, 1.15E+03, 1.15E+03, 1.03E+03, 857, 815, 794, 807, 838, 875, 935, 979, 1.05E+03, 1.09E+03, 1.11E+03, 1.15E+03, 1.20E+03, 1.26E+03, 1.36E+03, 1.43E+03, 1.55E+03, 1.64E+03, 1.79E+03, 1.91E+03, 2.02E+03, 2.11E+03, 2.29E+03, 2.46E+03, 2.80E+03, 3.04E+03] ] E_pospion = [1, 1.5, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 400, 500, 600, 800, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 15000, 20000, 30000, 40000, 50000, 60000, 80000, 100000, 150000, 200000] f_pospion = [ [314, 324, 340, 379, 429, 489, 540, 717, 819, 1000, 1.10E+03, 1.52E+03, 1.75E+03, 1.83E+03, 1.66E+03, 1.22E+03, 1.13E+03, 1.22E+03, 1.25E+03, 1.07E+03, 969, 943, 952, 999, 1.04E+03, 1.10E+03, 1.10E+03, 1.06E+03, 1.06E+03, 1.07E+03, 1.10E+03, 1.14E+03, 1.17E+03, 1.22E+03, 1.25E+03, 1.30E+03, 1.34E+03, 1.38E+03, 1.42E+03, 1.48E+03, 1.54E+03, 1.67E+03, 1.78E+03], [121, 125, 133, 151, 170, 183, 185, 177, 179, 201, 247, 494, 906, 1.48E+03, 1.82E+03, 1.38E+03, 1.12E+03, 1.15E+03, 1.23E+03, 1.10E+03, 998, 970, 980, 1.04E+03, 1.09E+03, 1.16E+03, 1.19E+03, 1.16E+03, 1.16E+03, 1.20E+03, 1.24E+03, 1.31E+03, 1.35E+03, 1.42E+03, 1.48E+03, 1.57E+03, 1.64E+03, 1.70E+03, 1.75E+03, 1.84E+03, 1.94E+03, 2.14E+03, 2.33E+03], [151, 160, 168, 183, 198, 216, 233, 265, 296, 367, 439, 602, 787, 953, 1.09E+03, 1.16E+03, 1.10E+03, 1.05E+03, 1.08E+03, 1.02E+03, 953, 930, 938, 993, 1.05E+03, 1.13E+03, 1.16E+03, 1.16E+03, 1.18E+03, 1.23E+03, 1.28E+03, 1.37E+03, 1.43E+03, 1.55E+03, 1.64E+03, 1.79E+03, 1.90E+03, 2.01E+03, 2.10E+03, 2.27E+03, 2.42E+03, 2.76E+03, 3.07E+03] ] E_He3ion = [1, 2, 3, 5, 10, 14, 20, 30, 50, 75, 100, 150, 200, 300, 500, 700, 1000, 2000, 3000, 5000, 10000, 20000, 50000, 100000] f_He3ion = [ [219, 438, 656, 1.09E+03, 2.19E+03, 4.61E+03, 1.72E+04, 3.01E+04, 4.75E+04, 8.05E+04, 1.01E+05, 9.25E+04, 6.74E+04, 5.14E+04, 4.27E+04, 4.11E+04, 4.00E+04, 4.02E+04, 4.08E+04, 4.12E+04, 4.56E+04, 5.12E+04, 6.12E+04, 7.14E+04], [219, 438, 657, 1.09E+03, 2.19E+03, 2.56E+03, 1.74E+03, 1.44E+03, 2.88E+03, 1.75E+04, 4.84E+04, 1.10E+05, 7.29E+04, 5.33E+04, 4.49E+04, 4.60E+04, 4.47E+04, 4.80E+04, 5.01E+04, 5.17E+04, 6.26E+04, 6.10E+04, 8.14E+04, 1.01E+05], [141, 281, 419, 689, 1.82E+03, 2.81E+03, 5.46E+03, 9.86E+03, 1.78E+04, 3.00E+04, 4.55E+04, 6.95E+04, 7.01E+04, 5.25E+04, 4.27E+04, 4.19E+04, 4.09E+04, 4.31E+04, 4.50E+04, 4.76E+04, 5.73E+04, 7.10E+04, 9.67E+04, 1.24E+05] ] E_all = [E_photon, E_electron, E_positron, E_neutron, E_proton, E_negmuon, E_posmuon, E_negpion, E_pospion, E_He3ion] f_all = [f_photon, f_electron, f_positron, f_neutron, f_proton, f_negmuon, f_posmuon, f_negpion, f_pospion, f_He3ion] pi = find(particle, pars_list) if particle in ['photon','neutron','proton']: gi = find(geometry, geo_list_all) else: gi = find(geometry, geo_list_short) E_list = E_all[pi] f_list = f_all[pi][gi] # Interpolate f given E if E in E_list: f = f_list[find(E,E_list)] else: if not extrapolation_on and (E < E_list[0] or E > E_list[-1]): # E is outside of bounds and extrapolation is off if E < E_list[0]: f = 0 # assume negligibly low energy particle if E > E_list[-1]: f = f_list[-1] # just set equal to max energy particle's coefficient else: if E < E_list[0]: E_list = [0] + E_list f_list = [0] + f_list interp_scale = 'linear' if interp_scale=='log': cs = interp1d(np.log10(np.array(E_list)),np.log10(np.array(f_list)), kind=interp_type,fill_value='extrapolate') f = 10**cs(np.log10(E)) else: cs = interp1d(np.array(E_list),np.array(f_list), kind=interp_type,fill_value='extrapolate') f = cs(E) # for sake of sanity, return zero for values quite below minimum coefficients if f < 1e-4: f = 0.0 #if interp_type=='cubic': # if interp_scale=='log': # cs = interp1d(np.log10(np.array(E_list)),np.log10(np.array(f_list)), kind='cubic',fill_value='extrapolate') # f = 10**cs(np.log10(E)) # else: # cs = interp1d(np.array(E_list),np.array(f_list), kind='cubic',fill_value='extrapolate') # f = cs(E) #else: # if interp_scale=='log': # f = 10**np.interp(np.log10(E),np.log10(np.array(E_list)),np.log10(np.array(f_list))) # else: # f = np.interp(E,np.array(E_list),np.array(f_list)) #if interp_type=='cubic': # if interp_scale=='log': # cs = lagrange(np.log10(np.array(E_list)),np.log10(np.array(f_list))) # f = 10**cs(np.log10(E)) # else: # cs = lagrange(np.array(E_list),np.array(f_list)) # f = cs(E) #if interp_type=='cubic': # if interp_scale=='log': # cs = CubicSpline(np.log10(np.array(E_list)),np.log10(np.array(f_list))) # f = 10**cs(np.log10(E)) # else: # cs = CubicSpline(np.array(E_list),np.array(f_list)) # f = cs(E) return f
def merge_dump_file_pickles(dump_filepath_list, merged_dump_base_filepath='merged_dump', delete_pre_merge_pickles=False, compress_pickles_with_lzma=True)
-
Description
Merge the pickle files (namedtuple lists and/or PandasDataFrames) belonging to numerous PHITS dump output files.
Inputs
dump_filepath_list
= list of Path objects or strings denoting filepaths to the PHITS dump files (e.g., "*_dmp.out") to be merged. Note that all dump files must be structured in the same way in terms of number of columns and their meanings.merged_dump_base_filepath
= (D=os.cwd()+'merged_dump'
) Path object or string designating the base file path name to be used for the merged pickle files, which will take the formmerged_dump_base_filepath
+ "_namedtuple_list.pickle[.xz]" and/or "_Pandas_df.pickle[.xz]" (the ".xz" being contingent on LZMA usage viacompress_pickles_with_lzma
).delete_pre_merge_pickles
= (optional, D=False
) Boolean designating whether the numerous pickle files to be merged into one should be deleted after the merge is successful. (Note: File deletion will only succeed if you have permissions to delete these files on your system.)compress_pickles_with_lzma
= (optional, D=True
) Boolean designating whether the pickle files to be saved of the merged namedtuple lists and/or the Pandas DataFrames will be compressed with LZMA compression (included within the baseline Python standard library); if so, the file extension of the saved pickle file(s) will be'.pickle.xz'
instead of just'.pickle'
. A *.pickle.xz file can then be opened (after importingpickle
andlzma
) as:with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)
. While compression will notably slow down the file-saving process, owing to the often large size of PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred.
Notes
For each dump file provided, this function will check for the existence of the same filename/path but with "_namedtuple_list.pickle[.xz]" and "_Pandas_df.pickle[.xz]" at the end. If neither file for a provided dump file is found but the dump file itself is found to exist,
parse_tally_dump_file()
will be called on it with default settings exceptsave_namedtuple_list=True
andsave_Pandas_dataframe=True
.Outputs
merge_success
= Boolean designating whether merging succeeded or not.
Expand source code
def merge_dump_file_pickles(dump_filepath_list, merged_dump_base_filepath='merged_dump', delete_pre_merge_pickles=False, compress_pickles_with_lzma=True): ''' Description: Merge the pickle files (namedtuple lists and/or PandasDataFrames) belonging to numerous PHITS dump output files. Inputs: - `dump_filepath_list` = list of Path objects or strings denoting filepaths to the PHITS dump files (e.g., "*_dmp.out") to be merged. Note that all dump files must be structured in the same way in terms of number of columns and their meanings. - `merged_dump_base_filepath` = (D=`os.cwd()+'merged_dump'`) Path object or string designating the base file path name to be used for the merged pickle files, which will take the form `merged_dump_base_filepath` + "_namedtuple_list.pickle[.xz]" and/or "_Pandas_df.pickle[.xz]" (the ".xz" being contingent on LZMA usage via `compress_pickles_with_lzma`). - `delete_pre_merge_pickles` = (optional, D=`False`) Boolean designating whether the numerous pickle files to be merged into one should be deleted after the merge is successful. (Note: File deletion will only succeed if you have permissions to delete these files on your system.) - `compress_pickles_with_lzma` = (optional, D=`True`) Boolean designating whether the pickle files to be saved of the merged namedtuple lists and/or the Pandas DataFrames will be compressed with [LZMA compression](https://docs.python.org/3/library/lzma.html) (included within the baseline [Python standard library](https://docs.python.org/3/library/index.html)); if so, the file extension of the saved pickle file(s) will be `'.pickle.xz'` instead of just `'.pickle'`. A *.pickle.xz file can then be opened (after importing `pickle` and `lzma`) as: `with lzma.open(path_to_picklexz_file, 'rb') as file: dump_data_list = pickle.load(file)`. While compression will notably slow down the file-saving process, owing to the often large size of PHITS dump files the additional reduction in file size (often around a factor of 5) is generally preferred. Notes: For each dump file provided, this function will check for the existence of the same filename/path but with "_namedtuple_list.pickle[.xz]" and "_Pandas_df.pickle[.xz]" at the end. If neither file for a provided dump file is found but the dump file itself is found to exist, `parse_tally_dump_file()` will be called on it with default settings except `save_namedtuple_list=True` and `save_Pandas_dataframe=True`. Outputs: - `merge_success` = Boolean designating whether merging succeeded or not. ''' import lzma import pickle merge_success = False if compress_pickles_with_lzma: compression_file_extension = '.xz' else: compression_file_extension = '' # Scan to see what files are available namedtuple_list_paths = [] pandads_DF_paths = [] for f in dump_filepath_list: fp = Path(f) filebasename = fp.name if filebasename[-4:]=='.out': filebasename = filebasename[:-4] nt_found, pd_found = False, False nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle.xz') if nt_path.is_file(): nt_found = True else: nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle') if nt_path.is_file(): nt_found = True pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle.xz') if pd_path.is_file(): pd_found = True else: pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle') if pd_path.is_file(): pd_found = True if not nt_found and not pd_found and fp.is_file(): parse_tally_dump_file(fp,return_namedtuple_list=False, return_Pandas_dataframe=False, save_namedtuple_list=True, save_Pandas_dataframe=True) nt_path = Path(fp.parent, filebasename + '_namedtuple_list.pickle.xz') pd_path = Path(fp.parent, filebasename + '_Pandas_df.pickle.xz') nt_found, pd_found = True, True if nt_found: namedtuple_list_paths.append(nt_path) if pd_found: pandads_DF_paths.append(pd_path) # Merge namedtuple lists if len(namedtuple_list_paths) > 1: from numpy.lib.recfunctions import stack_arrays if len(namedtuple_list_paths) != len(dump_filepath_list): print('WARNING: While multiple "_namedtuple_list.pickle[.xz]" files were found, some were missing from provided dump file list.') for i, f in enumerate(namedtuple_list_paths): if f.suffixes[-1] == '.xz': with lzma.open(f, 'rb') as file: dump_data_list = pickle.load(file) else: with open(f, 'rb') as file: dump_data_list = pickle.load(file) if i==0: a = dump_data_list continue b = dump_data_list records_np_array = stack_arrays((a, b), asrecarray=True, usemask=False) a = records_np_array del a, b # release memory for this, as it can be very, very big path_to_dump_file = Path(merged_dump_base_filepath) pickle_path = Path(path_to_dump_file.parent, path_to_dump_file.stem + '_namedtuple_list.pickle' + compression_file_extension) if compress_pickles_with_lzma: with lzma.open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) else: with open(pickle_path, 'wb') as handle: pickle.dump(records_np_array, handle, protocol=pickle.HIGHEST_PROTOCOL) print('Pickle file written:', pickle_path, '\n') merge_success = True del records_np_array # release memory for this, as it can be very, very big if delete_pre_merge_pickles: for f in namedtuple_list_paths: try: f.unlink() print('\tPickle file deleted:', f) except: pass print() # Merge Pandas DataFrames if len(pandads_DF_paths) > 1: import pandas as pd if len(pandads_DF_paths) != len(dump_filepath_list): print('WARNING: While multiple "_Pandas_df.pickle[.xz]" files were found, some were missing from provided dump file list.') dfs_to_concat = [] for i, f in enumerate(pandads_DF_paths): if f.suffixes[-1] == '.xz': with lzma.open(f, 'rb') as file: dump_dataframe = pickle.load(file) else: with open(f, 'rb') as file: dump_dataframe = pickle.load(file) dfs_to_concat.append(dump_dataframe) combined_df = pd.concat(dfs_to_concat, ignore_index=True) del dfs_to_concat # release memory for this, as it can be very, very big path_to_dump_file = Path(merged_dump_base_filepath) pickle_path = Path(path_to_dump_file.parent, path_to_dump_file.stem + '_Pandas_df.pickle' + compression_file_extension) combined_df.to_pickle(pickle_path) print('Pickle file written:', pickle_path, '\n') merge_success = True del combined_df # release memory for this, as it can be very, very big if delete_pre_merge_pickles: for f in pandads_DF_paths: try: f.unlink() print('\tPickle file deleted:', f) except: pass print() return merge_success
def ZZZAAAM_to_nuclide_plain_str(ZZZAAAM, include_Z=False, ZZZAAA=False, delimiter='-')
-
Description
Converts a plaintext string of a nuclide from an integer ZZZAAAM = 10000*Z + 10*A + M
Dependencies
Element_Z_to_Sym()
(function within the "PHITS Tools" package)Input
ZZZAAAM
= integer equal to 10000*Z + 10*A + M, where M designates the metastable state (0=ground)include_Z
= Boolean denoting whether the Z number should be included in the output string (D=False
)ZZZAAA
= Boolean denoting whether the input should be interpreted as a ZZZAAA value (1000Z+A) instead (D=False
)delimiter
= string which will be used to separate elements of the output string (D=-
)
Output
nuc_str
= string describing the input nuclide formatted as [Z]-[Symbol]-[A][m]
Expand source code
def ZZZAAAM_to_nuclide_plain_str(ZZZAAAM,include_Z=False,ZZZAAA=False,delimiter='-'): ''' Description: Converts a plaintext string of a nuclide from an integer ZZZAAAM = 10000*Z + 10*A + M Dependencies: `Element_Z_to_Sym` (function within the "PHITS Tools" package) Input: - `ZZZAAAM` = integer equal to 10000*Z + 10*A + M, where M designates the metastable state (0=ground) - `include_Z` = Boolean denoting whether the Z number should be included in the output string (D=`False`) - `ZZZAAA` = Boolean denoting whether the input should be interpreted as a ZZZAAA value (1000Z+A) instead (D=`False`) - `delimiter` = string which will be used to separate elements of the output string (D=`-`) Output: - `nuc_str` = string describing the input nuclide formatted as [Z]-[Symbol]-[A][m] ''' ZZZAAAM = int(ZZZAAAM) if ZZZAAA: ZZZAAAM = ZZZAAAM*10 m = ZZZAAAM % 10 A = (ZZZAAAM % 10000) // 10 Z = ZZZAAAM // 10000 symbol = Element_Z_to_Sym(Z) m_str = '' if m>0: m_str = 'm' + str(m) nuc_str = '' if include_Z: nuc_str += str(Z) + delimiter nuc_str += symbol + delimiter + str(A) + m_str return nuc_str
def nuclide_plain_str_to_latex_str(nuc_str, include_Z=False)
-
Description
Converts a plaintext string of a nuclide to a LaTeX-formatted raw string Note: if you already have the Z, A, and isomeric state information determined, the "nuclide_to_Latex_form" function can be used instead
Dependencies
Element_Z_to_Sym()
(function within the "PHITS Tools" package) (only required ifinclude_Z = True
)
Input
(required)
nuc_str
= string to be converted; a huge variety of formats are supported, but they all must follow the following rules:- Isomeric/metastable state characters must always immediately follow the atomic mass characters.
Isomeric state labels MUST either:
- (1) be a single lower-case character OR
- (2) begin with any non-numeric character and end with a number
- Atomic mass numbers must be nonnegative integers OR the string
"nat"
(in which case no metastable states can be written) - Elemental symbols MUST begin with an upper-case character
- Isomeric/metastable state characters must always immediately follow the atomic mass characters.
Isomeric state labels MUST either:
Input
(optional)
include_Z
=True
/False
determining whether the nuclide's atomic number Z will be printed as a subscript beneath the atomic mass
Output
- LaTeX-formatted raw string of nuclide
Expand source code
def nuclide_plain_str_to_latex_str(nuc_str,include_Z=False): ''' Description: Converts a plaintext string of a nuclide to a LaTeX-formatted raw string Note: if you already have the Z, A, and isomeric state information determined, the "nuclide_to_Latex_form" function can be used instead Dependencies: - `Element_Z_to_Sym` (function within the "PHITS Tools" package) (only required if `include_Z = True`) Input: (required) - `nuc_str` = string to be converted; a huge variety of formats are supported, but they all must follow the following rules: + Isomeric/metastable state characters must always immediately follow the atomic mass characters. Isomeric state labels MUST either: - (1) be a single lower-case character OR - (2) begin with any non-numeric character and end with a number + Atomic mass numbers must be nonnegative integers OR the string `"nat"` (in which case no metastable states can be written) + Elemental symbols MUST begin with an upper-case character Input: (optional) - `include_Z` = `True`/`False` determining whether the nuclide's atomic number Z will be printed as a subscript beneath the atomic mass Output: - LaTeX-formatted raw string of nuclide ''' tex_str = r'' # remove unwanted characters from provided string delete_characters_list = [' ', '-', '_'] for dc in delete_characters_list: nuc_str = nuc_str.replace(dc,'') # determine which characters are letters versus numbers isalpha_list = [] isdigit_list = [] for c in nuc_str: isalpha_list.append(c.isalpha()) isdigit_list.append(c.isdigit()) symbol = '' mass = '' isost = '' # string MUST begin with either mass number or elemental symbol if isdigit_list[0] or nuc_str[0:3]=='nat': # mass first mass_first = True else: mass_first = False if mass_first: if nuc_str[0:3]=='nat': mass = 'nat' ci = 3 else: ci = 0 while isdigit_list[ci]: mass += nuc_str[ci] ci += 1 mass = str(int(mass)) # eliminate any extra leading zeros # encountered a non-numeric character, end of mass # now, determine if metastable state is listed or if element is listed next # first, check to see if any other numerals are in string lni = 0 # last numeral index for i in range(ci,len(nuc_str)): if isdigit_list[i]: lni = i if lni != 0: # grab all characters between ci and last numeral as metastable state isost = nuc_str[ci:lni+1] ci = lni + 1 else: # no more numerals in string, now check for single lower-case letter if isalpha_list[ci] and nuc_str[ci].islower(): isost = nuc_str[ci] ci += 1 # Now extract elemental symbol for i in range(ci,len(nuc_str)): if isalpha_list[i]: symbol += nuc_str[i] else: # if elemental symbol is listed first if 'nat' in nuc_str: mass = 'nat' nuc_str = nuc_str.replace('nat','') ci = 0 # Extract all characters before first number as the elemental symbol while nuc_str[ci].isalpha(): symbol += nuc_str[ci] ci += 1 # now, extract mass if mass != 'nat': while nuc_str[ci].isdigit(): mass += nuc_str[ci] ci += 1 if ci == len(nuc_str): break # lastly, extract isomeric state, if present if ci != len(nuc_str): isost = nuc_str[ci:] # treating the cases of lowercase-specified particles (n, d, t, etc.) if symbol == '' and isost != '': symbol = isost isost = '' # Now assemble LaTeX string for nuclides if include_Z: if symbol == 'n': Z = 0 elif symbol == 'p' or symbol == 'd' or symbol == 't': Z = 1 else: Z = Element_Sym_to_Z(symbol) Z = str(int(Z)) tex_str = r"$^{{{}{}}}_{{{}}}$".format(mass,isost,Z) + "{}".format(symbol) else: tex_str = r"$^{{{}{}}}$".format(mass,isost) + "{}".format(symbol) return tex_str
def Element_Z_to_Sym(Z)
-
Description
Returns elemental symbol for a provided atomic number Z
Inputs
Z
= atomic number
Outputs
sym
= string of elemental symbol for element of atomic number Z
Expand source code
def Element_Z_to_Sym(Z): ''' Description: Returns elemental symbol for a provided atomic number Z Inputs: - `Z` = atomic number Outputs: - `sym` = string of elemental symbol for element of atomic number Z ''' elms = ["n ",\ "H ","He","Li","Be","B ","C ","N ","O ","F ","Ne",\ "Na","Mg","Al","Si","P ","S ","Cl","Ar","K ","Ca",\ "Sc","Ti","V ","Cr","Mn","Fe","Co","Ni","Cu","Zn",\ "Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y ","Zr",\ "Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn",\ "Sb","Te","I ","Xe","Cs","Ba","La","Ce","Pr","Nd",\ "Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb",\ "Lu","Hf","Ta","W ","Re","Os","Ir","Pt","Au","Hg",\ "Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th",\ "Pa","U ","Np","Pu","Am","Cm","Bk","Cf","Es","Fm",\ "Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds",\ "Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"] i = int(Z) if i < 0 or i > len(elms): print('Z={} is not valid, please select a number from 0 to 118 (inclusive).'.format(str(Z))) return None return elms[i].strip()
def Element_Sym_to_Z(sym)
-
Description
Returns atomic number Z for a provided elemental symbol
Dependencies
find()
(function within the "PHITS Tools" package)Inputs
sym
= string of elemental symbol for element of atomic number Z
Outputs
Z
= atomic number
Expand source code
def Element_Sym_to_Z(sym): ''' Description: Returns atomic number Z for a provided elemental symbol Dependencies: `find` (function within the "PHITS Tools" package) Inputs: - `sym` = string of elemental symbol for element of atomic number Z Outputs: - `Z` = atomic number ''' elms = ["n ",\ "H ","He","Li","Be","B ","C ","N ","O ","F ","Ne",\ "Na","Mg","Al","Si","P ","S ","Cl","Ar","K ","Ca",\ "Sc","Ti","V ","Cr","Mn","Fe","Co","Ni","Cu","Zn",\ "Ga","Ge","As","Se","Br","Kr","Rb","Sr","Y ","Zr",\ "Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn",\ "Sb","Te","I ","Xe","Cs","Ba","La","Ce","Pr","Nd",\ "Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb",\ "Lu","Hf","Ta","W ","Re","Os","Ir","Pt","Au","Hg",\ "Tl","Pb","Bi","Po","At","Rn","Fr","Ra","Ac","Th",\ "Pa","U ","Np","Pu","Am","Cm","Bk","Cf","Es","Fm",\ "Md","No","Lr","Rf","Db","Sg","Bh","Hs","Mt","Ds",\ "Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"] if len(sym.strip())>2: print('Please provide a valid elemental symbol (1 or 2 characters), {} is too long'.format(sym)) return -1 # handle exception for neutron first if sym.strip()=='XX': return 0 # make sure string is formatted to match entries in elms list sym2 = sym.strip() if len(sym2)==1: sym2 += ' ' sym2 = sym2[0].upper() + sym2[1].lower() Z = find(sym2,elms) if Z==None: print('Z could not be found for element "{}"; please make sure entry is correct.'.format(sym)) return -1 return Z
def kfcode_to_common_name(kf_code)
-
Description
Converts an integer kf-code to plaintext string of a particle/nuclide
Input
kf_code
= integer kf-code particle identification number (see PHITS manual Table 4.4)
Output
par_nuc_str
= string either naming the particle or describing the input nuclide formatted as [Symbol]-[A]
Expand source code
def kfcode_to_common_name(kf_code): ''' Description: Converts an integer kf-code to plaintext string of a particle/nuclide Input: - `kf_code` = integer kf-code particle identification number (see PHITS manual Table 4.4) Output: - `par_nuc_str` = string either naming the particle or describing the input nuclide formatted as [Symbol]-[A] ''' kf_code = int(kf_code) named_kf_codes = [2212 ,2112 ,22 ,11 ,-11 ,211 ,111 ,-211 ,-13 ,13 ,321 ,311 ,-321 ] named_kf_code_strs = ['proton','neutron','photon','electron','positron','pion+','pion0','pion-','muon+','muon-','kaon+','kaon0','kaon-'] if abs(kf_code) < 1000000: # specifically named particles if kf_code in named_kf_codes: i = find(kf_code,named_kf_codes) par_nuc_str = named_kf_code_strs[i] else: par_nuc_str = str(kf_code) else: A = kf_code % 1000000 Z = (kf_code-A) / 1000000 ZZZAAA = 1000*Z + A par_nuc_str = ZZZAAAM_to_nuclide_plain_str(ZZZAAA, ZZZAAA=True) return par_nuc_str
def is_number(n)
-
Description
Determine if a string is that of a number or not.
Inputs
n
= string to be tested
Outputs
True
if value is a number (can be converted to float() without an error)False
otherwise
Expand source code
def is_number(n): ''' Description: Determine if a string is that of a number or not. Inputs: - `n` = string to be tested Outputs: - `True` if value is a number (can be converted to float() without an error) - `False` otherwise ''' try: float(n) except ValueError: return False return True
def find(target, myList)
-
Description
Search for and return the index of the first occurance of a value in a list.
Inputs
target
= value to be searched formyList
= list of values
Output
- index of first instance of
target
inmyList
Expand source code
def find(target, myList): ''' Description: Search for and return the index of the first occurance of a value in a list. Inputs: - `target` = value to be searched for - `myList` = list of values Output: - index of first instance of `target` in `myList` ''' for i in range(len(myList)): if myList[i] == target: return i
def determine_PHITS_output_file_type(output_file)
-
Description
Determine what kind of PHITS file is being hanlded (tally standard output, binary tally dump, ASCII tally dump, etc.)
Inputs
output_file
= a file/filepath (string or Path object) to be judged
Outputs
PHITS_file_type
= a dictionary of Booleans detailing what kind of fileoutput_file
is (and isn't) with the following keys (each with a value set toTrue
orFalse
):'is_standard_tally_output'
,'is_binary_tally_dump'
,'is_ASCII_tally_dump'
,'is_PHITS_input_file'
,'is_file6_phitsout_file'
,'is_unknown_file_type'
, and'file_does_not_exist'
. By default, all are set toFalse
except for'is_unknown_file_type'
which isTrue
by default.
Notes
- Dump files are identified with the presence of '_dmp' in their filename. Whether it is ASCII or binary is simply determined by attempting to read the first line of the file in a try/except statement.
- Standard tally output files are identified by
[
being the very first character in the first line. - PHITS input files are identified by having a file extension in the following list (case insensitive):
['.inp','.in','.input','.i']
- 'phits.out' files (
file(6)
in the PHITS [Parameters] section) are identified by its first line consisting of 11 spaces followed by 57 underscores.
Expand source code
def determine_PHITS_output_file_type(output_file): ''' Description: Determine what kind of PHITS file is being hanlded (tally standard output, binary tally dump, ASCII tally dump, etc.) Inputs: - `output_file` = a file/filepath (string or Path object) to be judged Outputs: - `PHITS_file_type` = a dictionary of Booleans detailing what kind of file `output_file` is (and isn't) with the following keys (each with a value set to `True` or `False`): `'is_standard_tally_output'`, `'is_binary_tally_dump'`, `'is_ASCII_tally_dump'`, `'is_PHITS_input_file'`, `'is_file6_phitsout_file'`, `'is_unknown_file_type'`, and `'file_does_not_exist'`. By default, all are set to `False` except for `'is_unknown_file_type'` which is `True` by default. Notes: - Dump files are identified with the presence of '_dmp' in their filename. Whether it is ASCII or binary is simply determined by attempting to read the first line of the file in a try/except statement. - Standard tally output files are identified by `[` being the very first character in the first line. - PHITS input files are identified by having a file extension in the following list (case insensitive): `['.inp','.in','.input','.i']` - 'phits.out' files (`file(6)` in the PHITS [Parameters] section) are identified by its first line consisting of 11 spaces followed by 57 underscores. ''' import re PHITS_file_type = {'is_standard_tally_output': False, 'is_binary_tally_dump': False, 'is_ASCII_tally_dump': False, 'is_PHITS_input_file': False, 'is_file6_phitsout_file': False, 'is_unknown_file_type': True, 'file_does_not_exist': False } output_file = Path(output_file) if not output_file.is_file(): print('Provided output file',output_file,'was determined to not be a file!') PHITS_file_type['is_unknown_file_type'] = False PHITS_file_type['file_does_not_exist'] = True return PHITS_file_type with open(output_file, 'r', encoding='utf-8') as f: try: first_line = f.readline().strip() first_line_lower_no_spaces = re.sub(r"\s+", "", first_line, flags=re.UNICODE).lower() except: # triggered if encountering binary / non ASCII or UTF-8 file if '_dmp' in output_file.stem: PHITS_file_type['is_binary_tally_dump'] = True PHITS_file_type['is_unknown_file_type'] = False return PHITS_file_type if first_line_lower_no_spaces[:3] == '[t-': PHITS_file_type['is_standard_tally_output'] = True PHITS_file_type['is_unknown_file_type'] = False elif '_dmp' in output_file.stem: PHITS_file_type['is_ASCII_tally_dump'] = True PHITS_file_type['is_unknown_file_type'] = False elif output_file.suffix.lower() in ['.inp','.in','.input','.i']: PHITS_file_type['is_PHITS_input_file'] = True PHITS_file_type['is_unknown_file_type'] = False elif '_________________________________________________________' in first_line: PHITS_file_type['is_file6_phitsout_file'] = True PHITS_file_type['is_unknown_file_type'] = False return PHITS_file_type
def search_for_dump_parameters(output_file)
-
Description
Try to determine the dump settings used for a dump file by searching for the same file without "_dmp" and parsing its header for the "dump = " line and subsequent line specifying the column ordering.
Inputs
output_file
= a file/filepath (string or Path object) to be judged
Outputs
dump_data_number
= value following "dump = " in the PHITS tally (integer from -20 to 20, excluding 0) (D=None
)dump_data_sequence
= list of integers specifying the order and meaning of the dump file columns (D=None
)
Expand source code
def search_for_dump_parameters(output_file): ''' Description: Try to determine the dump settings used for a dump file by searching for the same file without "_dmp" and parsing its header for the "dump = " line and subsequent line specifying the column ordering. Inputs: - `output_file` = a file/filepath (string or Path object) to be judged Outputs: - `dump_data_number` = value following "dump = " in the PHITS tally (integer from -20 to 20, excluding 0) (D=`None`) - `dump_data_sequence` = list of integers specifying the order and meaning of the dump file columns (D=`None`) ''' dump_data_number, dump_data_sequence = None, None output_file = Path(output_file) output_file_suffixes = output_file.suffixes if len(output_file_suffixes) > 1 and is_number(output_file_suffixes[-1][1:]) and '_dmp' in output_file.stem: # MPI dump found origin_tally_file = Path(output_file.parent, output_file.stem.replace('_dmp', '')) else: origin_tally_file = Path(output_file.parent, output_file.stem.replace('_dmp','') + output_file.suffix) PHITS_file_type = determine_PHITS_output_file_type(origin_tally_file) if PHITS_file_type['file_does_not_exist']: print("Could not find this dump file's companion original standard tally output file",origin_tally_file) return dump_data_number, dump_data_sequence elif not PHITS_file_type['is_standard_tally_output']: print("Found dump file's suspected companion original standard tally output file, but it does not seem to actually be formatted as a standard tally output file",origin_tally_file) return dump_data_number, dump_data_sequence tally_header, tally_content = split_into_header_and_content(origin_tally_file) for li, line in enumerate(tally_header): if "dump =" in line: if line[0] == '#': # commented line key, value = extract_data_from_header_line(line[1:]) else: key, value = extract_data_from_header_line(line) dump_data_number = int(value) dump_data_sequence_str_list = tally_header[li+1].strip().split() dump_data_sequence = [int(i) for i in dump_data_sequence_str_list] break if dump_data_number == None and dump_data_sequence == None: print('Was unable to locate dump specification information in tally output file',origin_tally_file) return dump_data_number, dump_data_sequence
def split_into_header_and_content(output_file_path)
-
Description
Initial parsing of a PHITS tally output file to isolate its header section (containing metadata) and main tally results "content" section for later processing.
Inputs
output_file_path
= path to a PHITS tally output file
Outputs
header
= list of lines belonging to the tally output's header sectioncontent
= list of lists of remaining lines after the tally output's header section; the top level list is broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page.
Expand source code
def split_into_header_and_content(output_file_path): ''' Description: Initial parsing of a PHITS tally output file to isolate its header section (containing metadata) and main tally results "content" section for later processing. Inputs: - `output_file_path` = path to a PHITS tally output file Outputs: - `header` = list of lines belonging to the tally output's header section - `content` = list of lists of remaining lines after the tally output's header section; the top level list is broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page. ''' in_content = False header, content = [], [[]] with open(output_file_path, mode='rb') as f: for line in f: if b'\x00' in line: line = line.replace(b"\x00", b"") #line = line.decode() try: # skip lines with invalid characters in them... line = line.decode() except: continue #if "\x00" in line: line = line.replace("\x00", "") if '#newpage:' in line: in_content = True continue if in_content: if 'newpage:' in line: content.append([]) continue content[-1].append(line.strip()) else: header.append(line.strip()) # add "footer" to peel off last bit of "content" section? if in_debug_mode: print(len(content), 'content blocks (pages) found in tally output') return header, content
def parse_tally_header(tally_header, tally_content)
-
Description
Extracts metadata from PHITS tally output header (and some extra info from its contents section)
Dependencies
extract_data_from_header_line()
(function within the "PHITS tools" package)parse_group_string()
(function within the "PHITS tools" package)
Inputs
tally_header
= list of lines belonging to the tally output's header sectiontally_content
= list of lists of remaining lines after the tally output's header section; the top level list is broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page.
Outputs
meta
= Munch object / dictionary containing tally metadata
Expand source code
def parse_tally_header(tally_header,tally_content): ''' Description: Extracts metadata from PHITS tally output header (and some extra info from its contents section) Dependencies: - `extract_data_from_header_line` (function within the "PHITS tools" package) - `parse_group_string` (function within the "PHITS tools" package) Inputs: - `tally_header` = list of lines belonging to the tally output's header section - `tally_content` = list of lists of remaining lines after the tally output's header section; the top level list is broken into "blocks" ("newpage:"-separated) which are lists of lines belonging to each block/page. Outputs: - `meta` = Munch object / dictionary containing tally metadata ''' prefer_to_munch_meta_dict = True if prefer_to_munch_meta_dict: try: from munch import Munch use_munch = True except: use_munch = False else: use_munch = False nlines = len(tally_header) tally_type = tally_header[0].replace(' ','').replace('off','') if '[' not in tally_type and ']' not in tally_type: # file is not PHITS tally output if 'htitle' in tally_type: tally_type = '[T-Dchain]' else: tally_type = 'UNKNOWN' if use_munch: meta = Munch({}) else: meta = {} meta['tally_type'] = tally_type unsupported_tally_types = ['[T-WWG]', '[T-WWBG]', '[T-Volume]', '[T-Userdefined]', '[T-Gshow]', '[T-Rshow]', '[T-3Dshow]', '[T-4Dtrack]', '[T-Dchain]', 'UNKNOWN'] if tally_type in unsupported_tally_types: return meta # Initialize variables for possible array mesh_types = ['e','t','x','y','z','r','a','l'] for m in mesh_types: meta['n'+m] = None meta['reg'] = None meta['part'] = None meta['npart'] = None meta['nc'] = None meta['samepage'] = 'part' found_mesh_kinds = [] reading_axis_data = False reading_regions = False in_exceptional_mesh_kind = False for li, line in enumerate(tally_header): #if line[0]=='#': # commented line if 'data =' in line: # data section to parse reading_axis_data = True n_values_to_read = meta['n'+current_data_mesh_kind] + 1 remaining_n_values_to_read = n_values_to_read data_values = [] in_exceptional_mesh_kind = False #print('read ',n_values_to_read,current_data_mesh_kind,' values') continue elif '=' in line: if line[0] == '#': # commented line key, value = extract_data_from_header_line(line[1:]) if key=='file' and key in meta and '_dmp' in value: continue # do not overwrite existing file parameter with dump file else: key, value = extract_data_from_header_line(line) if in_exceptional_mesh_kind: if key[0]=='e': key = current_data_mesh_kind + key[1:] elif key=='ne': key = 'n' + current_data_mesh_kind meta[key] = value if (tally_type=='[T-Yield]' or tally_type=='[T-Track]') and key=='axis' and value=='dchain': # Unsupported tally output! return meta if 'type' in key: current_data_mesh_kind = key.replace('-type','') if current_data_mesh_kind == 'se': current_data_mesh_kind = 'e' current_data_mesh_type = value found_mesh_kinds.append(current_data_mesh_kind) if current_data_mesh_kind in ['e1','e2']: in_exceptional_mesh_kind = True #print(current_data_mesh_kind,current_data_mesh_type) if key=='part': part_groups = parse_group_string(str(value)) kf_groups = parse_group_string(tally_header[li + 1].split(':')[1]) if meta['npart'] == None: # first instance of "part" meta['part_groups'] = part_groups meta['kf_groups'] = kf_groups meta['npart'] = len(part_groups) meta['part_serial_groups'] = ['p'+str(gi+1)+'-group' for gi in range(len(part_groups))] else: # an additional occurance of part? if 'multiplier' not in tally_header[li - 1]: # the multiplier can also be followed by an erroneous "part" specification for pi,pg in enumerate(part_groups): if pg not in meta['part_groups']: meta['part_groups'] += [pg] meta['kf_groups'] += kf_groups[pi] meta['npart'] += 1 meta['part_serial_groups'] += ['p' + str(pi + 1) + '-group'] if key=='reg': if meta['tally_type']=='[T-Cross]': num_regs = value meta['num_reg_groups'] = num_regs meta['reg_groups'] = [] # manually read in reg groups li_start = li+2 li_stop = li_start + num_regs for lii in range(li_start,li_stop): non, rfrom, rto, area = tally_header[lii].split() meta['reg_groups'].append(rfrom+' - '+rto) else: reg_groups = parse_group_string(str(value)) eli = 0 # extra line index if '=' not in tally_header[eli+li+1] and 'volume' not in tally_header[eli+li+1]: # reg specification continues to next line while '=' not in tally_header[eli+li+1] and 'volume' not in tally_header[eli+li+1]: reg_groups += parse_group_string(tally_header[eli+li+1].strip()) eli += 1 if 'all' in reg_groups and 'volume' in tally_header[li+1] and '=' not in tally_header[eli+li+1]: # parse table of regions... found_reg_grps = [] meta['reg_groups_inputted'] = reg_groups reg_lines = tally_header[li+3:] for reg_line in reg_lines: if '=' in reg_line: break line_parts = reg_line.split('#') if len(line_parts) >= 2: found_reg_grps.append(line_parts[1].strip()) else: found_reg_grps.append(line_parts[0].split()[1]) meta['reg_groups'] = found_reg_grps meta['num_reg_groups'] = len(found_reg_grps) else: meta['reg_groups'] = reg_groups meta['num_reg_groups'] = len(reg_groups) if key == 'point': num_regs = value meta['point_detectors'] = {'non':[], 'x':[], 'y':[], 'z':[], 'r0':[]} # [T-Point] points li_start = li + 2 li_stop = li_start + num_regs for lii in range(li_start, li_stop): non, tppx, tppy, tppz, tppr0 = tally_header[lii].split() meta['point_detectors']['non'].append(non) meta['point_detectors']['x'].append(tppx) meta['point_detectors']['y'].append(tppy) meta['point_detectors']['z'].append(tppz) meta['point_detectors']['r0'].append(tppr0) if key == 'ring': num_regs = value meta['point_detectors'] = {'non':[], 'axis':[], 'ar':[], 'rr':[], 'r0':[]} # [T-Point] points li_start = li + 2 li_stop = li_start + num_regs for lii in range(li_start, li_stop): non, tppx, tppy, tppz, tppr0 = tally_header[lii].split() meta['point_detectors']['non'].append(non) meta['point_detectors']['axis'].append(tppx) meta['point_detectors']['ar'].append(tppy) meta['point_detectors']['rr'].append(tppz) meta['point_detectors']['r0'].append(tppr0) elif reading_axis_data: values = line.replace('#','').strip().split() for val in values: data_values.append(float(val)) remaining_n_values_to_read += -1 if remaining_n_values_to_read <= 0: reading_axis_data = False data_values = np.array(data_values) meta[current_data_mesh_kind+'-mesh_bin_edges'] = data_values meta[current_data_mesh_kind+'-mesh_bin_mids'] = 0.5*(data_values[1:]+data_values[:-1]) #meta[current_data_mesh_kind+'-mesh_bin_mids_log'] = np.sqrt(data_values[1:]*data_values[:-1]) # generate log-centered bin mids bin_mids_log = [] for i in range(len(data_values)-1): if data_values[i+1]<=0 or data_values[i]<=0: # if one or both edges <= 0 if data_values[i+1]<0 and data_values[i]<0: # both values are negative bin_mids_log.append(-1*np.sqrt(data_values[i]*data_values[i+1])) elif data_values[i+1]==0 or data_values[i]==0: # one value is zero # use linear center instead... bin_mids_log.append(0.5*(data_values[i]+data_values[i+1])) elif data_values[i+1]<0 or data_values[i]<0: # bin straddles zero # use linear center instead... bin_mids_log.append(0.5*(data_values[i]+data_values[i+1])) else: print('unknown binning encountered, skipping generation of log-scale bin mids for '+current_data_mesh_kind+'-mesh') break else: bin_mids_log.append(np.sqrt(data_values[i]*data_values[i+1])) meta[current_data_mesh_kind+'-mesh_bin_mids_log'] = np.array(bin_mids_log) continue else: continue meta['found_mesh_kinds'] = found_mesh_kinds if meta['tally_type']=='[T-Cross]': if meta['mesh']=='xyz': if 'enclos' in meta and meta['enclos']==1: pass # total items remains nx*ny*nz else: meta['nz_original'] = meta['nz'] meta['nz'] += 1 # zmesh surfaces are scored, making array nx*ny*(nz+1) elif meta['mesh']=='r-z': if 'enclos' in meta and meta['enclos']==1: pass # total items remains nr*nz else: # Current solution addresses this by expanding the ierr axis meta['nr_original'] = meta['nr'] meta['nz_original'] = meta['nz'] meta['nr'] = meta['nr'] + 1 meta['nz'] = meta['nz'] + 1 # OLD SOLUTION IMPLEMENTED IS BELOW # max total num of pages = nrsurf*nz + nzsurf*nr = (nr+1)*nz + nr*(nz+1) = 2*nr*nz + nr + nz # if one radius is 0, this becomes = nr*nz + nr*(nz+1) = 2*nr*nz + nr # Solution used here: # use ir to iterate nr, use iy to iterate nrsurf, use iz to iterate nz, use ic to iterate nzsurf # since only rsurf*z [iy,iz] and r*zsurf [ir,ic] pairs exist, when one pair is being written # the other will be [-1,-1], hence the dimensions for the array are increased by an extra 1 to prevent overlap #meta['nr_original'] = meta['nr'] #meta['nz_original'] = meta['nz'] #meta['ny_original'] = meta['ny'] ##meta['nc_original'] = meta['nc'] #meta['ny'] = meta['nr'] + 1 + 1 #meta['nc'] = meta['nz'] + 1 + 1 #meta['nr'] = meta['nr'] + 1 #meta['nz'] = meta['nz'] + 1 if meta['tally_type'] == '[T-Point]': if 'mesh' not in meta: if 'point' in meta: meta['mesh'] = 'point' meta['nreg'] = meta['point'] elif 'ring' in meta: meta['mesh'] = 'ring' meta['nreg'] = meta['ring'] axes_1D = ['eng','reg','x','y','z','r','t','cos','the','mass','charge','let','tet','eng1','eng2','sed','rad','deg','act'] axes_2D = ['xy','yz','zx','rz','chart','dchain','t-eng','eng-t','t-e1','e1-t','t-e2','e2-t','e12','e21','xz','yx','zy','zr'] axes_ital_1D = [3, 0, 0, 1, 2, 0, 4, 5, 5, 8, 8, 6, 0, 3, 8, 3, 5, 5, 8] axes_ital_2D = [ [0,1],[1,2],[2,0],[0,2],[None,None],[None,None],[4,3],[3,4],[4,3],[3,4],[4,8],[8,4],[3,8],[8,3],[0,2],[1,0],[2,1],[2,0]] if meta['axis'] in axes_1D: meta['axis_dimensions'] = 1 meta['axis_index_of_tally_array'] = axes_ital_1D[axes_1D.index(meta['axis'])] elif meta['axis'] in axes_2D: meta['axis_dimensions'] = 2 meta['axis_index_of_tally_array'] = axes_ital_2D[axes_2D.index(meta['axis'])] else: print("WARNING: axis value of ",meta['axis']," is not in list of known/registered values") meta['axis_dimensions'] = None meta['axis_index_of_tally_array'] = None # Now extract portion of metadata only available from tally content if meta['mesh'] == 'reg' or meta['mesh'] == 'tet': num, reg, vol = [], [], [] if meta['axis']=='reg' or meta['axis']=='tet': # get number of regions and region data from first block of tally content outblock = tally_content[0] in_reg_list = False for line in outblock: if '#' in line and ' num ' in line: cols = line[1:].split() #print(cols) in_reg_list = True continue if len(line.split()) == 0 or '{' in line: in_reg_list = False if in_reg_list: vals = line.split() if meta['tally_type'] == '[T-Cross]': num.append(vals[0]) reg.append(vals[0]) vol.append(vals[1]) else: num.append(vals[0]) reg.append(vals[1]) vol.append(vals[2]) else: # scan output for region numbers: regcount = 0 for outblock in tally_content: for line in outblock: if 'reg =' in line or 'reg =' in line: eq_strs = split_str_of_equalities(line[1:]) reg_eq_str = '' for eqsi in eq_strs: if 'reg' in eqsi: reg_eq_str = eqsi break regnum = reg_eq_str.split('=')[1].strip() #regnum = line.strip().split('reg =')[1].strip().replace("'",'') if regnum not in reg: regcount += 1 num.append(regcount) reg.append(regnum) vol.append(None) continue if meta['mesh'] == 'reg': meta['reg_serial_num'] = num meta['reg_num'] = reg if meta['tally_type'] == '[T-Cross]': meta['reg_area'] = vol else: meta['reg_volume'] = vol meta['nreg'] = len(reg) elif meta['mesh'] == 'tet': meta['tet_serial_num'] = num meta['tet_num'] = reg meta['reg_num'] = reg #meta['tet_volume'] = vol if meta['tally_type'] == '[T-Cross]': meta['tet_area'] = vol else: meta['tet_volume'] = vol meta['ntet'] = len(reg) #if meta['tally_type'] == '[T-Cross]': # meta['reg_groups'] = reg elif meta['mesh'] == 'tet': num, reg, vol = [], [], [] if meta['axis'] == 'tet': pass else: pass print('mesh=tet has not been tested!') meta['ntet'] = 0 axis1_label = '' axis2_label = '' value_label = '' hc_passed = False # passed colorbar definition line outblock = tally_content[0] for line in outblock: if len(line) == 0: continue if line[:2] == 'x:': axis1_label = line[2:].strip() if line[:2] == 'y:': if meta['axis_dimensions'] == 1: value_label = line[2:].strip() #break elif meta['axis_dimensions'] == 2: if hc_passed: # second instance of y: value_label = line[2:].strip() break else: # first instance of y: axis2_label = line[2:].strip() hc_passed = True #if line[:3] == 'hc:': # hc_passed = True h_line_str = '' if line[0] == 'h' and (line[1] == ':' or line[2] == ':'): if meta['axis_dimensions'] == 1: ndatacol = line.count(' y') if ndatacol != 1: # multiple columns are present "samepage" # get first string with y col_groups = parse_group_string(line) i_first_y = next((i for i,v in enumerate(col_groups) if v[0]=='y'), None) # index of first column with "y" first_data_col_header = col_groups[i_first_y][2:] for m in mesh_types: if first_data_col_header[0] == m: if m == 'e': meta['samepage'] = 'eng' elif m == 'r': if first_data_col_header[:3] == 'reg': meta['samepage'] = 'reg' else: meta['samepage'] = m elif m == 'l': meta['samepage'] = 'let' elif m == 'a': if first_data_col_header[:3] not in ['all','alp']: meta['samepage'] = 'the' # or cos else: meta['samepage'] = m if meta['samepage'] == 'part': # still is default value # double check to see if it could be region numbers vs particle names if ndatacol != meta['npart']: if 'num_reg_groups' in meta and ndatacol == meta['num_reg_groups']: meta['samepage'] = 'reg' else: print('"samepage" was not correctly identified; needs to be implemented') if meta['samepage'] == 'reg': hcols = parse_group_string(line[3:]) num, reg, vol = [], [], [] reg_ser_num = 1 for hcol in hcols: if hcol[0] == 'y': num.append(reg_ser_num) reg_ser_num += 1 reg.append(hcol.split(')')[0].replace('y(reg','')) vol.append(None) meta['reg_serial_num'] = num meta['reg_num'] = reg meta['reg_volume'] = vol meta['nreg'] = len(reg) break meta['axis1_label'] = axis1_label meta['axis2_label'] = axis2_label meta['value_label'] = value_label # Now do any final overrides for specific tallies / circumstances if meta['tally_type'] == '[T-Deposit2]': meta['nreg'] = 1 meta['reg_serial_num'] = [1] meta['reg_num'] = ['1'] meta['reg_volume'] = [None] if meta['num_reg_groups'] > 1: meta['num_reg_groups'] = 1 meta['reg_groups'] = [meta['reg_groups'][0] + ' ' + meta['reg_groups'][1]] if meta['tally_type'] == '[T-Heat]': if 'npart' not in meta or meta['npart'] == None: meta['npart'] = 1 if 'part_groups' not in meta: meta['part_groups'] = ['all'] return meta
def parse_tally_content(tdata, meta, tally_blocks, is_err_in_separate_file, err_mode=False)
-
Description
Parses the PHITS tally output content section and extract its results
Dependencies
split_str_of_equalities()
(function within the "PHITS tools" package)parse_group_string()
(function within the "PHITS tools" package)data_row_to_num_list()
(function within the "PHITS tools" package)
Inputs
tdata
= 10-dimensional NumPy array of zeros of correct size to hold tally output/resultsmeta
= Munch object / dictionary containing tally metadatatally_blocks
= blocks of tally output as outputted by thesplit_into_header_and_content()
functionis_err_in_separate_file
= Boolean denoting whether the tally's relative errors are located in a separate fileerr_mode
= Boolean (D=False
) used for manually forcing all read values to be regarded as relative uncertainties as is necessary when processing dedicated *_err files.
Outputs
tdata
= updatedtdata
array containing read/extracted tally results
Expand source code
def parse_tally_content(tdata,meta,tally_blocks,is_err_in_separate_file,err_mode=False): ''' Description: Parses the PHITS tally output content section and extract its results Dependencies: - `split_str_of_equalities` (function within the "PHITS tools" package) - `parse_group_string` (function within the "PHITS tools" package) - `data_row_to_num_list` (function within the "PHITS tools" package) Inputs: - `tdata` = 10-dimensional NumPy array of zeros of correct size to hold tally output/results - `meta` = Munch object / dictionary containing tally metadata - `tally_blocks` = blocks of tally output as outputted by the `split_into_header_and_content` function - `is_err_in_separate_file` = Boolean denoting whether the tally's relative errors are located in a separate file - `err_mode` = Boolean (D=`False`) used for manually forcing all read values to be regarded as relative uncertainties as is necessary when processing dedicated *_err files. Outputs: - `tdata` = updated `tdata` array containing read/extracted tally results ''' global ir, iy, iz, ie, it, ia, il, ip, ic, ierr global ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max ierr = 0 if is_err_in_separate_file and err_mode: ierr = 1 mesh_kind_chars = ['e', 't', 'x', 'y', 'z', 'r', 'a', 'l'] mesh_kind_iax = [3, 4, 0, 1, 2, 0, 5, 6] tdata_ivar_strs = ['ir', 'iy', 'iz', 'ie', 'it', 'ia', 'il', 'ip', 'ic'] ir, iy, iz, ie, it, ia, il, ip, ic = 0, 0, 0, 0, 0, 0, 0, 0, 0 ignored_eq_strs = ['axis','axs','ar','rr','m jm','Z','cmax nmax'] replace_eq_strs_dict = {'ang':'a'} ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata) axes_1D = ['eng', 'reg', 'x', 'y', 'z', 'r', 't', 'cos', 'the', 'mass', 'charge', 'let', 'tet', 'eng1', 'eng2', 'sed', 'rad', 'deg', 'act'] axes_2D = ['xy', 'yz', 'zx', 'rz', 'chart', 'dchain', 't-eng', 'eng-t', 't-e1', 'e1-t', 't-e2', 'e2-t', 'e12', 'e21', 'xz', 'yx', 'zy', 'zr'] axes_ital_1D = [3, 0, 0, 1, 2, 0, 4, 5, 5, 8, 8, 6, 0, 3, 8, 3, 5, 5, 8] axes_ital_2D = [[0, 1], [1, 2], [2, 0], [0, 2], [None, None], [None, None], [4, 3], [3, 4], [4, 3], [3, 4], [4, 8], [8, 4], [3, 8], [8, 3], [0, 2], [1, 0], [2, 1], [2, 0]] ierr_mod = 0 # add to ierr for weird [T-Cross], mesh=r-z, enclos=0 case banked_uninterpreted_lines = [] # store lines with equalities that may be useful but are skipped owing to being a bit exceptional i_metastable = 0 ZZZAAAM_list = [] if meta['axis_dimensions']==1: for bi, block in enumerate(tally_blocks): hli, fli = 0,0 ierr_mod = 0 hli_found = False for li, line in enumerate(block): if len(line) == 0: continue if line[:2].lower() == 'h:': # start of data is here hli = li hli_found = True continue if hli_found and (line[:12] == '# sum over' or line[:7] == '# sum' or line[:5] == '#----' or line[:12] == '# overflow' or (len(block[li-1]) == 0 and hli != 0 and li>hli+2) or "'" in line or '{' in line): fli = li if (len(block[li-1]) == 0 and hli != 0 and li>hli+2): fli = li - 1 # triggered by blank line after data #if "'" in line or '{' in line: # fli = li-1 break data_header = block[:hli] data_table = block[hli:fli] data_footer = block[fli:] if bi == len(tally_blocks) - 1: ffli = len(data_footer) for li, line in enumerate(data_footer): if line[:37] == '# Information for Restart Calculation': ffli = li break data_footer = data_footer[:ffli] # print(data_header) #print(data_table) # print(data_footer) hash_line_already_evaluated = False # try to get relevant indices data from header and footer blocks for li, line in enumerate(data_header+data_footer): if len(line) == 0: continue if '=' in line and (line[0] == "'" or (line[0] == "#" and ('no.' in line or 'i' in line or 'reg' in line or 'part' in line))): if line[0] == "#": hash_line_already_evaluated = True elif line[0] == "'" and hash_line_already_evaluated: if meta['samepage'] == 'part': continue # '-starting lines tend to have more problematic formatting, best skipped if possible elif meta['npart'] == 1: continue # can still skip if only one particle group tallied else: pass # but this needs to be parsed if not using samepage = part and npart > 1 parts = split_str_of_equalities(line) #print(line) for part in parts: mesh_char = part.split('=')[0].strip().replace('i','') #print(mesh_char) if mesh_char == 'no.': if '***' in part and bi<998: break # this is a bugged line (or "no." is 4+ digits, which is fine) continue elif mesh_char == 'part.' or mesh_char == 'partcle' or mesh_char == 'part': part_grp_name = part.split('=')[1].strip() if part_grp_name in meta['part_groups']: ip = (meta['part_groups']).index(part_grp_name) elif part_grp_name in meta['part_serial_groups']: ip = (meta['part_serial_groups']).index(part_grp_name) else: raise ValueError('ERROR! Particle "'+part_grp_name+'" could not be identified.') elif mesh_char == 'reg': regnum = part.split('=')[1].strip() ir = (meta['reg_num']).index(regnum) elif mesh_char == 'pont' or mesh_char == 'rng': # [T-Point] value_str = part.split('=')[1].strip() ir = int(value_str) - 1 elif mesh_char == 'e1': # [T-Deposit2] value_str = part.split('=')[1].strip() ie = int(value_str) - 1 elif mesh_char == 'e2': # [T-Deposit2] value_str = part.split('=')[1].strip() ic = int(value_str) - 1 elif mesh_char in mesh_kind_chars or mesh_char in replace_eq_strs_dict: if mesh_char in replace_eq_strs_dict: mesh_char = replace_eq_strs_dict[mesh_char] if 'i'+mesh_char not in part: continue # only looking for indices for meshes, not values imesh = mesh_kind_chars.index(mesh_char) itdata_axis = mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value_str = part.split('=')[1].strip() if ' - ' in value_str: vals = value_str.split('-') if int(vals[0]) == int(vals[1]): value_str = vals[0] else: # samepage axis value_str = vals[0] # this will be overwritten later value = str(int(value_str)-1) exec(tdata_ivar_str + ' = ' + value, globals()) elif mesh_char in ignored_eq_strs: continue elif meta['tally_type']=='[T-Cross]': if meta['mesh'] == 'xyz' and mesh_char=='z surf': #imesh = mesh_kind_chars.index('z') itdata_axis = 2 #mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value_str = part.split('=')[1].strip() value = str(int(value_str) - 1) exec(tdata_ivar_str + ' = ' + value, globals()) elif meta['mesh'] == 'r-z': if mesh_char=='r surf': itdata_axis = 0 # mesh_kind_iax[imesh] #itdata_axis = 1 # set to iy ierr_mod = int(ierr_max/2) #ir, ic = -1, -1 # imesh = mesh_kind_chars.index('y') elif mesh_char == 'z surf': itdata_axis = 2 # mesh_kind_iax[imesh] #itdata_axis = 8 # set to ic ierr_mod = 0 #iy, iz = -1, -1 # imesh = mesh_kind_chars.index('c') else: raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+'] found') tdata_ivar_str = tdata_ivar_strs[itdata_axis] value_str = part.split('=')[1].strip() if ' - ' in value_str: vals = value_str.split('-') if int(vals[0]) == int(vals[1]): value_str = vals[0] else: # samepage axis value_str = vals[0] # this will be overwritten later value = str(int(value_str) - 1) exec(tdata_ivar_str + ' = ' + value, globals()) else: raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+ '] found') elif meta['tally_type'] == '[T-Heat]': banked_uninterpreted_lines.append(line) else: raise ValueError('ERROR! Unregistered potential index ['+part.split('=')[0].strip()+'] found') if in_debug_mode: print('\tcontent block',bi,', indices:', ir, iy, iz, ie, it, ia, il, ip, ic) # extract data from table # determine meaning of table rows row_ivar = tdata_ivar_strs[meta['axis_index_of_tally_array']] # determine meaning of table columns hcols = parse_group_string(data_table[0][3:]) nhcols = len(hcols) col_names_line_str = data_table[1][1:] icol_mod = 0 # account for weirdness in column presence/absence if 'r surface position' in col_names_line_str: icol_mod = -1 ierr_mod = int(ierr_max / 2) # Test for error in hcols num_data_vals_in_first_row = len(data_row_to_num_list(data_table[2])) # first row of data if num_data_vals_in_first_row != nhcols: if num_data_vals_in_first_row == (nhcols+1): # most likely issue is hcol string is missing the "n" for the ?-lower column nhcols = nhcols + 1 icol_mod = 1 is_col_data = np.full(nhcols,False) data_col_indices = [] is_col_err = np.full(nhcols,False) err_col_indices = [] for iii in range(len(hcols)): if hcols[iii][0] == 'y': is_col_data[iii+icol_mod] = True is_col_err[iii+1+icol_mod] = True data_col_indices.append(iii+icol_mod) err_col_indices.append(iii+1+icol_mod) #print(is_col_data) #print(is_col_err) cols = data_table[1][1:].strip().split() ncols = len(cols) ndata_cols = np.sum(is_col_data) # number of data values per row # determine what variable this corresponds to, should be val of samepage # by default, this is usually particles (samepage = part by default) if meta['samepage'] == 'part': if meta['npart'] != ndata_cols: raise ValueError('ERROR! samepage number of particle types ('+str(meta['npart'])+') not equal to number of data columns y(part) = '+str(ndata_cols)) data_ivar = 'ip' data_ivar_indices = [j for j in range(ndata_cols)] else: # figure out what axis samepage is on if meta['samepage'] not in axes_1D: raise ValueError('ERROR! samepage parameter ('+str(meta['samepage'])+') must be "part" or one of valid options for "axis" parameter') data_ivar = tdata_ivar_strs[axes_ital_1D[axes_1D.index(meta['samepage'])]] if ndata_cols != eval(data_ivar+'_max'): if meta['tally_type']=='[T-Cross]' and ndata_cols+1 == eval(data_ivar+'_max'): # This is fine; for T-Cross, ndata cols can be one less than max length... pass elif meta['tally_type']=='[T-Cross]' and data_ivar == 'ir' and ndata_cols+2 == eval(data_ivar+'_max'): # This is fine; for T-Cross, ndata cols for radius can be two less than max length if rmin=0... pass else: raise ValueError('ERROR! number of data columns ('+str(ndata_cols)+') not equal to tally array dimension for '+str(data_ivar)+', '+str(eval(data_ivar+'_max'))) data_ivar_indices = [j for j in range(ndata_cols)] #print(cols) #print(ndata_cols) for li, line in enumerate(data_table[2:]): if len(line)==0: continue #print(line) rowi = li exec(row_ivar + '=' + str(rowi),globals()) #print(row_ivar + '=' + str(rowi)) values = data_row_to_num_list(line) dcoli = 0 ecoli = 0 for vi, value in enumerate(values): if is_col_data[vi]: exec(data_ivar + '=' + str(dcoli),globals()) #print(data_ivar + '=' + str(dcoli)) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0+ierr_mod] = value dcoli += 1 if is_col_err[vi]: exec(data_ivar + '=' + str(ecoli),globals()) #print(data_ivar + '=' + str(ecoli)) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1+ierr_mod] = value ecoli += 1 elif meta['axis_dimensions']==2: for bi, block in enumerate(tally_blocks): hli, fli = 0 , 0 data_keyword_found = False for li, line in enumerate(block): if meta['2D-type'] in [1, 2, 3, 6, 7]: if len(line) == 0: continue if line[:3].lower() in ['hc:', 'h2:', 'hd:', 'hc2']: # start of data is here hli = li if line[:12] == '#-----------': fli = li #if bi != len(tally_blocks) - 1: break elif meta['2D-type'] == 4: if line == '' and hli != 0: fli = li #if bi != len(tally_blocks) - 1: break elif line == '': # start of data is here hli = li elif meta['2D-type'] == 5: if 'data' in line: hli = li + 3 if line == '' and hli != 0 and li>hli+2: fli = li #if bi != len(tally_blocks) - 1: break data_header = block[:hli] data_table = block[hli:fli] data_footer = block[fli:] #print(data_header) #print(data_table) #print(data_footer) hash_line_already_evaluated = False if bi == len(tally_blocks) - 1: for li, line in enumerate(data_footer): if line[:37] == '# Information for Restart Calculation': ffli = li break data_footer = data_footer[:ffli] # try to get relevant indices data from header block for li, line in enumerate(data_header+data_footer): # +data_footer if len(line) == 0: continue #if 'reg =' in line: # regnum = line.strip().split('reg =')[1].strip() # ir = (meta.reg_num).index(regnum) # # print(ir) if '=' in line and (line[0] == "'" or (line[0] == "#" and ('no.' in line or 'i' in line or 'reg' in line or 'part' in line))): if line[0] == "#": hash_line_already_evaluated = True elif line[0] == "'" and hash_line_already_evaluated: if meta['samepage'] == 'part': continue # '-starting lines tend to have more problematic formatting, best skipped if possible elif meta['npart'] == 1: continue # can still skip if only one particle group tallied else: pass # but this needs to be parsed if not using samepage = part and npart > 1 parts = split_str_of_equalities(line) for part in parts: mesh_char = part.split('=')[0].strip().replace('i', '') #print(mesh_char) if mesh_char == 'no.': continue elif mesh_char == 'part.' or mesh_char == 'partcle': part_grp_name = part.split('=')[1].strip() try: ip = (meta['part_groups']).index(part_grp_name) except: ip = (meta['part_serial_groups']).index(part_grp_name) elif mesh_char == 'reg': # and meta['samepage'] != 'reg': regnum = part.split('=')[1].strip() ir = (meta['reg_num']).index(regnum) elif mesh_char == 'e1': # [T-Deposit2] value_str = part.split('=')[1].strip() ie = int(value_str) - 1 elif mesh_char == 'e2': # [T-Deposit2] value_str = part.split('=')[1].strip() ic = int(value_str) - 1 elif mesh_char in mesh_kind_chars or mesh_char in replace_eq_strs_dict: if mesh_char in replace_eq_strs_dict: mesh_char = replace_eq_strs_dict[mesh_char] if 'i'+mesh_char not in part: continue # only looking for indices for meshes, not values imesh = mesh_kind_chars.index(mesh_char) itdata_axis = mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value = str(int(part.split('=')[1].strip()) - 1) if mesh_char == 'l' and meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart': i_metastable = int(value) + 1 il = 0 else: exec(tdata_ivar_str + ' = ' + value, globals()) elif mesh_char in ignored_eq_strs: continue elif meta['tally_type']=='[T-Cross]': ierr_mod = 0 if meta['mesh'] == 'xyz' and mesh_char=='z surf': #imesh = mesh_kind_chars.index('z') itdata_axis = 2 #mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value = str(int(part.split('=')[1].strip()) - 1) exec(tdata_ivar_str + ' = ' + value, globals()) elif meta['mesh'] == 'r-z': if mesh_char=='r surf': # imesh = mesh_kind_chars.index('y') itdata_axis = 0 #1 # mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value = str(int(part.split('=')[1].strip()) - 1) exec(tdata_ivar_str + ' = ' + value, globals()) #ir, ic = -1, -1 ierr_mod = int(ierr_max / 2) elif mesh_char=='z surf': # imesh = mesh_kind_chars.index('c') itdata_axis = 2 #8 # mesh_kind_iax[imesh] tdata_ivar_str = tdata_ivar_strs[itdata_axis] value = str(int(part.split('=')[1].strip()) - 1) exec(tdata_ivar_str + ' = ' + value, globals()) iy, iz = -1, -1 ierr_mod = 0 else: raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+'] found') else: raise ValueError('ERROR! Unregistered potential index ['+ part.split('=')[0].strip()+ '] found') else: raise ValueError('ERROR! Unregistered potential index ['+part.split('=')[0].strip()+'] found') # Now read data_table, with formatting dependent on 2D-type, and can be inferred from last line of header axis1_ivar = meta['axis_index_of_tally_array'][0] axis2_ivar = meta['axis_index_of_tally_array'][1] if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart': # this setting does not respect 2D-type and uses its own formatting data_write_format_str = data_table[0][3:] Z_y_segment = data_write_format_str.split(';')[0] N_x_segment = data_write_format_str.split(';')[1] Z_y_vals = Z_y_segment.replace('=','').replace('to','').replace('by','').replace('y','').strip().split() N_x_vals = N_x_segment.replace('=','').replace('to','').replace('by','').replace('x','').strip().split() Z_y_max, Z_y_min, Z_y_increment = int(Z_y_vals[0]), int(Z_y_vals[1]), int(Z_y_vals[2]) N_x_max, N_x_min, N_x_increment = int(N_x_vals[1]), int(N_x_vals[0]), int(N_x_vals[2]) #print(Z_y_max, Z_y_min, Z_y_increment, N_x_max, N_x_min, N_x_increment ) elif meta['2D-type'] != 4: data_write_format_str = data_header[-2][1:] if 'data' not in data_write_format_str: for line in data_header[::-1]: if 'data' in line: data_write_format_str = line[1:] break #print(data_write_format_str) if 'data' not in data_write_format_str: # failed to find a "data" line telling us how the values are ordered # have to make guesses about output ordering... # axis variable should give us a hint axis = meta['axis'] if 'eng' in axis or 'e1' in axis or 'e2' in axis: if axis == 'e12' or axis == 'e21': ax1_ivar = 'ie' ax2_ivar = 'ie' else: # energy vs time if axis[0] == 't': ax1_ivar = 'it' ax2_ivar = 'ie' else: ax1_ivar = 'ie' ax2_ivar = 'it' else: if 'axis1_label' in meta and meta['axis1_label'][0] in axis: # we know horizontal axis variable ax1_ivar = 'i' + meta['axis1_label'][0] ax2_ivar = 'i' + axis.replace(meta['axis1_label'][0],'') else: ax1_ivar = 'i' + meta['axis'][1] ax2_ivar = 'i' + meta['axis'][0] else: # We can, with confidence, determine output value ordering :) for dsi in data_write_format_str.split(): if 'data' in dsi: data_index_str = dsi ax_vars = data_index_str.replace('data','').replace('(','').replace(')','') #print(data_index_str) #print(ax_vars) ax1_ivar, ax2_ivar = ax_vars.split(',')[:2] ax1_ivar = 'i' + ax1_ivar ax2_ivar = 'i' + ax2_ivar #print(data_write_format_str) else: # 2D-type = 4 cols = data_table[1][1:].split() ax1_ivar, ax2_ivar = cols[0], cols[1] ax1_ivar = 'i' + ax1_ivar ax2_ivar = 'i' + ax2_ivar # manually fix [T-Deposit2] axes if meta['tally_type'] == '[T-Deposit2]': if meta['axis'] == 'e12': ax1_ivar, ax2_ivar = 'ie', 'ic' elif meta['axis'] == 'e21': ax1_ivar, ax2_ivar = 'ic', 'ie' elif meta['axis'] == 't-e1': ax1_ivar, ax2_ivar = 'it', 'ie' elif meta['axis'] == 't-e2': ax1_ivar, ax2_ivar = 'it', 'ic' elif meta['axis'] == 'e1-t': ax1_ivar, ax2_ivar = 'ie', 'it' elif meta['axis'] == 'e2-t': ax1_ivar, ax2_ivar = 'ic', 'it' if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart': remaining_ndata_to_read = (Z_y_max - Z_y_min + 1) * (N_x_max - N_x_min + 1) else: # check if this is one of the backwards instances expected_ax1_ivar = tdata_ivar_strs[axis1_ivar] expected_ax2_ivar = tdata_ivar_strs[axis2_ivar] if meta['mesh']=='xyz': if expected_ax1_ivar == 'ir': expected_ax1_ivar = 'ix' if expected_ax2_ivar == 'ir': expected_ax1_ivar = 'ix' if ax1_ivar==expected_ax1_ivar and ax2_ivar==expected_ax2_ivar: pass # all is correct as is elif ax2_ivar == expected_ax1_ivar and ax1_ivar == expected_ax2_ivar: axis1_ivar_temp = axis1_ivar axis1_ivar = axis2_ivar axis2_ivar = axis1_ivar_temp #axis1_ivar = tdata_ivar_strs.index(ax1_ivar) #axis2_ivar = tdata_ivar_strs.index(ax2_ivar) #print('backwards!') else: raise ValueError('ERROR! Unknown axes ('+ax1_ivar+' '+ax2_ivar + ') encountered that did not match expected axes (' + tdata_ivar_strs[meta['axis_index_of_tally_array'][0]]+' ' + tdata_ivar_strs[meta['axis_index_of_tally_array'][1]]+')') axis1_ivar_str = tdata_ivar_strs[axis1_ivar] axis2_ivar_str = tdata_ivar_strs[axis2_ivar] axis1_size = np.shape(tdata)[axis1_ivar] axis2_size = np.shape(tdata)[axis2_ivar] ndata_to_read = axis1_size*axis2_size #print(axis1_ivar_str,axis2_ivar_str) #print(axis1_size,axis2_size,ndata_to_read) remaining_ndata_to_read = ndata_to_read iax1 = 0 iax2 = axis2_size - 1 if meta['tally_type'] == '[T-Yield]' and meta['axis'] == 'chart': #Z_y_max, Z_y_min, Z_y_increment # big, 1, -1 #N_x_max, N_x_min, N_x_increment # big, 1, 1 current_Z = Z_y_max current_N = N_x_min - N_x_increment ic = 0 for line in data_table[1:]: values = data_row_to_num_list(line) for value in values: remaining_ndata_to_read += -1 current_N += N_x_increment if current_N > N_x_max: current_N = N_x_min current_Z += Z_y_increment #print('Z=',current_Z,', N=',current_N) if value != 0: ZZZAAAM = 10000*current_Z + 10*(current_Z+current_N) + i_metastable if ZZZAAAM not in ZZZAAAM_list: ic = len(ZZZAAAM_list) ZZZAAAM_list.append(ZZZAAAM) else: ic = ZZZAAAM_list.index(ZZZAAAM) #print(ic, i_metastable) #print(ic,value) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value if remaining_ndata_to_read <= 0: break elif meta['2D-type'] in [1,2,3,6,7]: for line in data_table[1:]: values = data_row_to_num_list(line) #print(line) for value in values: exec(axis1_ivar_str + ' = ' + str(iax1), globals()) exec(axis2_ivar_str + ' = ' + str(iax2), globals()) #print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr, '\t', value) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value remaining_ndata_to_read += -1 #print(iax1, iax2) iax1 += 1 if iax1 == axis1_size: iax1 = 0 iax2 += -1 if remaining_ndata_to_read <= 0: break elif meta['2D-type'] == 4: iax2 = 0 for line in data_table[2:]: values = data_row_to_num_list(line) value = values[2] value_err = values[3] exec(axis1_ivar_str + ' = ' + str(iax1), globals()) exec(axis2_ivar_str + ' = ' + str(iax2), globals()) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0 + ierr_mod] = value tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1 + ierr_mod] = value_err # print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr,'\t',value) remaining_ndata_to_read += -1 # print(iax1, iax2) iax1 += 1 if iax1 == axis1_size: iax1 = 0 iax2 += 1 if remaining_ndata_to_read <= 0: break elif meta['2D-type'] == 5: for line in data_table[2:]: values = data_row_to_num_list(line) #print(line) for vi, value in enumerate(values): if vi==0: continue # header column exec(axis1_ivar_str + ' = ' + str(iax1), globals()) exec(axis2_ivar_str + ' = ' + str(iax2), globals()) #print(ir, iy, iz, ie, it, ia, il, ip, ic, ierr, '\t', value) tdata[ir, iy, iz, ie, it, ia, il, ip, ic, ierr + ierr_mod] = value remaining_ndata_to_read += -1 # print(iax1, iax2) iax1 += 1 if iax1 == axis1_size: iax1 = 0 iax2 += -1 if remaining_ndata_to_read <= 0: break else: raise ValueError('ERROR! unsupported 2D-type of '+str(meta['2D-type'])+' provided; legal values are [1,2,3,4,5,6,7]') else: raise ValueError(str(meta['axis_dimensions'])+'axis dimensions is unknown, ERROR!') if len(banked_uninterpreted_lines) != 0: print('The following potentially useful output lines were found but not stored anywhere:') for line in banked_uninterpreted_lines: print('\t'+line) return_updated_metadata_too = False if meta['tally_type'] == '[T-Yield]' and meta['axis'] in ['chart','charge','mass']: return_updated_metadata_too = True if meta['axis'] == 'chart': meta['nuclide_ZZZAAAM_list'] = ZZZAAAM_list meta['nuclide_isomer_list'] = [ZZZAAAM_to_nuclide_plain_str(i) for i in ZZZAAAM_list] nc_max = len(ZZZAAAM_list) #+ 1 meta['nc'] = nc_max tdata = tdata[:,:,:,:,:,:,:,:,:nc_max,:] elif meta['axis'] == 'charge' or meta['axis'] == 'mass': ic_axis_tdata_sum = tdata.sum(axis=(0,1,2,3,4,5,6,7,9)) nc_max = np.max(np.nonzero(ic_axis_tdata_sum)) + 1 meta['nc'] = nc_max tdata = tdata[:, :, :, :, :, :, :, :, :nc_max, :] if return_updated_metadata_too: return tdata, meta else: return tdata
def extract_data_from_header_line(line)
-
Description
Extract a "key" and its corresponding value from a PHITS tally output header line
Dependencies
is_number()
(function within the "PHITS tools" package)
Inputs
line
= string to be processed
Outputs
key
= a string "key" to become a key in the metadata dictionaryvalue
= corresponding value they "key" is equal to; dtype is string, int, or float
Expand source code
def extract_data_from_header_line(line): ''' Description: Extract a "key" and its corresponding value from a PHITS tally output header line Dependencies: - `is_number` (function within the "PHITS tools" package) Inputs: - `line` = string to be processed Outputs: - `key` = a string "key" to become a key in the metadata dictionary - `value` = corresponding value they "key" is equal to; dtype is string, int, or float ''' if '#' in line: info, trash = line.split('#',1) else: info = line key, value = info.split('=') key = key.strip() value = value.strip() if is_number(value): if '.' in value: value = float(value) else: value = int(value) return key, value
def split_str_of_equalities(text)
-
Description
Extract relevant regions, indices, etc. from somewhat inconsistently formatted lines in PHITS tally output content section.
Dependencies
is_number()
(function within the "PHITS tools" package)
Inputs
text
= string to be processed
Outputs
equalities_str_list
= list of strings of equalities each of the format "key = value"
Expand source code
def split_str_of_equalities(text): ''' Description: Extract relevant regions, indices, etc. from somewhat inconsistently formatted lines in PHITS tally output content section. Dependencies: - `is_number` (function within the "PHITS tools" package) Inputs: - `text` = string to be processed Outputs: - `equalities_str_list` = list of strings of equalities each of the format "key = value" ''' equalities_str_list = [] original_text = text #if text[0] == "'": # more loosely formatted text # problem_strs = ['tot DPA'] if text[0] == "#" and 'no. =***' in text and text[-1]=='=': return [] # skip broken lines text = text.replace("'",'').replace(',',' ').replace('#','').replace('=',' = ').replace('***','999999999') text_pieces = text.split() #i_equal_sign = [i for i, x in enumerate(text_pieces) if x == "="] is_i_equal_sign = [x=='=' for x in text_pieces] #i_is_number = [i for i, x in enumerate(text_pieces) if is_number(x)] is_i_number = [is_number(x) for x in text_pieces] #num_equalities = len(i_equal_sign) #remaining_equalities = num_equalities equality_str = '' # the only condition enforced is that the last item in each value be numeric or ) current_equality_contains_equalsign = False for i in reversed(range(len(text_pieces))): # easiest to build from right to left equality_str = text_pieces[i] + ' ' + equality_str if is_i_equal_sign[i]: current_equality_contains_equalsign = True elif current_equality_contains_equalsign: # looking to terminate if next item is numeric if i==0 or (is_i_number[i-1] or text_pieces[i-1][-1]==')'): # either final equality completed or next item belongs to next equality equalities_str_list.insert(0,equality_str.strip()) equality_str = '' current_equality_contains_equalsign = False if '(' in text: # need to break up potential (ia,ib) pairs new_eq_str_list = [] for x in equalities_str_list: if '(' in x: keys, values = x.split('=') keys = keys.strip().replace('(','').replace(')','').split() values = values.strip().replace('(','').replace(')','').split() for i in range(len(keys)): new_eq_str = keys[i].strip() + ' = ' + values[i].strip() new_eq_str_list.append(new_eq_str) else: new_eq_str_list.append(x) equalities_str_list = new_eq_str_list #print(equalities_str_list) return equalities_str_list
def parse_group_string(text)
-
Description
Separate "groups" in a string, wherein a group is a standalone value or a series of values inside parentheses.
Inputs
text
= string to be processed
Outputs
groups
= a list of strings extracted fromtext
Expand source code
def parse_group_string(text): ''' Description: Separate "groups" in a string, wherein a group is a standalone value or a series of values inside parentheses. Inputs: - `text` = string to be processed Outputs: - `groups` = a list of strings extracted from `text` ''' # returns list of items from PHITS-formatted string, e.g. w/ () parts = text.strip().split() #print(parts) groups = [] curly_vals = [] in_brackets_group = False in_curly_brace_group = False num_group_members = 0 for i in parts: if '(' in i and ')' in i: in_brackets_group = False groups.append(i) elif '(' in i: in_brackets_group = True groups.append(i) elif ')' in i: in_brackets_group = False num_group_members = 0 groups[-1] += i elif '{' in i: in_curly_brace_group = True curly_vals = [] elif '}' in i: in_curly_brace_group = False curly_int_strs = [str(j) for j in range(int(curly_vals[0]), int(curly_vals[-1])+1)] curly_vals = [] groups += curly_int_strs else: if in_brackets_group or in_curly_brace_group: if in_brackets_group: if num_group_members>0: groups[-1] += ' ' groups[-1] += i num_group_members += 1 if in_curly_brace_group: if i != '-': curly_vals.append(i) else: groups.append(i) #print(groups) return groups
def initialize_tally_array(tally_metadata, include_abs_err=True)
-
Description
Initializes main tally data array in which tally results will be stored when read
Dependencies
import numpy as np
Inputs
tally_metadata
= Munch object / dictionary containing tally metadatainclude_abs_err
= a Boolean (D=True
) on whether absolute error will be calculated; the final dimension oftdata
is3/2
if this value isTrue/False
Outputs
tdata
= 10-dimensional NumPy array of zeros of correct size for holding tally results
Expand source code
def initialize_tally_array(tally_metadata,include_abs_err=True): ''' Description: Initializes main tally data array in which tally results will be stored when read Dependencies: - `import numpy as np` Inputs: - `tally_metadata` = Munch object / dictionary containing tally metadata - `include_abs_err` = a Boolean (D=`True`) on whether absolute error will be calculated; the final dimension of `tdata` is `3/2` if this value is `True/False` Outputs: - `tdata` = 10-dimensional NumPy array of zeros of correct size for holding tally results ''' ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max = 1, 1, 1, 1, 1, 1, 1, 1, 1 if include_abs_err: ierr_max = 3 else: ierr_max = 2 if tally_metadata['mesh'] == 'reg': ir_max = tally_metadata['nreg'] elif tally_metadata['mesh'] == 'xyz': ir_max = tally_metadata['nx'] iy_max = tally_metadata['ny'] iz_max = tally_metadata['nz'] elif tally_metadata['mesh'] == 'r-z': ir_max = tally_metadata['nr'] iz_max = tally_metadata['nz'] if 'ny' in tally_metadata and tally_metadata['ny'] != None: iy_max = tally_metadata['ny'] if 'nc' in tally_metadata and tally_metadata['nc'] != None: ic_max = tally_metadata['nc'] elif tally_metadata['mesh'] == 'tet': ir_max = tally_metadata['ntet'] elif tally_metadata['mesh'] == 'point' or tally_metadata['mesh'] == 'ring': ir_max = tally_metadata['nreg'] else: raise ValueError('ERROR! Unknown geometry mesh:'+ str(tally_metadata['mesh'])) if tally_metadata['na'] != None: ia_max = tally_metadata['na'] if tally_metadata['nt'] != None: it_max = tally_metadata['nt'] if tally_metadata['nl'] != None: il_max = tally_metadata['nl'] if 'nc' in tally_metadata and tally_metadata['nc'] != None: ic_max = tally_metadata['nc'] #if 'npart' in tally_metadata and tally_metadata.npart != None: ip_max = tally_metadata.np if tally_metadata['ne'] == None: if tally_metadata['tally_type'] == '[T-Deposit2]': if 'ne1' in tally_metadata: ie_max = tally_metadata['ne1'] if 'ne2' in tally_metadata: ic_max = tally_metadata['ne2'] elif 'e1' in tally_metadata['axis'] or 'e2' in tally_metadata['axis']: # This should now be redundant? if tally_metadata['axis'] == 'e12': ie_max = tally_metadata['ne1'] ic_max = tally_metadata['ne2'] elif tally_metadata['axis'] == 'e21': ie_max = tally_metadata['ne1'] ic_max = tally_metadata['ne2'] elif 'e1' in tally_metadata['axis'] or 'eng1' in tally_metadata['axis']: ie_max = tally_metadata['ne1'] if 'ne2' in tally_metadata: ic_max = tally_metadata['ne2'] elif 'e2' in tally_metadata['axis'] or 'eng2' in tally_metadata['axis']: ic_max = tally_metadata['ne2'] if 'ne1' in tally_metadata: ie_max = tally_metadata['ne1'] else: if 'ne1' in tally_metadata: ie_max = tally_metadata['ne1'] if 'ne2' in tally_metadata: ic_max = tally_metadata['ne2'] else: ie_max = tally_metadata['ne'] ip_max = tally_metadata['npart'] if tally_metadata['tally_type'] == '[T-Cross]' and tally_metadata['mesh'] == 'r-z': if 'enclos' in tally_metadata and tally_metadata['enclos'] == 1: pass else: # enclos = 0 case ierr_max = 2*ierr_max if tally_metadata['tally_type'] == '[T-Yield]': if tally_metadata['axis'] == 'charge': ic_max = 130 elif tally_metadata['axis'] == 'mass': ic_max = 320 elif tally_metadata['axis'] == 'chart': if int(tally_metadata['mxnuclei']) == 0: ic_max = 10000 else: ic_max = int(tally_metadata['mxnuclei']) if tally_metadata['tally_type'] == '[T-Interact]' and tally_metadata['axis'] == 'act': ic_max = 100 if 'maxact' in tally_metadata: ic_max = tally_metadata['maxact'] if in_debug_mode: dims_str = 'tally dims: nr={:g}, ny={:g}, nz={:g}, ne={:g}, nt={:g}, na={:g}, nl={:g}, np={:g}, nc={:g}, nerr={:g}' print(dims_str.format(ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max)) tally_data = np.zeros((ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max)) return tally_data
def data_row_to_num_list(line)
-
Description
Extract numeric values from line of text from PHITS tally output content section
Dependencies
is_number()
(function within the "PHITS tools" package)
Inputs
line
= string to be processed
Outputs
values
= a list of ints and/or floats of numeric values inline
Expand source code
def data_row_to_num_list(line): ''' Description: Extract numeric values from line of text from PHITS tally output content section Dependencies: - `is_number` (function within the "PHITS tools" package) Inputs: - `line` = string to be processed Outputs: - `values` = a list of ints and/or floats of numeric values in `line` ''' value_strs = line.strip().split() values = [] for value in value_strs: if is_number(value): if '.' in value: value = float(value) else: value = int(value) values.append(value) return values
def calculate_tally_absolute_errors(tdata)
-
Description
Calculates the absolute uncertainty for every value in the PHITS tally data array
Inputs
tdata
= 10-dimensional NumPy array containing read/extracted tally results
Outputs
tdata
= updatedtdata
array now with absolute uncertainties inierr = 2
index
Expand source code
def calculate_tally_absolute_errors(tdata): ''' Description: Calculates the absolute uncertainty for every value in the PHITS tally data array Inputs: - `tdata` = 10-dimensional NumPy array containing read/extracted tally results Outputs: - `tdata` = updated `tdata` array now with absolute uncertainties in `ierr = 2` index ''' ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata) for ir in range(ir_max): for iy in range(iy_max): for iz in range(iz_max): for ie in range(ie_max): for it in range(it_max): for ia in range(ia_max): for il in range(il_max): for ip in range(ip_max): for ic in range(ic_max): tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2] = \ tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0] * \ tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1] if ierr_max==6: for ir in range(ir_max): for iy in range(iy_max): for iz in range(iz_max): for ie in range(ie_max): for it in range(it_max): for ia in range(ia_max): for il in range(il_max): for ip in range(ip_max): for ic in range(ic_max): tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 5] = \ tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 3] * \ tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 4] return tdata
def build_tally_Pandas_dataframe(tdata, meta)
-
Description
Calculates the absolute uncertainty for every value in the PHITS tally data array
Dependencies
import pandas as pd
Inputs
tdata
= 10-dimensional NumPy array containing read/extracted tally resultsmeta
= Munch object / dictionary containing tally metadata
Outputs
tally_df
= Pandas dataframe containing the entire contents of thetdata
array; note that tally_df.attrs returns values which are the same for all rows
Expand source code
def build_tally_Pandas_dataframe(tdata,meta): ''' Description: Calculates the absolute uncertainty for every value in the PHITS tally data array Dependencies: - `import pandas as pd` Inputs: - `tdata` = 10-dimensional NumPy array containing read/extracted tally results - `meta` = Munch object / dictionary containing tally metadata Outputs: - `tally_df` = Pandas dataframe containing the entire contents of the `tdata` array; note that tally_df.attrs returns values which are the same for all rows ''' import pandas as pd ir_max, iy_max, iz_max, ie_max, it_max, ia_max, il_max, ip_max, ic_max, ierr_max = np.shape(tdata) num_df_rows = ir_max * iy_max * iz_max * ie_max * it_max * ia_max * il_max * ip_max * ic_max # determine what columns to include, based on what info was specified vs left at default values col_names_list = [] in_irregular_TCross_rz_mesh = False in_irregular_TCross_xyz_mesh = False ierr_mod = 0 if meta['tally_type'] == '[T-Cross]' and (meta['mesh'] == 'xyz' or meta['mesh'] == 'r-z'): if 'enclos' in meta and meta['enclos'] == 1: pass else: if meta['mesh'] == 'r-z': in_irregular_TCross_rz_mesh = True min_r_is_zero = False if meta['r-mesh_bin_edges'][0]==0: min_r_is_zero = True ierr_mod = int(ierr_max / 2) else: in_irregular_TCross_xyz_mesh = True # region columns if meta['mesh'] == 'reg': reg_cols = ['ir','reg','reg#'] # use meta.reg_groups and meta.reg_num elif meta['mesh'] == 'xyz': if in_irregular_TCross_xyz_mesh: reg_cols = ['ix', 'iy', 'iz', 'x_mid', 'y_mid', 'z_surf'] else: reg_cols = ['ix','iy','iz','x_mid','y_mid','z_mid'] elif meta['mesh'] == 'r-z': if in_irregular_TCross_rz_mesh: #reg_cols = ['ir', 'ic', 'r_mid', 'z_surf', 'iy', 'iz', 'r_surf', 'z_mid'] reg_cols = ['ir', 'iz', 'r_mid', 'z_surf', 'r_surf', 'z_mid'] else: reg_cols = ['ir','iz','r_mid','z_mid'] elif meta['mesh'] == 'tet': reg_cols = ['ir','tet'] #,'tet#'] elif meta['mesh'] == 'point': reg_cols = ['ir','point#'] elif meta['mesh'] == 'ring': reg_cols = ['ir','ring#'] col_names_list += reg_cols # Determine what other columns will be present ecols, tcols, acols, lcols, pcols, ccols = False, False, False, False, False, False single_specified_bin_axes = [] # log axes which are provided by user but only contain 1 bin single_bin_ranges_or_values = [] if meta['ne'] != None or ie_max>1: # if meta['ne']==1: single_specified_bin_axes.append('e') single_bin_ranges_or_values.append(['Energy',meta['e-mesh_bin_edges']]) elif meta['tally_type']=='[T-Deposit2]': if meta['ne1']==1: single_specified_bin_axes.append('e1') single_bin_ranges_or_values.append(['Energy1', meta['e1-mesh_bin_edges']]) else: ecols = True ecol_names_list = ['ie', 'e1_mid'] col_names_list += ecol_names_list else: ecols = True ecol_names_list = ['ie','e_mid'] col_names_list += ecol_names_list else: single_bin_ranges_or_values.append(['Energy','default/all']) if meta['nt'] != None: if meta['nt']==1: single_specified_bin_axes.append('t') single_bin_ranges_or_values.append(['Time',meta['t-mesh_bin_edges']]) else: tcols = True tcol_names_list = ['it', 't_mid'] col_names_list += tcol_names_list else: single_bin_ranges_or_values.append(['Time','default/all']) if meta['na'] != None: if meta['na']==1: single_specified_bin_axes.append('a') single_bin_ranges_or_values.append(['Angle',meta['a-mesh_bin_edges']]) else: acols = True acol_names_list = ['ia', 'a_mid'] col_names_list += acol_names_list else: single_bin_ranges_or_values.append(['Angle','default/all']) if meta['nl'] != None: if meta['nl']==1: single_specified_bin_axes.append('l') single_bin_ranges_or_values.append(['LET',meta['l-mesh_bin_edges']]) else: lcols = True lcol_names_list = ['il', 'LET_mid'] col_names_list += lcol_names_list else: single_bin_ranges_or_values.append(['LET','default/all']) if meta['nc'] != None or ic_max>1: if meta['nc'] == 1 or ic_max==1: pass else: ccols = True if meta['tally_type'] == '[T-Yield]': if meta['axis'] == 'chart': ccol_names_list = ['ic', 'nuclide', 'ZZZAAAM'] col_names_list += ccol_names_list elif meta['axis'] == 'charge': ccol_names_list = ['ic/Z/charge'] col_names_list += ccol_names_list elif meta['axis'] == 'mass': ccol_names_list = ['ic/A/mass'] col_names_list += ccol_names_list elif meta['tally_type'] == '[T-Interact]': if meta['axis'] == 'act': ccol_names_list = ['ic', '#Interactions'] col_names_list += ccol_names_list elif meta['tally_type'] == '[T-Deposit2]': if meta['ne2'] == 1: single_specified_bin_axes.append('e2') single_bin_ranges_or_values.append(['Energy2', meta['e2-mesh_bin_edges']]) else: ccol_names_list = ['ic', 'e2_mid'] col_names_list += ccol_names_list if meta['npart'] != None: # and meta['part_groups'][0]=='all': if meta['npart']==1: single_specified_bin_axes.append('p') single_bin_ranges_or_values.append(['Particle',meta['part_groups'][0]]) else: pcols = True pcol_names_list = ['ip', 'particle', 'kf-code'] col_names_list += pcol_names_list else: single_bin_ranges_or_values.append(['Particle','default/all']) # HANDLE SPECIAL COLUMNS HERE (ic / ccols) # value columns come last val_names_list = ['value', 'rel.err.'] if ierr_max == 3 or ierr_max == 6: val_names_list += ['abs.err.'] if ierr_max >= 4: val_names_list += ['value2', 'rel.err.2'] if ierr_max == 6: val_names_list += ['abs.err.2'] col_names_list += val_names_list # Initialize dictionary df_dict = {} for col in col_names_list: df_dict[col] = [] # Populate dictionary for ir in range(ir_max): for iy in range(iy_max): for iz in range(iz_max): for ie in range(ie_max): for it in range(it_max): for ia in range(ia_max): for il in range(il_max): for ip in range(ip_max): for ic in range(ic_max): # Region columns if in_irregular_TCross_rz_mesh: if (ir == ir_max - 1 and iz == iz_max - 1): # only index that should be empty continue # ['ir', 'iz', 'r_mid', 'z_surf', 'r_surf', 'z_mid'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(iz) if ir==ir_max-1: df_dict[reg_cols[2]].append(None) else: df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir]) df_dict[reg_cols[3]].append(meta['z-mesh_bin_edges'][iz]) df_dict[reg_cols[4]].append(meta['r-mesh_bin_edges'][ir]) if iz == iz_max - 1: df_dict[reg_cols[5]].append(None) else: df_dict[reg_cols[5]].append(meta['z-mesh_bin_mids'][iz]) # OLD IMPLEMENTATION IS BELOW: ''' # skip unwritten indices # reg_cols = ['ir', 'ic', 'r_mid', 'z_surf', 'iy', 'iz', 'r_surf', 'z_mid'] if (ir==ir_max-1 and ic==ic_max-1): if (iy == iy_max - 1 or iz == iz_max - 1): continue if min_r_is_zero and iy==0: continue # surface vals not written for r=0.0 df_dict[reg_cols[0]].append(None) df_dict[reg_cols[1]].append(None) df_dict[reg_cols[2]].append(None) df_dict[reg_cols[3]].append(None) df_dict[reg_cols[4]].append(iy) df_dict[reg_cols[5]].append(iz) df_dict[reg_cols[6]].append(meta['r-mesh_bin_edges'][iy]) df_dict[reg_cols[7]].append(meta['z-mesh_bin_mids'][iz]) elif (iy==iy_max-1 and iz==iz_max-1): if (ir == ir_max - 1 or ic == ic_max - 1): continue df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(ic) df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir]) df_dict[reg_cols[3]].append(meta['z-mesh_bin_edges'][ic]) df_dict[reg_cols[4]].append(None) df_dict[reg_cols[5]].append(None) df_dict[reg_cols[6]].append(None) df_dict[reg_cols[7]].append(None) else: # all other indices should not have any content written into them continue ''' else: if meta['mesh'] == 'reg': #reg_cols = ['ir','reg', 'reg#'] # use meta.reg_groups and meta.reg_num df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(meta['reg_groups'][ir]) df_dict[reg_cols[2]].append(meta['reg_num'][ir]) elif meta['mesh'] == 'xyz': #reg_cols = ['ix', 'iy', 'iz', 'xmid', 'ymid', 'zmid'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(iy) df_dict[reg_cols[2]].append(iz) df_dict[reg_cols[3]].append(meta['x-mesh_bin_mids'][ir]) df_dict[reg_cols[4]].append(meta['y-mesh_bin_mids'][iy]) if in_irregular_TCross_xyz_mesh: df_dict[reg_cols[5]].append(meta['z-mesh_bin_edges'][iz]) else: df_dict[reg_cols[5]].append(meta['z-mesh_bin_mids'][iz]) elif meta['mesh'] == 'r-z': #reg_cols = ['ir', 'iz', 'rmid', 'zmid'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(iz) df_dict[reg_cols[2]].append(meta['r-mesh_bin_mids'][ir]) df_dict[reg_cols[3]].append(meta['z-mesh_bin_mids'][iz]) elif meta['mesh'] == 'tet': #reg_cols = ['ir','tet'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(meta['tet_num'][ir]) elif meta['mesh'] == 'point': #reg_cols = ['ir','point#'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(str(ir+1)) elif meta['mesh'] == 'ring': #reg_cols = ['ir','ring#'] df_dict[reg_cols[0]].append(ir) df_dict[reg_cols[1]].append(str(ir+1)) #ecols, tcols, acols, lcols, pcols, ccols if pcols: # pcol_names_list = ['ip', 'particle', 'kf-code'] df_dict[pcol_names_list[0]].append(ip) df_dict[pcol_names_list[1]].append(meta['part_groups'][ip]) df_dict[pcol_names_list[2]].append(meta['kf_groups'][ip]) if ecols: # ecol_names_list = ['ie','e_mid'] df_dict[ecol_names_list[0]].append(ie) if meta['tally_type'] == '[T-Deposit2]': df_dict[ecol_names_list[1]].append(meta['e1-mesh_bin_mids'][ie]) else: df_dict[ecol_names_list[1]].append(meta['e-mesh_bin_mids'][ie]) if tcols: # tcol_names_list = ['it','t_mid'] df_dict[tcol_names_list[0]].append(it) df_dict[tcol_names_list[1]].append(meta['t-mesh_bin_mids'][it]) if acols: # acol_names_list = ['ia','a_mid'] df_dict[acol_names_list[0]].append(ia) df_dict[acol_names_list[1]].append(meta['a-mesh_bin_mids'][ia]) if lcols: # lcol_names_list = ['il','LET_mid'] df_dict[lcol_names_list[0]].append(il) df_dict[lcol_names_list[1]].append(meta['l-mesh_bin_mids'][il]) if ccols: if meta['tally_type'] == '[T-Yield]': if meta['axis'] == 'chart': #ccol_names_list = ['ic', 'nuclide', 'ZZZAAAM'] df_dict[ccol_names_list[0]].append(ic) df_dict[ccol_names_list[1]].append(meta['nuclide_isomer_list'][ic]) df_dict[ccol_names_list[2]].append(meta['nuclide_ZZZAAAM_list'][ic]) elif meta['axis'] == 'charge': #ccol_names_list = ['ic/Z/charge'] df_dict[ccol_names_list[0]].append(ic) elif meta['axis'] == 'mass': #ccol_names_list = ['ic/A/mass'] df_dict[ccol_names_list[0]].append(ic) elif meta['tally_type'] == '[T-Interact]': if meta['axis'] == 'act': #ccol_names_list = ['act'] df_dict[ccol_names_list[0]].append(ic) df_dict[ccol_names_list[1]].append(ic+1) elif meta['tally_type'] == '[T-Deposit2]': df_dict[ccol_names_list[0]].append(ic) df_dict[ccol_names_list[1]].append(meta['e2-mesh_bin_mids'][ic]) # Value columns #val_names_list = ['value', 'rel.err.','abs.err.'] df_dict[val_names_list[0]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0]) df_dict[val_names_list[1]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1]) if ierr_max == 3 or ierr_max == 6: df_dict[val_names_list[2]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2]) if in_irregular_TCross_rz_mesh: df_dict[val_names_list[0+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 0+ierr_mod]) df_dict[val_names_list[1+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 1+ierr_mod]) if ierr_max == 6: df_dict[val_names_list[2+ierr_mod]].append(tdata[ir, iy, iz, ie, it, ia, il, ip, ic, 2 + ierr_mod]) # Convert dictionary to Pandas dataframe #for key in df_dict.keys(): # print(key,len(df_dict[key])) #sys.exit() tally_df = pd.DataFrame(df_dict) # store information on settings provided by user that are different from default but same for all rows if len(single_bin_ranges_or_values) > 0: for i in single_bin_ranges_or_values: col, val = i tally_df.attrs[col] = val #with pd.option_context('display.max_rows', None, 'display.max_columns', None): print(tally_df) if in_debug_mode: #print(tally_df.to_string()) print(tally_df.attrs) return tally_df
def extract_tally_outputs_from_phits_input(phits_input, use_path_and_string_mode=False, only_seek_phitsout=False)
-
Description
Extract a list of output files produced from a PHITS input file (or its "phits.out" file, using its input echo). In cases where the PHITS
infl:{*}
function is used to insert text files of PHITS input (namely input for tallies), it is strongly recommended to pass this function the "phits.out" file (file(6)
in the PHITS [Parameters] section) to have access to the complete input echo including all inserted files. Note that this function's output will only include files that actually exist.Inputs
phits_input
= string or Path object denoting the path to the PHITS input file to be parsed (or the "phits.out" file, which should be located in the same directory as the PHITS input producing it)use_path_and_string_mode
= (optional, D=False
) Boolean for special use case byparse_phitsout_file()
. IfTrue
,phits_input
should instead be a dictionary with the following keys:'sim_base_dir_path'
: apathlib.Path()
object pointing to the directory containing the PHITS input file and phits.out file'input_echo'
: a string of the input echo as generated byparse_phitsout_file()
only_seek_phitsout
= (optional, D=False
) Boolean designating if this function should only search for the phits.out file and no other outputs.
Outputs
files_dict
= a dictionary organizing and listing files that were to be produced by the PHITS input and were found to exist, with the following keys:'standard_output'
: a list ofpathlib.Path()
objects pointing to the locations of all of the standard tally output files found.'dump_output'
: a list ofpathlib.Path()
objects pointing to the locations of all of the dump tally output files found.'phitsout'
: a singlepathlib.Path()
object pointing to the location of the "phits.out" file.'active_infl_found'
: a Boolean denoting whether an activeinfl:{*}
insert file function was found in the PHITS input. IfTrue
, it is possible this files dictionary is missing some produced output files from tallies / tally portions included in the input file viainfl:{*}
. This should always beFalse
when provided a "phits.out" file since it includes inserted file contents.
To conveniently view the contents of
files_dict
, one can import the built-in pprint libraryimport pprint
and then usepprint.pp(dict(files_dict))
.Expand source code
def extract_tally_outputs_from_phits_input(phits_input, use_path_and_string_mode=False, only_seek_phitsout=False): ''' Description: Extract a list of output files produced from a PHITS input file (or its "phits.out" file, using its input echo). In cases where the PHITS `infl:{*}` function is used to insert text files of PHITS input (namely input for tallies), it is strongly recommended to pass this function the "phits.out" file (`file(6)` in the PHITS [Parameters] section) to have access to the complete input echo including all inserted files. Note that this function's output will only include files that actually exist. Inputs: - `phits_input` = string or Path object denoting the path to the PHITS input file to be parsed (or the "phits.out" file, which should be located in the same directory as the PHITS input producing it) - `use_path_and_string_mode` = (optional, D=`False`) Boolean for special use case by `parse_phitsout_file()`. If `True`, `phits_input` should instead be a dictionary with the following keys: - `'sim_base_dir_path'` : a [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object pointing to the directory containing the PHITS input file and phits.out file - `'input_echo'` : a string of the input echo as generated by `parse_phitsout_file()` - `only_seek_phitsout` = (optional, D=`False`) Boolean designating if this function should _only_ search for the phits.out file and no other outputs. Outputs: - `files_dict` = a dictionary organizing and listing files that were to be produced by the PHITS input and were found to exist, with the following keys: - `'standard_output'` : a list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects pointing to the locations of all of the standard tally output files found. - `'dump_output'` : a list of [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) objects pointing to the locations of all of the dump tally output files found. - `'phitsout'` : a single [`pathlib.Path()`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) object pointing to the location of the "phits.out" file. - `'active_infl_found'` : a Boolean denoting whether an active `infl:{*}` insert file function was found in the PHITS input. If `True`, it is possible this files dictionary is missing some produced output files from tallies / tally portions included in the input file via `infl:{*}`. This should always be `False` when provided a "phits.out" file since it includes inserted file contents. To conveniently view the contents of `files_dict`, one can import the built-in pprint library `import pprint` and then use `pprint.pp(dict(files_dict))`. ''' ''' Basically, look for "file =", accounting for presence of "dump =" in the tally and whether "off" is present in the tally's first line. Off tallies should be excluded from the list. Once an initial list of files is formed, search for them actually existing (including dump files / MPI splits) and return a list of the existing files. ''' import io def merge_paths_if_not_absolute(known_directory,path_segment): if not Path(path_segment).is_absolute(): path = Path(known_directory,path_segment) else: path = Path(path_segment) if not path.exists(): path = None return path if use_path_and_string_mode: sim_base_dir_path = phits_input['sim_base_dir_path'] file = io.StringIO(phits_input['input_echo']) else: sim_base_dir_path = Path(phits_input).parent PHITS_file_type = determine_PHITS_output_file_type(phits_input) if PHITS_file_type['is_file6_phitsout_file']: # if determined to be a phits.out-type file file = io.StringIO(parse_phitsout_file(phits_input['input_echo'])) else: # otherwise, assume an input file was passed file = open(Path(phits_input), 'r', encoding="utf-8") files_dict = {'standard_output':[], 'dump_output':[], 'phitsout':None} in_valid_tally = False in_parameters_section = False phitsout_file = None tally_out_files = [] tally_has_dump = [] # scan tally input for files produced by PHITS active_infl_found = False if only_seek_phitsout: for line in file: if len(line.strip())==0: continue if line.strip()[0] == '[' and ']' in line: # in a section header block in_parameters_section = False line_section_title_slug = line.lower().replace(' ', '') if '[parameters]' in line_section_title_slug and ']off' not in line_section_title_slug: in_parameters_section = True if in_parameters_section and line.strip()[:7] == 'file(6)': phitsout_file = extract_data_from_header_line(line)[1] else: for line in file: if len(line.strip()) == 0: continue if line.strip()[0] in ['$', '#']: continue # comment line if line.lstrip()[0] == 'c' and 'c ' in line[:min(6,len(line))]: continue # comment line if line.strip()[0] == '[' and ']' in line: # in a section header block line_section_title_slug = line.lower().replace(' ', '') in_valid_tally = False in_parameters_section = False if '[t-' in line_section_title_slug and ']off' not in line_section_title_slug: in_valid_tally = True tally_has_dump.append(False) elif '[parameters]' in line_section_title_slug and ']off' not in line_section_title_slug: in_parameters_section = True elif 'infl:' in line: active_infl_found = True if in_valid_tally: if line.strip()[0] == '#': continue if line.strip()[:4]=='file': key, value = extract_data_from_header_line(line) tally_out_files.append(value) if line.strip()[:4] == 'dump': tally_has_dump[-1] = True elif in_parameters_section: if line.strip()[:7] == 'file(6)': key, value = extract_data_from_header_line(line) phitsout_file = value file.close() files_dict['active_infl_found'] = active_infl_found if phitsout_file is None: phitsout_file = 'phits.out' files_dict['phitsout'] = merge_paths_if_not_absolute(sim_base_dir_path, phitsout_file) if not only_seek_phitsout: for i, file in enumerate(tally_out_files): file_path = merge_paths_if_not_absolute(sim_base_dir_path, file) if file_path is not None: files_dict['standard_output'].append(file_path) if tally_has_dump[i]: dump_files_for_this_tally = list(set(file_path.parent.glob(file_path.stem+"_dmp*"))-set(file_path.parent.glob(file_path.stem+"*.pickle*"))) files_dict['dump_output'] += dump_files_for_this_tally return files_dict