Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mesh integration #10

Merged
merged 15 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ExampleScript.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
'start': 3,
'stop': None,
'name': 'magnet_coils',
'h5m_tag': 'magnets'
'h5m_tag': 'magnets',
'meshing': True
}
# Define export parameters
export = {
Expand Down
189 changes: 189 additions & 0 deletions coils_tet_mesh.py
eitan-weinstein marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import pandas as pd
import numpy as np
import subprocess as sbp
import cubit


def coils_tet_mesh(coils_path, total_coil_count, log_general_path,
exo_general_path, h5m_general_path):
"""Create a tet mesh of the magnet coils using Cubit
and export them as .exo and .h5m files

Arguments:
coils_path (str): absolute path to the coils .step file
containing all of the coil geometries to be meshed.
total_coil_count (int): number of coils contained in the .step file
log_general_path (str): absolute path to a .log file that will be
created by Cubit.
**Note: Do not include any numerical index at the end of
the file name because the function iterates over every coil in the .step
file and will append the index to the .log filename.**
exo_general_path (str): absolute path to a .exo file that will be
created by the function.
**Note: Do not include any numerical index at the
end of the file name because the function iterates over every coil in
the .step file and will append the index to the .exo filename.**
h5m_general_path (str): absolute path to a .h5m file that will be
created by the function.
**Note: Do not include any numerical index at the
end of the file name because the function iterates over every coil in
the .step file and will append the index to the .h5m filename.**
"""

# GEOMETRY AND MESH CREATION

# Start Cubit - this step is key
cubit.init(['cubit', '-nojournal'])

# Import coils
cubit.cmd(f'import step "{coils_path}" heal')
cubit.cmd('volume size auto factor 5')

# For-loop iterates over all 48 coils included in the coils.step file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment still refers to 48 coils

for coil_num in np.arange(1,total_coil_count):

# Create the mesh
cubit.cmd(f'volume {coil_num} scheme tetmesh')
cubit.cmd(f'volume {coil_num} scheme tetmesh')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated line... does it do anything?

cubit.cmd(f'mesh volume {coil_num}')

# Set log file path and initiate logging
log_split = log_general_path.split('.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this once before the loop, perhaps name the components for clarity something like log_prefix and log_ext?

log_path = f"{log_split[0]}_{coil_num}.{log_split[1]})"
cubit.cmd(f'logging on file "{log_path}"')

# Mesh quality metrics
quality_metric_list = ['shape', 'aspect ratio', 'condition no.', 'distortion', 'element volume',
'equiangle skew', 'equivolume skew','inradius', 'jacobian',
'normalized inradius', 'relative size', 'scaled jacobian', 'shape and size']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define this once before the loop


# Run mesh quality metrics for each relevant metric
for metric in quality_metric_list:
command = f'quality volume {coil_num} {metric} global draw mesh list'
mesh_analytics = cubit.cmd(command)


# MESH QUALITY ANALYSIS

# Read the text file
with open(log_path, 'r') as file:
text = file.read()

# Cut out each of the string 'tables'
start_table = 'Function Name'
end_table = 'Finished Command:'
ranges = []
start_index = 0

while True:
# Find the starting index after the previous occurrence
start_index = text.find(start_table, start_index)

# Break the loop if the starting cue is not found
if start_index == -1:
break

# Find the ending index after the starting index
end_index = text.find(end_table, start_index + 1)

# Break the loop if the ending cue is not found
if end_index == -1:
break

# Extract the range between the starting and ending index
range_text = text[start_index : end_index + 1]
ranges.append(range_text)

# Update the starting index for the next iteration
start_index = end_index + 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be a function to reduce cognitive load

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably, the file reading should perhaps be outside the function for separation of concerns


# Extract the tabular data from the .log file and convert
function_names = []
averages = []
std_devs = []
minima = []
maxima = []

for qual_metric in ranges:
range_split = qual_metric[qual_metric.index('-\n'):].split(' ')

row_extraction = []
for i in range_split:
if i != '' and "(" not in i:
row_extraction.append(i)
offset = len(row_extraction) - 4
row_extraction[0] = [''.join(row_extraction[:offset])]

for j in np.arange(1,5):
row_extraction[j] = row_extraction[offset + (j-1) : offset + j]
row_extraction = row_extraction[:5]
row_extraction[0][0] = row_extraction[0][0].split('-\n')[1]

data_lists = [function_names, averages, std_devs, minima, maxima]
indices = np.arange(5)
for index, data_list in zip(indices, data_lists):
if index == 0:
data_list.append(row_extraction[index][0])
else:
data_list.append(float(row_extraction[index][0]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe another function to reduce cognitive load

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also want to review whether there are better ways to do this parsing....


# The above for-loop extracts the function names each as one word
# In order to be able to later match-case with Cubit's libary, the following for-loop separates function names to multiple words if applicable
split_list = []

for name in function_names:
split_words = []
current_word = ""

for char in name:
if char.isupper() and current_word.isupper() and len(current_word) > 1:
split_words.append(current_word)
current_word = char
elif char.isupper() and current_word and not current_word.isupper():
split_words.append(current_word)
current_word = char
else:
current_word += char

split_words.append(current_word)
split_list.append(" ".join(split_words))
function_names = split_list


# Compile the .log data into a Pandas DataFrame
quality_metrics_df = pd.DataFrame({'Function Name': function_names,
'Average': averages,
'Standard Deviation': std_devs,
'Minimum': minima,
'Maximum': maxima})

# Clean the dataframe of implausible entries (e.g. found a standard dev of 0 -- seemed too unlikely)
quality_metrics_df = quality_metrics_df.loc[quality_metrics_df['Standard Deviation'] != 0]

# Sort data to find the highest average mesh quality
quality_metrics_df = quality_metrics_df.sort_values('Average', ascending = False).reset_index()
best_analytic = quality_metrics_df['Function Name'][0]
print(best_analytic)


# MESH EXPORT

# Recreate the geometry and mesh with the best_analytic
cubit.cmd('reset')
cubit.cmd(f'import step "{coils_path}" heal')
cubit.cmd('volume size auto factor 5')
cubit.cmd(f'volume {coil_num} scheme tetmesh')
cubit.cmd(f'volume {coil_num} scheme tetmesh')
cubit.cmd(f'mesh volume {coil_num}')
cubit.cmd(f'mesh volume {coil_num}')
cubit.cmd(f'quality volume {coil_num} {best_analytic} global draw mesh list')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is doing anything meaningful...


# Export the mesh as a .exo file
exo_split = exo_general_path.split('.')
exo_path = f"{exo_split[0]}_{coil_num}.{exo_split[1]}"
cubit.cmd(f'export mesh "{exo_path}"')

# Convert to h5m
h5m_split = h5m_general_path.split('.')
h5m_path = f"{h5m_split[0]}_{coil_num}.{h5m_split[1]}"
h5m_conversion = sbp.run(f'mbconvert {exo_path} {h5m_path}', shell = True)
16 changes: 13 additions & 3 deletions magnet_coils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import cubit
import numpy as np
import sys
import os
import subprocess


def unit_vector(vec):
Expand Down Expand Up @@ -144,7 +146,7 @@ def extract_cs(cross_section, logger):
return shape_str


def create_magnets(filaments, cross_section, logger):
def create_magnets(filaments, cross_section, meshing, logger):
"""Creates magnet coil solids.

Arguments:
Expand All @@ -157,6 +159,7 @@ def create_magnets(filaments, cross_section, logger):
['circle' (str), radius (float, cm)]
For a rectangular cross-section, the list format is
['rectangle' (str), width (float, cm), thickness (float, cm)]
meshing (bool): setting for tetrahedral mesh generation
logger (object): logger object.

Returns:
Expand Down Expand Up @@ -255,11 +258,18 @@ def create_magnets(filaments, cross_section, logger):
f'individual'
)
# Store volume index
vol_ids.append(cubit.get_last_id("volume"))
volume_id = cubit.get_last_id("volume")
vol_ids.append(volume_id)
# Delete extraneous curves and vertices
cubit.cmd(f'delete curve {curve_id}')
cubit.cmd('delete vertex all')

# Optional tetrahedral mesh functionality
if meshing:
# Create scheme and meshes
cubit.cmd(f'volume {volume_id} scheme tetmesh')
cubit.cmd(f'mesh volume {volume_id}')

# Reinitialize path list
path = []

Expand Down Expand Up @@ -365,7 +375,7 @@ def magnet_coils(magnets, logger = None):
)

# Generate magnet coil solids
vol_ids = create_magnets(filaments, magnets['cross_section'], logger)
vol_ids = create_magnets(filaments, magnets['cross_section'], magnets['meshing'], logger)

# Export magnet coils
cubit.cmd(f'export step "{magnets["name"]}.step" overwrite')
Expand Down
13 changes: 13 additions & 0 deletions parametric_stellarator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from scipy.optimize import root_scalar
import os
import sys
import subprocess


def cubit_export(components, export, magnets):
Expand Down Expand Up @@ -175,6 +176,7 @@ def exports(export, components, magnets, logger):
'stop': stopping line index for data in file (int),
'name': name to use for STEP export (str),
'h5m_tag': material tag to use in H5M neutronics model (str)
'meshing': setting for tetrahedral mesh generation (bool)
}
For the list defining the coil cross-section, the cross-section
shape must be either a circle or rectangle. For a circular
Expand Down Expand Up @@ -206,6 +208,16 @@ def exports(export, components, magnets, logger):
for name, comp in components.items():
cq.exporters.export(comp['solid'], name + '.step')

if magnets['meshing']:
# Get current working directory
cwd = os.getcwd()
# Exodus export
exo_path = f'{cwd}/coil_mesh.exo'
cubit.cmd(f'export mesh "{exo_path}"')
# h5m conversion
h5m_path = f'{cwd}/coil_mesh.h5m'
h5m_conversion = subprocess.run(f'mbconvert {exo_path} {h5m_path}', shell = True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do this with pymoab commands by just reading and writing a file in different formats, possibly in a new MOAB instance but maybe just into a new meshset? It will be lighter weight than launching a subprocess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked in the MOAB bitbucket for a pymoab version of mbconvert, but could not find anything. Is there a specific function that you have in mind to replace the subprocess command?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

myFileset = pymoab.EntitySet()
myMoab.read_file('file.exo', fileset = myFileset)
myMoab.write_file('file.h5m', fileset = myFileset)


# Conditinally export H5M file via Cubit
if export['h5m_export'] == 'Cubit':
# Signal H5M export via Cubit
Expand Down Expand Up @@ -520,6 +532,7 @@ def parametric_stellarator(
'stop': stopping line index for data in file (int),
'name': name to use for STEP export (str),
'h5m_tag': material tag to use in H5M neutronics model (str)
'meshing': setting for tetrahedral mesh generation (bool)
}
For the list defining the coil cross-section, the cross-section
shape must be either a circle or rectangle. For a circular
Expand Down