From 6a902d0f6ba23b3890428b3afe8c9471ad6f6aff Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 14:00:16 -0500 Subject: [PATCH 1/7] begin work on parastell plotter --- plot_parastell_build.py | 65 +++++++++++++++++++++++++++++++++++++++++ plot_radial_build.py | 41 ++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 plot_parastell_build.py diff --git a/plot_parastell_build.py b/plot_parastell_build.py new file mode 100644 index 0000000..9e59bdc --- /dev/null +++ b/plot_parastell_build.py @@ -0,0 +1,65 @@ +import numpy as np +import plot_radial_build + + +def plot_parastell_build(build, phi, theta, title, colors=None, max_characters=35, + max_thickness=1e6, size=(8, 4), unit='cm'): + """ + Use the plot_radial_build script to generate a png plot and yml definition + from a parastell build dict. + + Arguments: + build (dict): dictionary of list of toroidal and poloidal angles, as + well as dictionary of component names with corresponding thickness + matrix and optional material tag to use in H5M neutronics model. + The thickness matrix specifies component thickness at specified + (polidal angle, toroidal angle) pairs. This dictionary takes the + form + { + 'phi_list': toroidal angles at which radial build is specified. + 'theta_list': poloidal angles at which radial build is + specified. + 'wall_s': closed flux surface label extrapolation at wall + (float), + 'radial_build': { + 'component': { + 'thickness_matrix': list of list of float (cm), + 'h5m_tag': h5m_tag (str) + } + } + } + phi (float): torodial angle in degrees. Must be in the phi_list of the + build dict. + theta (float): poloidal angle in degrees. Must be in the theta_list of the + build dict. + title (string): title for plot and filename to save to + colors (list of str): list of matplotlib color strings. + If specific colors are desired for each layer they can be added here + max_characters (float): maximum length of a line before wrapping the + text + max_thickness (float): maximum thickness of layer to display, useful + for reducing the total size of the figure. + size (iter of float): figure size, inches. (width, height) + unit (str): Unit of thickness values + """ + # access the thickness values at given theta phi + phi_list = build['phi_list'] + theta_list = build['theta_list'] + radial_build = build['radial_build'] + + phi_index = np.where(phi_list == phi)[0] + theta_index = np.where(theta_list == theta)[0] + + plotter_build = {} + #build the dictionary for plotting + for layer_name, layer in radial_build.items(): + thickness = layer['thickness_matrix'][phi_index, theta_index][0] + material = layer['h5m_tag'] + plotter_build[layer_name] = {"thickness": thickness, + "composition":{material: 1} + } + print(plotter_build) + plot_radial_build.plot_radial_build(plotter_build, title, colors, + max_characters, max_thickness, size, + unit) + diff --git a/plot_radial_build.py b/plot_radial_build.py index 3750d5b..cbe65c2 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -28,6 +28,45 @@ def build_composition_string(composition, max_characters): return comp_string[0:-2] +def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): + """ + Writes yml file defining radial build plot generated. File will be called + title.yml + + Arguments: + build (dict): {"layer name": {"thickness": (float), + "composition": { + "material name": fraction (float) + } + } + } + title (string): title for plot and filename to save to + colors (list of str): list of matplotlib color strings. + If specific colors are desired for each layer they can be added here + max_characters (float): maximum length of a line before wrapping the + text + max_thickness (float): maximum thickness of layer to display, useful + for reducing the total size of the figure. + size (iter of float): figure size, inches. (width, height) + unit (str): Unit of thickness values + """ + + data_dict = {} + data_dict['build'] = build + data_dict['title'] = title + data_dict['colors'] = colors + data_dict['max_characters'] = max_characters + data_dict['max_thickness'] = max_thickness + data_dict['size'] = size + data_dict['unit'] = unit + + filename = title.replace(' ',"") + '.yml' + + with open(filename, 'w') as file: + yaml.dump(data_dict, file, default_flow_style=False) + + + def plot_radial_build(build, title, colors = None, max_characters = 35, max_thickness = 1e6, size = (8,4), unit = 'cm'): @@ -103,6 +142,8 @@ def plot_radial_build(build, title, colors = None, plt.savefig(title.replace(' ',"") + '.png',dpi=200) plt.close() + write_yaml(build, title, colors, max_characters, max_thickness, size, unit) + def parse_args(): """Parser for running as a script """ From 7f4e2791fcead4b3f4f78126f0252efafbeceba0 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 16:06:05 -0500 Subject: [PATCH 2/7] added method to plot from parastell build dict, write yaml file --- plot_parastell_build.py | 2 +- plot_radial_build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plot_parastell_build.py b/plot_parastell_build.py index 9e59bdc..ce05c0b 100644 --- a/plot_parastell_build.py +++ b/plot_parastell_build.py @@ -53,7 +53,7 @@ def plot_parastell_build(build, phi, theta, title, colors=None, max_characters=3 plotter_build = {} #build the dictionary for plotting for layer_name, layer in radial_build.items(): - thickness = layer['thickness_matrix'][phi_index, theta_index][0] + thickness = float(layer['thickness_matrix'][phi_index, theta_index][0]) material = layer['h5m_tag'] plotter_build[layer_name] = {"thickness": thickness, "composition":{material: 1} diff --git a/plot_radial_build.py b/plot_radial_build.py index cbe65c2..69ede38 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -63,7 +63,7 @@ def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): filename = title.replace(' ',"") + '.yml' with open(filename, 'w') as file: - yaml.dump(data_dict, file, default_flow_style=False) + yaml.safe_dump(data_dict, file, default_flow_style=False) From 9f6a77c822f4b0b831e2264a95dcc646a165d0f3 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 17:30:51 -0500 Subject: [PATCH 3/7] update to text wrapping, allow more flexibility in layer text, add parastell tool --- ExampleRadialBuild.yml | 84 ++++++++++++++++++++++++++ README.md | 8 +++ example.yaml | 44 -------------- examples/plot_radial_build_example.py | 51 ++++++++++++++++ plot_parastell_build.py | 1 - plot_radial_build.py | 87 +++++++++++++++++++-------- 6 files changed, 205 insertions(+), 70 deletions(-) create mode 100644 ExampleRadialBuild.yml delete mode 100644 example.yaml create mode 100644 examples/plot_radial_build_example.py diff --git a/ExampleRadialBuild.yml b/ExampleRadialBuild.yml new file mode 100644 index 0000000..bef8567 --- /dev/null +++ b/ExampleRadialBuild.yml @@ -0,0 +1,84 @@ +build: + BW: + composition: + HeT410P80: 0.2 + MF82H: 0.8 + thickness: 2 + Coil Pack: + composition: + Cu: 0.1307 + HTS TAPE: 0.0622 + HeT410P80: 0.0288 + SS316L: 0.7435 + Solder: 0.0438 + description: '[combines winding pack and coil case]' + thickness: 52.5 + FW: + composition: + HeT410P80: 0.66 + MF82H: 0.34 + thickness: 3.8 + FW_armor: + thickness: 0.2 + HTS: + description: Composition and thickness vary + thickness: 8 + LTS: + description: Composition and thickness vary + thickness: 8 + Thermal Insulator (Gap): + composition: + Void: 1.0 + thickness: 10 + breeder: + description: Composition and thickness vary + thickness: 8 + gap_1: + composition: + Void: 1.0 + thickness: 8 + gap_2: + composition: + Void: 1.0 + thickness: 2 + manifolds: + description: composition varies + thickness: 10 + sol: + thickness: 8 + vv_back_plate: + composition: + SS316L: 1.0 + thickness: 2 + vv_fill: + composition: + HeT410P80: 0.4 + SS316L: 0.6 + thickness: 6 + vv_front_plate: + composition: + SS316L: 1.0 + thickness: 2 +colors: +- '#acc2d9' +- '#56ae57' +- '#b2996e' +- '#a8ff04' +- '#69d84f' +- '#894585' +- '#70b23f' +- '#d4ffff' +- '#65ab7c' +- '#952e8f' +- '#fcfc81' +- '#a5a391' +- '#388004' +- '#4c9085' +- '#5e9b8a' +max_characters: 39 +max_thickness: 1000000.0 +size: +- 10 +- 4 +title: Example Radial Build +unit: cm diff --git a/README.md b/README.md index 2a96074..28f97e5 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,11 @@ Tools for building, manipulating and representing radial build for fusion power systems [Early vision](https://docs.google.com/presentation/d/1yDzG23BL8KTqxQCjatCVnmPRx0kgijyP6wGbssfKwiQ/edit#slide=id.p) + +## plot_radial_build.py +Main plotting functionality, can be called from command line via: + +`python plot_radial_build.py ExampleRadialBuild.yml` + +`plot_radial_build.py` will write both a png of a plot and a yml file which +can be used to recreate it. diff --git a/example.yaml b/example.yaml deleted file mode 100644 index 9241f3f..0000000 --- a/example.yaml +++ /dev/null @@ -1,44 +0,0 @@ -title: Example Radial Build -build: - SOL: - thickness: 4 - composition: - Vacuum: 1 - FW: - thickness: 4 - composition: - MF82H: 0.34 - He: 0.66 - Breeder: - thickness: 50 - composition: - FNSFDCLL: 1.0 - BW: - thickness: 4 - composition: - MF82H: 0.8 - He: 0.2 - HTS: - thickness: 20 - composition: - WC: 0.69 - He: 0.26 - MF82H: 0.05 - VV: - thickness: 10 - composition: - SS316L: 1.0 - LTS: - thickness: 20 - composition: - Water: 0.3 - WC: 0.33 - SS316L: 0.3 - Winding Pack: - thickness: 63 - composition: - Cu: 0.43 - JK2LB: 0.29 - He: 0.14 - Nb3Sn: 0.06 - Insulator: 0.08 diff --git a/examples/plot_radial_build_example.py b/examples/plot_radial_build_example.py new file mode 100644 index 0000000..7e9f13e --- /dev/null +++ b/examples/plot_radial_build_example.py @@ -0,0 +1,51 @@ +from plot_radial_build import plot_radial_build + +build_dict = { + # here is a layer with no optional data incluede + "sol": {}, + + # here is a layer where only thickness is included + "FW_armor": {"thickness": 0.2}, + + "FW": {'thickness': 3.8, 'composition': {"MF82H": 0.34, "HeT410P80": 0.66}}, + + # here is a layer where only a description is included + "breeder": {'description': "Composition and thickness vary"}, + + 'BW': {'thickness': 2, 'composition': {"MF82H": 0.80, "HeT410P80": 0.20}}, + + # here is a layer where thickness and description are included + 'manifolds': {'thickness': 10, "description": "composition varies"}, + + 'HTS': {'description': "Composition and thickness vary"}, + + # here is a layer where only composition is inclued + 'gap_1': {'composition': {"Void": 1.0}}, + + 'vv_front_plate': {'thickness': 2, 'composition': {"SS316L": 1.0}}, + + 'vv_fill': {'thickness': 6, 'composition': {"SS316L": 0.6, + "HeT410P80": 0.4}}, + + 'vv_back_plate': {'thickness': 2, 'composition': {"SS316L": 1.0}}, + + 'gap_2': {'thickness': 2, 'composition': {"Void": 1.0}}, + + 'LTS': {'description': "Composition and thickness vary"}, + + 'Thermal Insulator (Gap)': {'thickness': 10, + 'composition': {"Void": 1.0}}, + + # here is a layer with all optional data included + 'Coil Pack': {'thickness': 52.5, 'composition': {"SS316L": 0.7435, + "HTS TAPE": 0.0622, + "Cu": 0.1307, + "Solder": 0.0438, + "HeT410P80": 0.0288}, + 'description': '[combines winding pack and coil case]'} +} + +# note that since this is quite a detailed radial build, the default figure +# size was insufficient +plot_radial_build(build_dict, 'Example Radial Build', + max_characters=39, size=(10, 4)) diff --git a/plot_parastell_build.py b/plot_parastell_build.py index ce05c0b..d734b1f 100644 --- a/plot_parastell_build.py +++ b/plot_parastell_build.py @@ -58,7 +58,6 @@ def plot_parastell_build(build, phi, theta, title, colors=None, max_characters=3 plotter_build[layer_name] = {"thickness": thickness, "composition":{material: 1} } - print(plotter_build) plot_radial_build.plot_radial_build(plotter_build, title, colors, max_characters, max_thickness, size, unit) diff --git a/plot_radial_build.py b/plot_radial_build.py index 69ede38..c3c9d31 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -4,6 +4,34 @@ import yaml import argparse +def wrap_text(text, max_characters): + """ + loop thru text, if line length is too long, go back and replace the + previous space with a linebreak + + Arguments: + text (str): text to wrap + max_characters (int): line length limit + + returns: + text (str): wrapped text + """ + + character_counter = 0 + for index, character in enumerate(text): + + character_counter += 1 + if character == ' ': + space_index = index + elif character == '\n': + character_counter = 0 + + if character_counter > max_characters: + text = text[:space_index]+'\n'+text[space_index+1:] + character_counter = 0 + + return text + def build_composition_string(composition, max_characters): """ Assembles string from composition dict for use in radial build plot @@ -19,14 +47,11 @@ def build_composition_string(composition, max_characters): for material, fraction in composition.items(): mat_string = f'{material}: {round(fraction*100,3)}%, ' - line_len =len(comp_string+mat_string)-(comp_string+mat_string).rfind('\n') - - if line_len > max_characters: - comp_string += '\n' + mat_string - else: - comp_string += mat_string + comp_string += mat_string + + comp_string = wrap_text(comp_string, max_characters) - return comp_string[0:-2] + return comp_string[0:-2]+'\n' def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): """ @@ -34,12 +59,7 @@ def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): title.yml Arguments: - build (dict): {"layer name": {"thickness": (float), - "composition": { - "material name": fraction (float) - } - } - } + build (dict): build dict used in plot_radial_build title (string): title for plot and filename to save to colors (list of str): list of matplotlib color strings. If specific colors are desired for each layer they can be added here @@ -75,10 +95,11 @@ def plot_radial_build(build, title, colors = None, maximum pixel width to preserve readability Arguments: - build (dict): {"layer name": {"thickness": (float), - "composition": { - "material name": fraction (float) - } + build (dict): {"layer name": {"thickness": (float) optional, + "composition": (dict) optional { + "material name": fraction (float) + } + "description": (str) optional } } title (string): title for plot and filename to save to @@ -109,13 +130,30 @@ def plot_radial_build(build, title, colors = None, total_thickness = 0 for (name, layer), color in zip(build.items(), colors): - - comp_string = build_composition_string(layer['composition'], + + if 'thickness' not in layer: + layer['thickness'] = min_line_height + thickness_str = '' + else: + thickness_str = f': {layer['thickness']} {unit}' + + if 'composition' not in layer: + comp_string = '' + else: + comp_string = build_composition_string(layer['composition'], max_characters) + + if 'description' not in layer: + description_str = '' + else: + description_str = wrap_text(f'{layer['description']}', max_characters) - thickness_str = layer['thickness'] + text = f'{name}{thickness_str}\n{comp_string}{description_str}' + text = wrap_text(text, max_characters) + if text[-1] == '\n': + text = text[0:-1] - newlines = comp_string.count('\n') + newlines = text.count('\n') min_thickness = (min_lines + newlines) * min_line_height @@ -127,11 +165,10 @@ def plot_radial_build(build, title, colors = None, #put the text in centerx = ll[0] + thickness/2 + 1 centery = height/2 - plt.text(centerx, centery, - f'{name}: {thickness_str} {unit}\n{comp_string}', - rotation = "vertical", ha = "center", va = "center", wrap=True) + + plt.text(centerx, centery, text, rotation = "vertical", ha = "center", + va = "center") - #update lower left corner ll[0] += float(thickness) total_thickness += thickness From b8abd6af6940949191ab3d0c9e910c8366ebd3e4 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 17:38:08 -0500 Subject: [PATCH 4/7] added example for plot_parastell_build --- examples/plot_parastell_build_example.py | 75 ++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 examples/plot_parastell_build_example.py diff --git a/examples/plot_parastell_build_example.py b/examples/plot_parastell_build_example.py new file mode 100644 index 0000000..2952167 --- /dev/null +++ b/examples/plot_parastell_build_example.py @@ -0,0 +1,75 @@ +import numpy as np +from plot_parastell_build import plot_parastell_build + +num_phi = 80 +num_theta = 90 + +phi_list = np.linspace(0,90,num_phi) +theta_list = np.linspace(0,360,num_theta) +ones = np.ones((len(phi_list),len(theta_list))) + +build = { + 'phi_list': phi_list, + 'theta_list': theta_list, + 'wall_s': 1.2, + 'radial_build': { + 'fw': { + 'thickness_matrix': ones*4, #cell 3 + 'h5m_tag': 'FNSFFW' + }, + 'breeder': { + 'thickness_matrix': ones*50, #cell 4 + 'h5m_tag':'FNSFDCLL' + }, + 'BW': { + 'thickness_matrix': ones*2, #cell 5 + 'h5m_tag': 'FNSFBW' + }, + 'manifolds': { + 'thickness_matrix': ones*6, #cell 6 + 'h5m_tag': 'FNSFHeManifolds' + }, + 'HTS': { + 'thickness_matrix': ones*20, #cell 7 + 'h5m_tag': 'FNSFIBSR' + }, + 'Gap_1': { + 'thickness_matrix': ones*1, #cell 8 + 'h5m_tag': 'Vacuum' + }, + 'vvfrontplate': { + 'thickness_matrix': ones*2, #cell 9 + 'h5m_tag': 'SS316L' + }, + 'VVFill': { + 'thickness_matrix': ones*6, #cell 10 + 'h5m_tag': 'VVFill' + }, + 'VVBackPlate': { + 'thickness_matrix': ones*2, #cell 11 + 'h5m_tag': 'SS316L' + }, + 'Gap_2':{ + 'thickness_matrix': ones*2, #cell 12 + 'h5m_tag': 'AirSTP' + }, + 'LTS':{ + 'thickness_matrix': ones*23, #cell 13 + 'h5m_tag': 'LTS' + }, + 'Thermal_Insulator':{ + 'thickness_matrix': ones*10, #cell 14 + 'h5m_tag': 'AirSTP' + }, + 'coilfrontplate':{ + 'thickness_matrix': ones*2, #cell 15 + 'h5m_tag': 'coils' + }, + 'coils':{ + 'thickness_matrix': ones*50.5, #cell 16 + 'h5m_tag':'coils' + } + } +} + +plot_parastell_build(build, phi_list[-1], theta_list[-1], 'Example Parastell Build') \ No newline at end of file From ff728b83ea28b1d1258baafdb0014b9bc54221a3 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 17:42:47 -0500 Subject: [PATCH 5/7] make writing yml optional behavior --- README.md | 2 ++ plot_radial_build.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28f97e5..ded9f09 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Tools for building, manipulating and representing radial build for fusion power [Early vision](https://docs.google.com/presentation/d/1yDzG23BL8KTqxQCjatCVnmPRx0kgijyP6wGbssfKwiQ/edit#slide=id.p) +See examples folders for demonstrations of tools + ## plot_radial_build.py Main plotting functionality, can be called from command line via: diff --git a/plot_radial_build.py b/plot_radial_build.py index c3c9d31..9ad61fc 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -89,7 +89,7 @@ def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): def plot_radial_build(build, title, colors = None, max_characters = 35, max_thickness = 1e6, size = (8,4), - unit = 'cm'): + unit = 'cm', write_yml=True): """ Creates a radial build plot, with layers scaled between a minimum and maximum pixel width to preserve readability @@ -179,7 +179,9 @@ def plot_radial_build(build, title, colors = None, plt.savefig(title.replace(' ',"") + '.png',dpi=200) plt.close() - write_yaml(build, title, colors, max_characters, max_thickness, size, unit) + if write_yaml: + write_yaml(build, title, colors, max_characters, max_thickness, size, + unit) def parse_args(): """Parser for running as a script From 3c0c4e586849ea356fc9f8e186100a128fc03c85 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 26 Apr 2024 17:44:46 -0500 Subject: [PATCH 6/7] pep8 --- plot_parastell_build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plot_parastell_build.py b/plot_parastell_build.py index d734b1f..dcea068 100644 --- a/plot_parastell_build.py +++ b/plot_parastell_build.py @@ -2,8 +2,9 @@ import plot_radial_build -def plot_parastell_build(build, phi, theta, title, colors=None, max_characters=35, - max_thickness=1e6, size=(8, 4), unit='cm'): +def plot_parastell_build(build, phi, theta, title, colors=None, + max_characters=35, max_thickness=1e6, size=(8, 4), + unit='cm', write_yml = True): """ Use the plot_radial_build script to generate a png plot and yml definition from a parastell build dict. From 2dd75a553243d8e5bc70467590ac1ecbf7085312 Mon Sep 17 00:00:00 2001 From: Edgar Date: Fri, 10 May 2024 15:23:14 -0500 Subject: [PATCH 7/7] combined functions in plot_radial_build.py into the class radial_build, with parastell method --- examples/plot_parastell_build_example.py | 12 +- examples/plot_radial_build_example.py | 11 +- plot_parastell_build.py | 65 ----- plot_radial_build.py | 344 ++++++++++++----------- 4 files changed, 206 insertions(+), 226 deletions(-) delete mode 100644 plot_parastell_build.py diff --git a/examples/plot_parastell_build_example.py b/examples/plot_parastell_build_example.py index 2952167..c3f3dcb 100644 --- a/examples/plot_parastell_build_example.py +++ b/examples/plot_parastell_build_example.py @@ -1,5 +1,5 @@ import numpy as np -from plot_parastell_build import plot_parastell_build +import plot_radial_build num_phi = 80 num_theta = 90 @@ -72,4 +72,12 @@ } } -plot_parastell_build(build, phi_list[-1], theta_list[-1], 'Example Parastell Build') \ No newline at end of file +radial_build = plot_radial_build.radial_build.from_parastell_build( + build, "Example Parastell Build", phi_list[-1], theta_list[-1]) + + +# create the radial build plot png +radial_build.plot_radial_build() + +# save the plot configuration as a yml file +radial_build.write_yml() \ No newline at end of file diff --git a/examples/plot_radial_build_example.py b/examples/plot_radial_build_example.py index 7e9f13e..679bb44 100644 --- a/examples/plot_radial_build_example.py +++ b/examples/plot_radial_build_example.py @@ -1,4 +1,4 @@ -from plot_radial_build import plot_radial_build +import plot_radial_build build_dict = { # here is a layer with no optional data incluede @@ -47,5 +47,12 @@ # note that since this is quite a detailed radial build, the default figure # size was insufficient -plot_radial_build(build_dict, 'Example Radial Build', +radial_build = plot_radial_build.radial_build(build_dict, 'Example Radial Build', max_characters=39, size=(10, 4)) + +# create the radial build plot png +radial_build.plot_radial_build() + +# save the plot configuration as a yml file +radial_build.write_yml() + diff --git a/plot_parastell_build.py b/plot_parastell_build.py deleted file mode 100644 index dcea068..0000000 --- a/plot_parastell_build.py +++ /dev/null @@ -1,65 +0,0 @@ -import numpy as np -import plot_radial_build - - -def plot_parastell_build(build, phi, theta, title, colors=None, - max_characters=35, max_thickness=1e6, size=(8, 4), - unit='cm', write_yml = True): - """ - Use the plot_radial_build script to generate a png plot and yml definition - from a parastell build dict. - - Arguments: - build (dict): dictionary of list of toroidal and poloidal angles, as - well as dictionary of component names with corresponding thickness - matrix and optional material tag to use in H5M neutronics model. - The thickness matrix specifies component thickness at specified - (polidal angle, toroidal angle) pairs. This dictionary takes the - form - { - 'phi_list': toroidal angles at which radial build is specified. - 'theta_list': poloidal angles at which radial build is - specified. - 'wall_s': closed flux surface label extrapolation at wall - (float), - 'radial_build': { - 'component': { - 'thickness_matrix': list of list of float (cm), - 'h5m_tag': h5m_tag (str) - } - } - } - phi (float): torodial angle in degrees. Must be in the phi_list of the - build dict. - theta (float): poloidal angle in degrees. Must be in the theta_list of the - build dict. - title (string): title for plot and filename to save to - colors (list of str): list of matplotlib color strings. - If specific colors are desired for each layer they can be added here - max_characters (float): maximum length of a line before wrapping the - text - max_thickness (float): maximum thickness of layer to display, useful - for reducing the total size of the figure. - size (iter of float): figure size, inches. (width, height) - unit (str): Unit of thickness values - """ - # access the thickness values at given theta phi - phi_list = build['phi_list'] - theta_list = build['theta_list'] - radial_build = build['radial_build'] - - phi_index = np.where(phi_list == phi)[0] - theta_index = np.where(theta_list == theta)[0] - - plotter_build = {} - #build the dictionary for plotting - for layer_name, layer in radial_build.items(): - thickness = float(layer['thickness_matrix'][phi_index, theta_index][0]) - material = layer['h5m_tag'] - plotter_build[layer_name] = {"thickness": thickness, - "composition":{material: 1} - } - plot_radial_build.plot_radial_build(plotter_build, title, colors, - max_characters, max_thickness, size, - unit) - diff --git a/plot_radial_build.py b/plot_radial_build.py index 9ad61fc..7ac38ed 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -3,105 +3,20 @@ import matplotlib.colors import yaml import argparse +import numpy as np -def wrap_text(text, max_characters): - """ - loop thru text, if line length is too long, go back and replace the - previous space with a linebreak - - Arguments: - text (str): text to wrap - max_characters (int): line length limit - - returns: - text (str): wrapped text - """ - - character_counter = 0 - for index, character in enumerate(text): - - character_counter += 1 - if character == ' ': - space_index = index - elif character == '\n': - character_counter = 0 - - if character_counter > max_characters: - text = text[:space_index]+'\n'+text[space_index+1:] - character_counter = 0 - - return text - -def build_composition_string(composition, max_characters): - """ - Assembles string from composition dict for use in radial build plot - - Arguments: - composition (dict): "material name (str)":volume_fraction (float) - - Returns: - comp_string (string): formatted string with material definition - """ - - comp_string = '' - for material, fraction in composition.items(): - - mat_string = f'{material}: {round(fraction*100,3)}%, ' - comp_string += mat_string - - comp_string = wrap_text(comp_string, max_characters) +class radial_build(object): - return comp_string[0:-2]+'\n' - -def write_yaml(build, title, colors, max_characters, max_thickness, size, unit): - """ - Writes yml file defining radial build plot generated. File will be called - title.yml - - Arguments: - build (dict): build dict used in plot_radial_build - title (string): title for plot and filename to save to - colors (list of str): list of matplotlib color strings. - If specific colors are desired for each layer they can be added here - max_characters (float): maximum length of a line before wrapping the - text - max_thickness (float): maximum thickness of layer to display, useful - for reducing the total size of the figure. - size (iter of float): figure size, inches. (width, height) - unit (str): Unit of thickness values """ - - data_dict = {} - data_dict['build'] = build - data_dict['title'] = title - data_dict['colors'] = colors - data_dict['max_characters'] = max_characters - data_dict['max_thickness'] = max_thickness - data_dict['size'] = size - data_dict['unit'] = unit - - filename = title.replace(' ',"") + '.yml' + A representation of a radial build for plotting - with open(filename, 'w') as file: - yaml.safe_dump(data_dict, file, default_flow_style=False) - - - -def plot_radial_build(build, title, colors = None, - max_characters = 35, max_thickness = 1e6, size = (8,4), - unit = 'cm', write_yml=True): - """ - Creates a radial build plot, with layers scaled between a minimum and - maximum pixel width to preserve readability - - Arguments: - build (dict): {"layer name": {"thickness": (float) optional, - "composition": (dict) optional { - "material name": fraction (float) - } - "description": (str) optional - } + Parameters + `build (dict): {"layer name": {"thickness": (float), + "composition": { + "material name": fraction (float) } + } + ` } title (string): title for plot and filename to save to colors (list of str): list of matplotlib color strings. If specific colors are desired for each layer they can be added here @@ -113,76 +28,189 @@ def plot_radial_build(build, title, colors = None, unit (str): Unit of thickness values """ - char_to_height = 1.15 - min_line_height = 8 - min_lines = 2 - height = char_to_height*max_characters - - if colors is None: - colors = list(matplotlib.colors.XKCD_COLORS.values())[0:len(build)] - - #initialize list for lower left corner of each layer rectangle - ll = [0,0] - plt.figure(1, figsize=size) - plt.tight_layout() - ax = plt.gca() - ax.set_ylim(0,height+1) - - total_thickness = 0 - for (name, layer), color in zip(build.items(), colors): - - if 'thickness' not in layer: - layer['thickness'] = min_line_height - thickness_str = '' + def __init__( + self, + build, + title, + colors=None, + max_characters = 35, + max_thickness = 1e6, + size = (8,4), + unit = 'cm' + ): + self.build = build + self.title = title + if colors == None: + self.colors = list(matplotlib.colors.XKCD_COLORS.values())[0:len(build)] else: - thickness_str = f': {layer['thickness']} {unit}' - - if 'composition' not in layer: - comp_string = '' - else: - comp_string = build_composition_string(layer['composition'], - max_characters) + self.colors = colors + self.max_characters = max_characters + self.max_thickness = max_thickness + self.size = size + self.unit = unit + + def wrap_text(self, text): + """ + loop thru text, if line length is too long, go back and replace the + previous space with a linebreak + + Arguments: + text (str): text to wrap + + returns: + text (str): wrapped text + """ + + character_counter = 0 + for index, character in enumerate(text): - if 'description' not in layer: - description_str = '' - else: - description_str = wrap_text(f'{layer['description']}', max_characters) - - text = f'{name}{thickness_str}\n{comp_string}{description_str}' - text = wrap_text(text, max_characters) - if text[-1] == '\n': - text = text[0:-1] - - newlines = text.count('\n') + character_counter += 1 + if character == ' ': + space_index = index + elif character == '\n': + character_counter = 0 - min_thickness = (min_lines + newlines) * min_line_height - - thickness = min(max(layer['thickness'], min_thickness), max_thickness) - - ax.add_patch(Rectangle(ll,thickness, height, facecolor = color, - edgecolor = "black")) + if character_counter > self.max_characters: + text = text[:space_index]+'\n'+text[space_index+1:] + character_counter = 0 - #put the text in - centerx = ll[0] + thickness/2 + 1 - centery = height/2 - - plt.text(centerx, centery, text, rotation = "vertical", ha = "center", - va = "center") + return text - ll[0] += float(thickness) + def build_composition_string(self, composition): + """ + Assembles string from composition dict for use in radial build plot - total_thickness += thickness + Arguments: + composition (dict): "material name (str)":volume_fraction (float) - ax.set_xlim(-1, total_thickness+1) - ax.set_axis_off() - plt.title(title) - plt.savefig(title.replace(' ',"") + '.png',dpi=200) - plt.close() + Returns: + comp_string (string): formatted string with material definition + """ - if write_yaml: - write_yaml(build, title, colors, max_characters, max_thickness, size, - unit) + comp_string = '' + for material, fraction in composition.items(): + + mat_string = f'{material}: {round(fraction*100,3)}%, ' + comp_string += mat_string + + comp_string = self.wrap_text(comp_string) + return comp_string[0:-2]+'\n' + + def write_yml(self): + """ + Writes yml file defining radial build object. File will be called + self.title.yml + """ + + data_dict = {} + data_dict['build'] = self.build + data_dict['title'] = self.title + data_dict['colors'] = self.colors + data_dict['max_characters'] = self.max_characters + data_dict['max_thickness'] = self.max_thickness + data_dict['size'] = self.size + data_dict['unit'] = self.unit + + filename = self.title.replace(' ',"") + '.yml' + + with open(filename, 'w') as file: + yaml.safe_dump(data_dict, file, default_flow_style=False) + + def plot_radial_build(self): + """ + Creates a radial build plot, with layers scaled between a minimum and + maximum pixel width to preserve readability + """ + + char_to_height = 1.15 + min_line_height = 8 + min_lines = 2 + height = char_to_height*self.max_characters + + #initialize list for lower left corner of each layer rectangle + ll = [0,0] + plt.figure(1, figsize=self.size) + plt.tight_layout() + ax = plt.gca() + ax.set_ylim(0,height+1) + + total_thickness = 0 + for (name, layer), color in zip(self.build.items(), self.colors): + + if 'thickness' not in layer: + layer['thickness'] = min_line_height + thickness_str = '' + else: + thickness_str = f': {layer["thickness"]} {self.unit}' + + if 'composition' not in layer: + comp_string = '' + else: + comp_string = self.build_composition_string(layer['composition']) + + if 'description' not in layer: + description_str = '' + else: + description_str = self.wrap_text(f'{layer["description"]}') + + text = f'{name}{thickness_str}\n{comp_string}{description_str}' + text = self.wrap_text(text) + if text[-1] == '\n': + text = text[0:-1] + + newlines = text.count('\n') + + min_thickness = (min_lines + newlines) * min_line_height + + thickness = min(max(layer['thickness'], min_thickness), + self.max_thickness) + + ax.add_patch(Rectangle(ll,thickness, height, facecolor = color, + edgecolor = "black")) + + #put the text in + centerx = ll[0] + thickness/2 + 1 + centery = height/2 + + plt.text(centerx, centery, text, rotation = "vertical", + ha = "center", va = "center") + + ll[0] += float(thickness) + + total_thickness += thickness + + ax.set_xlim(-1, total_thickness+1) + ax.set_axis_off() + plt.title(self.title) + plt.savefig(self.title.replace(' ',"") + '.png',dpi=200) + plt.close() + + @classmethod + def from_parastell_build(cls, parastell_build_dict, title, phi, theta, + colors = None, max_characters=35, max_thickness=1e6, + size=(8,4), unit='cm'): + + # access the thickness values at given theta phi + phi_list = parastell_build_dict['phi_list'] + theta_list = parastell_build_dict['theta_list'] + radial_build= parastell_build_dict['radial_build'] + + phi_index = np.where(phi_list == phi)[0] + theta_index = np.where(theta_list == theta)[0] + plotter_build = {} + #build the dictionary for plotting + for layer_name, layer in radial_build.items(): + thickness = float(layer['thickness_matrix'][phi_index, theta_index][0]) + material = layer['h5m_tag'] + plotter_build[layer_name] = {"thickness": thickness, + "description":material} + + radial_build = cls(plotter_build, title, colors, max_characters, + max_thickness, size, unit) + + return radial_build + def parse_args(): """Parser for running as a script """ @@ -220,10 +248,12 @@ def main(): data_dict = data_default.copy() data_dict.update(data) - plot_radial_build(data_dict['build'], data_dict['title'], + rb = radial_build(data_dict['build'], data_dict['title'], data_dict['colors'], data_dict['max_characters'], data_dict['max_thickness'], data_dict['size'], data_dict['unit']) + + rb.plot_radial_build() if __name__ == "__main__": - main() + main() \ No newline at end of file