Skip to content

API: Engines

matmdl.engines

Choose the computational engine for running, checking, and extracting finite element job information. Currently only Abaqus exists.

abaqus

This module contains helper functions for dealing with Abaqus but has no Abaqus-specific imports.

extract(outname)

Call :py:mod:matmdl.engines.abaqus_extract from new shell to extract force-displacement data.

Source code in matmdl/engines/abaqus.py
def extract(outname: str):
	"""
	Call :py:mod:`matmdl.engines.abaqus_extract` from new shell to extract force-displacement data.
	"""
	src_dir = os.path.dirname(os.path.abspath(__file__))
	extractions_script_path = os.path.join(src_dir, "abaqus_extract.py")
	run_string = f"abaqus python {extractions_script_path}"
	subprocess.run(run_string, shell=True)
	os.rename("temp_time_disp_force.csv", f"temp_time_disp_force_{outname}.csv")

has_completed()

Return True if Abaqus has finished sucessfully.

Source code in matmdl/engines/abaqus.py
def has_completed():
	"""
	Return ``True`` if Abaqus has finished sucessfully.
	"""
	stafile = uset.jobname + ".sta"
	if os.path.isfile(stafile):
		last_line = str(subprocess.check_output(["tail", "-1", stafile]))
	else:
		last_line = ""
	return "SUCCESSFULLY" in last_line

load_subroutine()

Compile the user subroutine uset.umat as a shared library in the directory.

Source code in matmdl/engines/abaqus.py
def load_subroutine():
	"""
	Compile the user subroutine uset.umat as a shared library in the directory.
	"""
	try:
		os.remove("libstandardU.so")
	except FileNotFoundError:
		pass
	try:
		os.remove(f'{uset.umat[:uset.umat.find(".")]}-std.o')
	except FileNotFoundError:
		pass

	subprocess.run("abaqus make library=" + uset.umat, shell=True)

pre_run(next_params, orient, in_opt)

Things to do before each run.

Source code in matmdl/engines/abaqus.py
def pre_run(next_params, orient, in_opt):
	"""Things to do before each run."""
	do_orientation_inputs(next_params, orient, in_opt)

prepare()

Main call to prepare for all runs.

Source code in matmdl/engines/abaqus.py
def prepare():
	"""
	Main call to prepare for all runs.
	"""
	load_subroutine()

run()

Run the Abaqus job!

Source code in matmdl/engines/abaqus.py
def run():
	"""Run the Abaqus job!"""
	subprocess.run(
		"abaqus job="
		+ uset.jobname
		+ " user="
		+ uset.umat[: uset.umat.find(".")]
		+ "-std.o"
		+ " cpus="
		+ str(uset.cpus)
		+ " double int ask_delete=OFF",
		shell=True,
	)

write_strain(strain, jobname)

Modify boundary conditions in main Abaqus input file to match max strain.

Parameters:

Name Type Description Default
strain float

signed float used to specify axial displacement

required
jobname str

Filename for main Abaqus job -- unique to orientation if applicable.

required
Note

Relies on finding RP-TOP under *Boundary keyword in main input file.

Source code in matmdl/engines/abaqus.py
def write_strain(strain: float, jobname: str):
	"""
	Modify boundary conditions in main Abaqus input file to match max strain.

	Args:
	    strain: signed float used to specify axial displacement
	    jobname: Filename for main Abaqus job -- unique to
	        orientation if applicable.

	Note:
	    Relies on finding ``RP-TOP`` under ``*Boundary`` keyword in main
	    input file.
	"""
	# input file:
	max_bound = round(strain * uset.length, 4)  # round to 4 digits

	if jobname[-4:] == ".inp":
		local_jobname = jobname
	else:
		local_jobname = jobname + ".inp"

	with open(local_jobname, "r") as f:
		lines = f.readlines()

	# find last number after RP-TOP under *Boundary
	bound_line_ind = [i for i, line in enumerate(lines) if line.lower().startswith("*boundary")][0]
	bound_line_ind += [
		i
		for i, line in enumerate(lines[bound_line_ind:])
		if line.strip().lower().startswith("rp-top")
	][0]
	bound_line = [number.strip() for number in lines[bound_line_ind].strip().split(",")]

	new_bound_line = bound_line[:-1] + [max_bound]
	new_bound_line_str = str(new_bound_line[0])

	for i in range(1, len(new_bound_line)):
		new_bound_line_str = new_bound_line_str + ", "
		new_bound_line_str = new_bound_line_str + str(new_bound_line[i])
	new_bound_line_str = new_bound_line_str + "\n"

	# write out
	with open(local_jobname, "w") as f:
		f.writelines(lines[:bound_line_ind])
		f.writelines(new_bound_line_str)
		f.writelines(lines[bound_line_ind + 1 :])

