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..ded9f09 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,13 @@ 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) + +See examples folders for demonstrations of tools + +## 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_parastell_build_example.py b/examples/plot_parastell_build_example.py new file mode 100644 index 0000000..c3f3dcb --- /dev/null +++ b/examples/plot_parastell_build_example.py @@ -0,0 +1,83 @@ +import numpy as np +import plot_radial_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' + } + } +} + +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 new file mode 100644 index 0000000..679bb44 --- /dev/null +++ b/examples/plot_radial_build_example.py @@ -0,0 +1,58 @@ +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 +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_radial_build.py b/plot_radial_build.py index 3750d5b..7ac38ed 100644 --- a/plot_radial_build.py +++ b/plot_radial_build.py @@ -3,45 +3,20 @@ import matplotlib.colors import yaml import argparse +import numpy as np -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) +class radial_build(object): - Returns: - comp_string (string): formatted string with material definition """ + A representation of a radial build for plotting - comp_string = '' - 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 - - return comp_string[0:-2] - -def plot_radial_build(build, title, colors = None, - max_characters = 35, max_thickness = 1e6, size = (8,4), - unit = 'cm'): - """ - 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), - "composition": { - "material name": fraction (float) - } - } + 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 @@ -53,56 +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 + 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: + 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): + + character_counter += 1 + if character == ' ': + space_index = index + elif character == '\n': + character_counter = 0 + + if character_counter > self.max_characters: + text = text[:space_index]+'\n'+text[space_index+1:] + character_counter = 0 + + return text + + def build_composition_string(self, composition): + """ + Assembles string from composition dict for use in radial build plot - if colors is None: - colors = list(matplotlib.colors.XKCD_COLORS.values())[0:len(build)] + Arguments: + composition (dict): "material name (str)":volume_fraction (float) - #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) + Returns: + comp_string (string): formatted string with material definition + """ - total_thickness = 0 - for (name, layer), color in zip(build.items(), colors): + 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' - comp_string = build_composition_string(layer['composition'], - max_characters) + def write_yml(self): + """ + Writes yml file defining radial build object. File will be called + self.title.yml + """ - thickness_str = layer['thickness'] + 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' - newlines = comp_string.count('\n') + with open(filename, 'w') as file: + yaml.safe_dump(data_dict, file, default_flow_style=False) - min_thickness = (min_lines + newlines) * min_line_height + 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}' - thickness = min(max(layer['thickness'], min_thickness), max_thickness) + 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"]}') - ax.add_patch(Rectangle(ll,thickness, height, facecolor = color, - edgecolor = "black")) + text = f'{name}{thickness_str}\n{comp_string}{description_str}' + text = self.wrap_text(text) + if text[-1] == '\n': + text = text[0:-1] - #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) + newlines = text.count('\n') - #update lower left corner - ll[0] += float(thickness) + min_thickness = (min_lines + newlines) * min_line_height - total_thickness += thickness + thickness = min(max(layer['thickness'], min_thickness), + self.max_thickness) + + ax.add_patch(Rectangle(ll,thickness, height, facecolor = color, + edgecolor = "black")) - ax.set_xlim(-1, total_thickness+1) - ax.set_axis_off() - plt.title(title) - plt.savefig(title.replace(' ',"") + '.png',dpi=200) - plt.close() + #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 """ @@ -140,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