Recursivity and permutations
Motivation and context
- The amazing computing platform FEniCS comes packed with a lot of useful features for the automation of solution of partial differential equations. The fact that people develop such powerful software and give it to the public as open source still honestly astonishes me after years of passion about open-source-software.
- Social considerations aside, I recently stumbled upon an interesting feature that was not covered in the tutorials I came across, because, let’s be honest, open-source is great, but bright people definitely spend more time writing software then explaining others what they created and why.
- The feature I stumbled upon is re-meshing: in FEniCS it’s possible to define a function over the mesh and drive a mesh-refinement algorithm with this input, great! I immediately took occasion to experiment with this feature and try out different approaches for mesh refinement in fracture propagation simulations.
- The fact is that I ended up having a couple of options I wanted to try with quite a slow model for the testing… that’s the perfect excuse to spend a bit of time on automation! And when I say a bit I mean probably I spent as much time automating the procedure as the time I would have needed to perform the task manually. Well… at least the script is ready for the next time!
- The core of the automation script ended up being the task of combining a bunch of parameters in permutations to analyze the effect of the variation of each one separately on the result of the Finite Element computation.
Challenges I faced
- Long story short: I have a list of lists of parameters, the length of the list (and of the sub-lists) is undefined, and I want to get all the possible permutations of those, seems like a doable task but I was forces to leverage a programming functionality which I seldom see around: recursivity.
- Why use recursivity? Let’s imagine the workflow to the solution of the task: for each element in the first list of parameters take each element in the second list of parameters and each element in the third and so on till the last list of parameters. Nested for cycles will do the job right?
- Well only if the number of parameters to vary is predefined, which is not the case here: I’d like to run multiple parametric studies changing only some parameter of interest in each study, so I would need to manually change the for cycles nesting for each parametric study: not a geek option!
- The solution to this issue is in 2 hours of stackoverflow, coding, testing and asking my-self why I’m still stubborn enough not to use ChatGPT. The outcome is a better understanding of how to use recursivity and an “AH-AH” moment of realization when I got it working, here is a sample code for understanding purposes:
parametric_studies = [
[1, 2],
[3, 4, 5],
[6, 7, 8, 9]
]
def permutations(list_of_lists):
if len(list_of_lists) == 1:
return list_of_lists[0]
else:
permutations_list = []
for element_upper_level in list_of_lists[0]:
for element_lower_level in permutations(list_of_lists[1:]):
permutations_list.append( f"{element_upper_level}_{element_lower_level}")
return permutations_list
studies = permutations(parametric_studies)
for study in studies:
print(study)
- Using recursivity, the
permutationsfunction is calling itself every time it needs to open a new list to iterate over its elements thus grabbing one element of each list independently from the number of lists it is fed with, brilliant!
Useful lessons I learned
- I am perfectly aware that the time spent automating this task will be repaid only if I get a change to use it quite often, but I value the learning more then the outcome and now that it’s ready I can use it forever, unless I forget about it… that’s why I put all the code over here!
"""
- This script is meant to execute a batch of simulations in series
with the purpose of parametric study
"""
#%% ---------------------------------------------------------------70
# Import necessary packages
# -----------------------------------------------------------------70
import os
import sys
import time
#%% ---------------------------------------------------------------70
# Control parameters
# -----------------------------------------------------------------70
FEM_file = "Slide_Gate_Plate_adaptative_refinement_without_materials_batch.py"
parameters_labels = [
"damageRefinementThreshold",
"beta",
"lowSpecificationsMaterialsFlag",
"eta",
"plasticityFlag"
]
parametric_studies = [
[ # Study influence of eta parameter
[0.3],
[0.5],
[1],
[1e6, 2e6, 3e6, 4e6],
[0, 1]
]
]
outfile = "./Parametric_Study/Parametric_study.md"
#%% ---------------------------------------------------------------70
# Auxiliary functions
# -----------------------------------------------------------------70
def permutations(list_of_lists):
"""
Compute list_of_lists permutations recursively
"""
if len(list_of_lists) == 1:
return list_of_lists[0]
else:
permutations_list = []
for element_upper_level in list_of_lists[0]:
for element_lower_level in permutations(list_of_lists[1:]):
permutations_list.append( f"{element_upper_level}_{element_lower_level}")
return permutations_list
def fileprint(string):
print(string)
with open(outfile, "a") as file:
file.writelines( "\n" + string)
#%% ---------------------------------------------------------------70
# Run Batch
# -----------------------------------------------------------------70
# measure elapsed time for inverse analysis procedure
tic = time.time()
for study_values in parametric_studies:
# append parameter names to parameter values
named_study_values = []
for ii in range(len(study_values)):
named_list = []
for element in study_values[ii]:
named_list.append(f"{parameters_labels[ii]}_{element}")
named_study_values.append(named_list)
# run the batch
for sim_ID in permutations(named_study_values):
# launch the FEM with the input parameters
try:
os.system(f"python {FEM_file} {sim_ID}")
fileprint(f"Sim: {sim_ID} completed.")
except:
fileprint(f"Got some trouble with {sim_ID}")
fileprint(f"Elapsed CPU time: {int((time.time() - tic)/60):5d} [min]")