abaqus_extract

This runnable module requires Abaqus libraries and must be run from Abaqus python. It extracts force-displacement data from pre-defined reference points for use in comparison of model input parameters to reference data.

GetForceDisplacement

Bases: object

Open ODB file and store force-displacement data from a reference point.

Parameters:

Name Type Description Default
ResultFile str

name of odb file without the '.odb'

required

Attributes:

Name Type Description
Time

simulation time (0->1)

TopU2

displacement of reference point

TopRF2

reaction force of reference point

Note

Depends on the Abaqus names of loading step, part instance, and reference point. Requires Abaqus-specific libraries, must be called from Abaqus python.

Source code in matmdl/engines/abaqus_extract.py
class GetForceDisplacement(object):
	"""
	Open ODB file and store force-displacement data from a reference point.

	Args:
	    ResultFile (str): name of odb file without the '.odb'

	Attributes:
	    Time: simulation time (0->1)
	    TopU2: displacement of reference point
	    TopRF2: reaction force of reference point

	Note:
	    Depends on the Abaqus names of loading step, part instance, and reference point.
	    Requires Abaqus-specific libraries, must be called from Abaqus python.
	"""

	def __init__(self, ResultFile):
		CurrentPath = os.getcwd()
		self.ResultFilePath = os.path.join(CurrentPath, ResultFile + ".odb")

		self.Time = []
		self.TopU2 = []
		self.TopRF2 = []

		# Names that need to match the Abaqus simulation:
		step = "Loading"
		instance = "PART-1-1"
		TopRPset = "RP-TOP"

		odb = openOdb(path=self.ResultFilePath, readOnly=True)
		steps = odb.steps[step]
		frames = odb.steps[step].frames
		numFrames = len(frames)

		# if node set is in Part:
		TopRP = odb.rootAssembly.instances[instance].nodeSets[TopRPset]
		# if the node set is in Assembly:
		# TopNodes = odb.rootAssembly.nodeSets[NodeSetTop]

		for x in range(numFrames):
			Frame = frames[x]
			self.Time.append(Frame.frameValue)
			# Top RP results:
			Displacement = Frame.fieldOutputs["U"]
			ReactionForce = Frame.fieldOutputs["RF"]
			TopU = Displacement.getSubset(region=TopRP).values
			TopRf = ReactionForce.getSubset(region=TopRP).values
			# add to lists:
			self.TopU2 = self.TopU2 + map(lambda x: x.data[1], TopU)
			self.TopRF2 = self.TopRF2 + map(lambda x: x.data[1], TopRf)

		odb.close()

write2file()

Using GetForceDisplacement object, read odb file and write time-displacement-force to csv file.

Source code in matmdl/engines/abaqus_extract.py
def write2file():
	"""
	Using GetForceDisplacement object, read odb file and write time-displacement-force to csv file.
	"""
	job = [f for f in os.listdir(os.getcwd()) if f.endswith(".odb")][0][:-4]
	Result_Fd = GetForceDisplacement(job)
	with open("temp_time_disp_force.csv", "w") as f:
		f.write("Time, U2, RF2\n")
		for i in range(len(Result_Fd.Time)):
			f.write("%.5f," % Result_Fd.Time[i])
			f.write("%.5f," % Result_Fd.TopU2[i])
			f.write("%.5f\n" % Result_Fd.TopRF2[i])

fepx

Functions for dealing with FEPX.

extract(outname)

Get time, displacement, force data from simdir.

Note

This does extra work to match the Abaqus format... worth a change?

Source code in matmdl/engines/fepx.py
def extract(outname: str):
	"""
	Get time, displacement, force data from simdir.

	Note:
	    This does extra work to match the Abaqus format... worth a change?
	"""
	# get loading direction from config
	config = _parse_config()
	loading_dir = config["loading_direction"][0]
	strain = float(config["target_strain"][0])

	# extract output data:
	data = np.loadtxt(
		os.path.join("simulation.sim", "results", "forces", loading_dir + "1"),
		skiprows=2,
	)
	possible_dirs = ["x", "y", "z"]
	force = data[:, 2 + possible_dirs.index(loading_dir)]
	time = data[:, -1]
	time = time / max(time)

	# use uset dimensions if available, fallback to area and assumption of a cube
	if uset.length:
		length_og = uset.length
	else:
		area = data[0, 5]
		length_og = area ** (0.5)

	displacement = time * strain * length_og

	time_disp_force = np.stack(
		(time.transpose(), displacement.transpose(), force.transpose()), axis=1
	)
	header = "time, displacement, force"
	try:
		os.remove(f"temp_time_disp_force_{outname}.csv")
	except FileNotFoundError:
		pass  # fine for first run
	np.savetxt(
		f"temp_time_disp_force_{outname}.csv",
		time_disp_force,
		header=header,
		delimiter=",",
	)

has_completed()

Return True if Abaqus has finished sucessfully.

Source code in matmdl/engines/fepx.py
def has_completed():
	"""
	Return ``True`` if Abaqus has finished sucessfully.
	"""
	runlog = "temp_run_log"
	if os.path.isfile(runlog):
		try:
			check_line = subprocess.run(
				f"tail -n2 {runlog} | head -n 1",
				capture_output=True,
				check=True,
				shell=True,
			).stdout.decode("utf-8")
		except subprocess.CalledProcessError:
			raise RuntimeError("FEPX incomplete run")
	else:
		check_line = ""
	return "Final step terminated. Simulation completed successfully." in check_line

pre_run(next_params, orient, in_opt)

Things to do before each run.

Source code in matmdl/engines/fepx.py
def pre_run(next_params, orient, in_opt):
	"""Things to do before each run."""
	pass

prepare()

Main call to prepare for all runs. Nothing to do for FEPX?

Source code in matmdl/engines/fepx.py
def prepare():
	"""
	Main call to prepare for all runs. Nothing to do for FEPX?
	"""
	pass

run()

Starts FEPX, assuming fepx and mpirun are on system's path. Otherwise, give path to fepx as executable_path in input file.

Source code in matmdl/engines/fepx.py
def run():
	"""
	Starts FEPX, assuming `fepx` and `mpirun` are on system's path.
	Otherwise, give path to `fepx` as executable_path in input file.
	"""
	runlog = "temp_run_log"
	if uset.executable_path:
		fepx = uset.executable_path
	else:
		fepx = "fepx"
	subprocess.run(f"mpirun -np ${{SLURM_NTASKS}} {fepx} | tee {runlog}", shell=True)

write_strain(strain, jobname=None, debug=False)

Modify boundary conditions in main Abaqus input file to match max strain.

Parameters:

Name Type Description Default
strain float

signed float used to specify axial displacement

required
jobname str

ignored, only included to match call signature in abaqus

None
Note

Relies on finding target_strain in simulation.cfg

Source code in matmdl/engines/fepx.py
def write_strain(strain: float, jobname: str = None, debug=False):
	"""
	Modify boundary conditions in main Abaqus input file to match max strain.

	Args:
	    strain: signed float used to specify axial displacement
	    jobname: ignored, only included to match call signature in abaqus

	Note:
	    Relies on finding `target_strain` in simulation.cfg
	"""
	fname = "simulation.cfg"
	key = "target_strain"
	max_bound = round(strain, 4)  # round to 4 digits

	new_lines = []
	with open(fname, "r+") as f:
		for line in f.readlines():
			if line.strip().startswith(key):
				old_items = line.strip().split(" ")
				# ^ expecting: target_strain <strainValue> <outputFrequency> print_data
				space = " "  # to avoid another set of quotes within the follow f-string
				new_line = (
					line[0 : line.find(key)]
					+ f"{old_items[0]} {strain} {space.join(old_items[2:])}\n"
				)
				new_lines.append(new_line)
			else:
				new_lines.append(line)

	with open(f"temp_{fname}", "w+") as f:
		f.writelines(new_lines)

	if not debug:
		os.remove(fname)
		os.rename("temp_" + fname, fname)