Source code for grains.abaqus

# -*- coding: utf-8 -*-
"""
.. warning::
    This module will substantially be rewritten. See `this issue
    <https://github.com/CsatiZoltan/CristalX/issues/28>`_.

This module allows to create and manipulate Abaqus input files through the `Abaqus keywords
<https://abaqus-docs.mit.edu/2017/English/SIMACAECAERefMap/simacae-c-gen-kwbrowser.htm>`_, thereby
providing automation.
Note that it is not intended to be a complete API to Abaqus. If you want fine control over the
whole Abaqus ecosystem, consult with the Abaqus Scripting Reference Guide (`ASRG`). However,
`ASRG` needs Abaqus to be installed, moreover, you must use the Python interpreter embedded into
Abaqus. That version of Python is very old even in the latest versions of Abaqus. Furthermore,
if you need to use custom Python packages for your work, chances are high that they will not work
with the embedded interpreter, and may even crash the installation. To use this module, no Abaqus
installation is needed. In fact, only functions from the Python standard library are used.

The documentation of Abaqus version 2017 is hosted on the following website:
https://abaqus-docs.mit.edu/2017/English/SIMACAEEXCRefMap/simaexc-c-docproc.htm.
Throughout the documentation of this module (:mod:`grains.abaqus`), we will make references to
that website. If the links cease to exist, please let me know by
`opening an issue <https://github.com/CsatiZoltan/CristalX/issues/new>`_.
Alternatively, once you have registered, you can browse the documentation on the
`official website <https://www.3ds.com/support/documentation/users-guides/>`_.

Classes
-------
.. autosummary::
    :nosignatures:
    :toctree: classes/

    Geometry
    Material
    Procedure

Functions
---------
.. autosummary::
    :toctree: functions/

    extract
    validate_file

"""

# TODO:
#     - finish the `create` method
#     - put the methods in order of importance
#     - update the docstrings
#     - finish the class docstring
#     - add to version control
#     - consider using the extract function
#     - allow Abaqus keywords taking options (see the previous TODO)

import os
import re


[docs]class Material: """Adds, removes, modifies materials. Requirements: be able to - create an empty .inp file, containing only the materials - add materials to an existing .inp file TODO: only document public attributes! Attributes ---------- materials : dict materials, their behaviors and their parameters are stored here. Intended for internal representation. To view them, use the `show` method, to write them to an input file, use the `write` method. Methods ------- read(inp_file) Reads material data from an Abaqus .inp file. write(output_file=None) Writes material data to an Abaqus .inp file. remove(inp_file, output_file=None) Removes material definitions from an Abaqus .inp file. create(inp_file) Creates empty Abaqus .inp file. show() Shows material data as it appears in the Abaqus .inp file. Notes ----- The aim of this class is to programmatically add, remove, modify materials in a format understandable by Abaqus. This class does not target editing materials through a GUI (that can be done in Abaqus CAE). """
[docs] def __init__(self, from_Abaqus=False): """ Parameters ---------- from_Abaqus : bool, optional True if the input file was generated by Abaqus. The default is False. Abaqus generates input files with a consistent format. This allows certain optimizations: the input file may not need to be completely traversed to extract the materials. Third-party programs sometimes generate .inp files, which are valid but do not follow the Abaqus pattern. In this case, it cannot be predicted where the material definition ends in the file -- the whole file needs to be traversed. Returns ------- None. """ self.inp_file = '' self.state = {'begin': None, 'end': None, 'read': False} self.materials = {} self.is_greedy = from_Abaqus
[docs] def add_material(self, material): """Defines a new material by its name. Parameters ---------- material : str Name of the material to be added. A material can have multiple behaviors (e.g. elastoplastic). Returns ------- None. """ if material in self.materials: print('Material {0} already exists.'.format(material)) else: self.materials[material] = {}
[docs] def add_linearelastic(self, material, E, nu): """Adds linear elastic behavior to a given material. Parameters ---------- material : str Name of the material the behavior belongs to. E : int, float Young's modulus. nu : int, float Poisson's ratio. Returns ------- None. """ if material in self.materials: if 'Elastic' in self.materials[material]: print('Elastic behavior already exists for material {0}. ' 'Remove it first to add the new one.'.format(material)) else: self.materials[material]['Elastic'] = [[str(E), ' ' + str(nu)]] else: print('Material {0} does not exist. Behavior not added.'.format(material))
[docs] def add_plastic(self, material, sigma_y, epsilon_p): """Adds metal plasticity behavior to a given material. Parameters ---------- material : str Name of the material the behavior belongs to. sigma_y : int, float Yield stress. epsilon_p : int, float Plastic strain. Returns ------- None. """ if material in self.materials: if 'Plastic' in self.materials[material]: print('Plastic behavior already exists for material {0}. ' 'Remove it first to add the new one.'.format(material)) else: self.materials[material]['Plastic'] = [[str(sigma_y), ' ' + str(epsilon_p)]] else: print('Material {0} does not exist. Behavior not added.'.format(material))
[docs] def read(self, inp_file): """Reads material data from an Abaqus .inp file. Parameters ---------- inp_file : str Abaqus input (.inp) file to be created. Returns ------- None. Notes ----- - This method is designed to read material data. Although the logic could be used to process other properties (parts, assemblies, etc.) in an input file, they are not yet implemented in this class. - This method assumes that the input file is valid. If it is, the material data can be extacted. If not, the behavior is undefined: the program can crash or return garbage. This is by design: the single responsibility principle dictates that the validity of the input file must be provided by other methods. If the input file was generated from within Abaqus CAE, it is guaranteed to be valid. The `write` method of this class also ensures that the resulting input file is valid. This design choice also makes the program logic simpler. For valid syntax in the input file, check the Input Syntax Rules section in the Abaqus user's guide. - To read material data from an input file, one has to identify the structure of .inp files in Abaqus. Abaqus is driven by keywords and corresponding data. For a list of accepted keywords, consult the Abaqus Keywords Reference Guide. There are three types of input lines in Abaqus: - keyword line: begins with a star, followed by the name of the keyword. Parameters, if any, are separated by commas and are given as parameter-value pairs. Keywords and parameters are not case sensitive. Example: *ELASTIC, TYPE=ISOTROPIC, DEPENDENCIES=1 Some keywords can only be defined once another keyword has already been defined. E.g. the keyword ELASTIC must come after MATERIAL in a valid .inp file. - data line: immediately follows a keyword line. All data items must be separated by commas. Example: -12.345, 0.01, 5.2E-2, -1.2345E1 - comment line: starts with ** and is ignored by Abaqus. Example: ** This is a comment line - Internally, the materials are stored in a dictionary. It holds the material data read from the file. The keys in this dictionary are the names of the materials, and the values are dictionaries themselves. Each such dictionary stores a behavior for the given material. E.g. an elastoplastic material is governed by an elastic and a plastic behavior. The parameters for each behavior are stored in a list. """ if self.state['read']: raise Exception('Material database already exists. Instantiate a ' 'new Material object for another database.') validate_file(inp_file, 'read') in_material = False ## true when reached the material definition block materials = {} ## holds the materials read from the input file generalkeyword = re.compile('^\*\s?\w') # any Abaqus keyword material = re.compile('\*material, name=([\w-]+)', re.IGNORECASE) behavior = re.compile('\*(plastic|elastic)', re.IGNORECASE) comment = re.compile('^[\s]?\*\*') is_greedy = self.is_greedy with open(inp_file, 'r') as file: for line_number, line in enumerate(file, 0): # read the file line by line is_generalkeyword = generalkeyword.match(line) is_material = material.match(line) is_behavior = behavior.match(line) is_comment = comment.match(line) is_parameter = not is_generalkeyword and not is_comment is_material_param = is_parameter and in_material if is_material: if not in_material: # we entered the first material in_material = True begin = line_number material_name = is_material.group(1) materials[material_name] = {} elif is_behavior: behavior_name = is_behavior.group(1) materials[material_name][behavior_name] = [] elif is_material_param: params = line[0:-1].split(',') materials[material_name][behavior_name].append(params) elif (is_generalkeyword or is_comment) and in_material and is_greedy: # We were previously in a material definition section but # now we detected a new keyword. In an Abaqus-generated # .inp file this indicates that the material section has # ended: no need to search for more end = line_number-1 break if 'end' not in locals(): # input file ends with materials end = line_number if 'begin' not in locals(): # input file does not contain materials begin = line_number+1 self.inp_file = inp_file self.materials = materials self.state['begin'] = begin self.state['end'] = end self.state['read'] = True
[docs] def write(self, output_file=None): """Writes material data to an Abaqus .inp file. Parameters ---------- output_file : str, optional Output file name to write the modifications into. If not given, the original file name is appended with '_mod'. Returns ------- None. Notes ----- - If the output file name is the same as the input file, the original .inp file will be overwritten. This is strongly not recommended. - The whole content of the original input file is read to memory. It might be a problem for very large .inp files. In that case, a possible implementation could be the following: 1. Remove old material data 2. Append new material data to the proper position in the file Appending is automatically done at the end of the file. Moving the material description to the end of the file is not possible in general because defining materials cannot be done from any module, i.e. the *MATERIAL keyword cannot follow an arbitrary keyword. In this case, Abaqus throws an AbaqusException with the following message: It can be suboption for the following keyword(s)/level(s): model """ if not self.materials: raise Exception('Material does not exist. Nothing to write.') if self.state["read"]: # database was created by reading a .inp file with open(self.inp_file, 'r') as source: old = source.readlines() before = old[0:self.state['begin']] after = old[self.state['end']+1:] new = before + self.__format() + after if not output_file: name, extension = os.path.splitext(self.inp_file) output_file = name + '_mod' + extension validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(new)) else: # database was created manually if not output_file: dirname = os.path.split(os.path.abspath(__file__))[0] files = os.listdir() filename = 'materials.inp' i = 0 while filename in files: i += 1 filename = 'materials-{0}.inp'.format(i) output_file = filename validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(self.__format())) print('Material database saved as `{0}`.'.format(output_file))
[docs] def create(self, inp_file): """Creates empty Abaqus .inp file. Parameters ---------- inp_file : str Abaqus input file to be created. If an extension is not given, the default .inp is used. Returns ------- None. """ self.state['created'] = True
[docs] @staticmethod def remove(inp_file, output_file=None): """Removes material definitions from an Abaqus .inp file. Parameters ---------- inp_file : str Abaqus .inp file from which the materials should be removed. output_file : str, optional Output file name to write the modifications into. If not given, the original file name is appended with '_mod'. Returns ------- None. """ with open(inp_file, 'r') as source: a = Material() a.read(inp_file) old = source.readlines() before = old[0:a.state['begin']] after = old[a.state['end']+1:] new = before + after if not output_file: name, extension = os.path.splitext(inp_file) output_file = name + '_mod' + extension validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(new))
[docs] @staticmethod def add_sections(inp_file, output_file=None): """Adds section definitions to an Abaqus .inp file. Defines properties for elements by associating materials to them. The element set containing the elements for which the material behavior is being defined is assumed to have the same name as that of the material. E.g. if materials with names `mat-1` and `mat-2` exist, element sets with names `mat-1` and `mat-2` must also exist. If such element sets do not exist, Abaqus will throw a warning and the section assignment will not be successful. Parameters ---------- inp_file : str Abaqus .inp file from which the materials should be removed. output_file : str, optional Output file name to write the modifications into. If not given, the original file name is appended with '_mod'. Returns ------- None. Notes ----- If fine control is required for associating custom material names to custom element set names, that can be done from the Abaqus GUI. The purpose of this method is automation for large number of element sets, each associated to a (possibly distinct) material. In that case, custom element set names are not reasonable any more, and having the same names for the element sets and for the materials is completely meaningful. """ with open(inp_file, 'r') as source: a = Material() a.read(inp_file) old = source.readlines() if a.state['begin'] == a.state['end'] + 1: # input file does not contain materials raise Exception('Material does not exist.') sections = [] for material in a.materials.keys(): sections.append('*SOLID SECTION, ELSET={0}, MATERIAL={0}\n'.format(material)) new = old + sections if not output_file: name, extension = os.path.splitext(inp_file) output_file = name + '_mod' + extension validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(new))
[docs] def show(self): """Shows material data as it appears in the Abaqus .inp file. Returns ------- None. """ print(''.join(self.__format()))
def __format(self): """Formats the material data in the Abaqus .inp format. The internal representation of the material data in converted to a string understood by Abaqus. Returns ------- abaqus_format : list List of strings, each element of the list corresponding to a line (with \n line ending) in the Abaqus .inp file. In case of no material, an empty list is returned. Notes ----- The output is a list so that further concatenation operations are easy. If you want a string, merge the elements of the list: output = ''.join(output) This is what the `show` method does. """ abaqus_format = ['\n'] for material_name, behaviors in self.materials.items(): abaqus_format.append('*Material, name={0}\n'.format(material_name)) for behavior_name, parameters in behaviors.items(): abaqus_format.append('*{0}\n'.format(behavior_name)) for parameter in parameters: abaqus_format.append(','.join(parameter) + '\n') return abaqus_format @staticmethod def __isnumeric(x): """Decides if the input is a scalar number. Parameters ---------- x : any type Input to be tested. Returns ------- bool True if the given object is a scalar number. """ return isinstance(x, (int, float, complex)) and not isinstance(x, bool) def __str__(self): """Customizes how the object is printed. Displays basic information about the materials. For detailed information, use the `show` method. Returns ------- str DESCRIPTION. """ n_material = len(self.materials) if n_material in {0, 1}: display = ['{0} material.\n'.format(n_material)] else: display = ['{0} materials.\n'.format(n_material)] for material_name in self.materials: display.append(' {0}\n'.format(material_name)) display = ''.join(display) return display
[docs]class Geometry: """Geometrical operations on the mesh. """
[docs] def __init__(self): self.inp_file = '' self.state = {'begin': None, 'end': None, 'read': False} self.nodes = {}
[docs] def read(self, inp_file): """Reads material data from an Abaqus .inp file. Parameters ---------- inp_file : str Abaqus input (.inp) file to be created. Returns ------- None. Notes ----- - This method is designed to read material data. Although the logic could be used to process other properties (parts, assemblies, etc.) in an input file, they are not yet implemented in this class. - This method assumes that the input file is valid. If it is, the material data can be extacted. If not, the behavior is undefined: the program can crash or return garbage. This is by design: the single responsibility principle dictates that the validity of the input file must be provided by other methods. If the input file was generated from within Abaqus CAE, it is guaranteed to be valid. The `write` method of this class also ensures that the resulting input file is valid. This design choice also makes the program logic simpler. For valid syntax in the input file, check the Input Syntax Rules section in the Abaqus user's guide. - To read material data from an input file, one has to identify the structure of .inp files in Abaqus. Abaqus is driven by keywords and corresponding data. For a list of accepted keywords, consult the Abaqus Keywords Reference Guide. There are three types of input lines in Abaqus: - keyword line: begins with a star, followed by the name of the keyword. Parameters, if any, are separated by commas and are given as parameter-value pairs. Keywords and parameters are not case sensitive. Example: *ELASTIC, TYPE=ISOTROPIC, DEPENDENCIES=1 Some keywords can only be defined once another keyword has already been defined. E.g. the keyword ELASTIC must come after MATERIAL in a valid .inp file. - data line: immediately follows a keyword line. All data items must be separated by commas. Example: -12.345, 0.01, 5.2E-2, -1.2345E1 - comment line: starts with ** and is ignored by Abaqus. Example: ** This is a comment line - Internally, the materials are stored in a dictionary. It holds the material data read from the file. The keys in this dictionary are the names of the materials, and the values are dictionaries themselves. Each such dictionary stores a behavior for the given material. E.g. an elastoplastic material is governed by an elastic and a plastic behavior. The parameters for each behavior are stored in a list. """ if self.state['read']: raise Exception('Material database already exists. Instantiate a ' 'new Material object for another database.') validate_file(inp_file, 'read') in_node = False ## true when reached the node definition block nodes = [] ## holds the materials read from the input file generalkeyword = re.compile('^\*\s?\w') # any Abaqus keyword node = re.compile('\*node', re.IGNORECASE) comment = re.compile('^[\s]?\*\*') with open(inp_file, 'r') as file: for line_number, line in enumerate(file, 0): # read the file line by line is_generalkeyword = generalkeyword.match(line) is_node = node.match(line) is_comment = comment.match(line) is_parameter = not is_generalkeyword and not is_comment is_node_param = is_parameter and in_node if is_node: # we entered the NODE definition block in_node = True begin = line_number elif is_node_param: params = line[0:-1].split(',') nodes.append(params) elif (is_generalkeyword or is_comment) and in_node: # We were previously in a material definition section but # now we detected a new keyword. In an Abaqus-generated # .inp file this indicates that the material section has # ended: no need to search for more end = line_number-1 break if 'end' not in locals(): # input file ends with nodes end = line_number self.inp_file = inp_file self.nodes = nodes self.state['begin'] = begin self.state['end'] = end self.state['read'] = True
[docs] def scale(self, factor): """Scales the geometry by modifying the coordinates of the nodes. Parameters ---------- factor : float Each nodal coordinate is multiplied by this non-negative number. Returns ------- None. Notes ----- The modification happens in-place. """ if not self.nodes: raise Exception('Geometry does not exist. Load it first with the `read` method.') for node in self.nodes: for index, coordinate in enumerate(node[1:], 1): node[index] = ' ' + str(float(coordinate)*factor)
[docs] def write(self, output_file=None): """Writes material data to an Abaqus .inp file. Parameters ---------- output_file : str, optional Output file name to write the modifications into. If not given, the original file name is appended with '_mod'. Returns ------- None. Notes ----- - If the output file name is the same as the input file, the original .inp file will be overwritten. This is strongly not recommended. - The whole content of the original input file is read to memory. It might be a problem for very large .inp files. In that case, a possible implementation could be the following: 1. Remove old material data 2. Append new material data to the proper position in the file Appending is automatically done at the end of the file. Moving the material description to the end of the file is not possible in general because defining materials cannot be done from any module, i.e. the *MATERIAL keyword cannot follow an arbitrary keyword. In this case, Abaqus throws an AbaqusException with the following message: It can be suboption for the following keyword(s)/level(s): model """ if not self.nodes: raise Exception('Material does not exist. Nothing to write.') with open(self.inp_file, 'r') as source: old = source.readlines() before = old[0:self.state['begin']] after = old[self.state['end']+1:] new = before + self.__format() + after if not output_file: name, extension = os.path.splitext(self.inp_file) output_file = name + '_mod' + extension validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(new)) print('Material database saved as `{0}`.'.format(output_file))
def __format(self): """Formats the material data in the Abaqus .inp format. The internal representation of the material data in converted to a string understood by Abaqus. Returns ------- abaqus_format : list List of strings, each element of the list corresponding to a line (with \n line ending) in the Abaqus .inp file. In case of no material, an empty list is returned. Notes ----- The output is a list so that further concatenation operations are easy. If you want a string, merge the elements of the list: output = ''.join(output) This is what the `show` method does. """ abaqus_format = ['*NODE\n'] for node in self.nodes: abaqus_format.append(','.join(node) + '\n') return abaqus_format
[docs]class Procedure: """Handling analysis steps during the simulation. TODO: only document public attributes! Attributes ---------- steps : dict analysis steps, each step collecting the necessary information. Intended for internal representation. To view them, use the :meth:`show` method, to write them to an input file, use the :meth:`write` method. """
[docs] def __init__(self, from_Abaqus=False): """ Parameters ---------- from_Abaqus : bool, optional True if the input file was generated by Abaqus. The default is False. Abaqus generates input files with a consistent format. This allows certain optimizations: the input file may not need to be completely traversed to extract the materials. Third-party programs sometimes generate .inp files, which are valid but do not follow the Abaqus pattern. In this case, it cannot be predicted where the material definition ends in the file -- the whole file needs to be traversed. Returns ------- None. """ self.inp_file = '' self.state = {'begin': None, 'end': None, 'read': False} self.steps = {} self.is_greedy = from_Abaqus
[docs] def create_step(self, name, nlgeom=False, max_increments=100): """Defines a new step. A `step` is the fundamental part of the simulation workflow in Abaqus. Among the `available options <https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/ simakey-r-step.htm#simakey-r-step>`_, only the `NLGEOM` and `INC` options are supported currently. .. todo:: Check in the :meth:`~Procedure.write` method whether every step contains an analysis, as this is required by Abaqus. Parameters ---------- name : str Name of the analysis step to be added. Step names must be unique. nlgeom : bool, optional Omit this parameter or set to False to perform a geometrically linear analysis during the current step. Set it to True to indicate that geometric nonlinearity should be accounted for during the step. Once the :code:`nlgeom` option has been switched on, it will be active during all subsequent steps in the analysis. The default is False. max_increments : int The analysis will stop if the maximum number of increments is exceeded before the complete solution for the step has been obtained. The default is 100. Returns ------- None """ if name in self.steps: print('"{0}" already exists. Choose a different name.'.format(name)) else: nlgeom = 'YES' if nlgeom else 'NO' self.steps[name] = {'analysis': [], 'boundary_conditions': {}, 'step_options': {'nlgeom': nlgeom, 'inc': max_increments}}
[docs] def add_analysis(self, step, time_period=1.0, initial_increment=None, min_increment=0, max_increment=None): """Adds an analysis type to a given step. .. note:: Currently only static stress/displacement analysis is supported, indicated by the `STATIC <https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/ simakey-r-static.htm#simakey-r-static>`_ Abaqus keyword. No optional parameters are supported. .. todo:: Check user inputs. Define exceptions (`ValueError` class) with the error messages Abaqus CAE throws. Parameters ---------- step : str Name of the step the analysis type belongs to. initial_increment : float, optional Initial time increment. This value will be modified as required if the automatic time stepping scheme is used. If this entry is zero or is not specified, a default value that is equal to the total time period of the step is assumed. time_period : float, optional Time period of the step. The default is 1.0. min_increment : float, optional Only used for automatic time incrementation. If ABAQUS/Standard finds it needs a smaller time increment than this value, the analysis is terminated. If this entry is zero, a default value of the smaller of the suggested initial time increment or 1e–5 times the total time period is assumed. max_increment : float, optional Maximum time increment allowed. Only used for automatic time incrementation. If this value is not specified, no upper limit is imposed, i.e. :code:`max_increment = time_period`. Returns ------- None Examples -------- >>> proc = Procedure() >>> proc.create_step('Step-1', max_increments=1000) >>> proc.add_analysis('Step-1', initial_increment=0.001, min_increment=1e-8) """ if initial_increment in {0, None}: initial_increment = time_period if min_increment == 0: min_increment = min(initial_increment, 1e-5*time_period) if not max_increment: max_increment = time_period if step in self.steps: if self.steps[step]['analysis']: print('Analysis already exists for {0}. Remove it first to add the new one.' .format(step)) else: self.steps[step]['analysis'] = [str(initial_increment), ' ' + str(time_period), ' ' + str(min_increment), ' ' + str(max_increment)] else: print('"{0}" does not exist. Analysis not added.'.format(step))
[docs] def add_boundary_condition(self, name, step, nodes, first_dof, last_dof=None, magnitude=0.0): """Adds boundary condition to a given step. Boundary conditions can be prescribed on node sets, using the Abaqus keyword `BOUNDARY <https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/simakey-r-boundary.htm#simakey -r-boundary>`_. Optional parameters are not supported. Multiple boundary conditions can be given on the same node set. It is the responsibility of the user to ensure that the constraints are compatible. .. note:: Currently, boundary conditions can only be defined in a user-defined step, and not in the initial step. Parameters ---------- name : str Name of the boundary condition to be added. Boundary condition names must be unique. step : str Name of the step the boundary condition belongs to. nodes : str or int If a string, the label of the node set, if a positive integer, the label of a single node on which the boundary condition is prescribed. first_dof : int First degree of freedom constrained. last_dof : int, optional Last degree of freedom constrained. Not necessary to be given if only one degree of freedom is being constrained. magnitude : float Magnitude of the variable. If this magnitude is a rotation, it must be given in radians. The default value is 1.0. Returns ------- None Examples -------- Let us consider a two-dimensional structure, fixed on a part of its boundary (called `'left'`), and displaced on another part (called `'right'`). First, we create an analysis step (named `'Step-1'`). >>> proc = Procedure() >>> proc.create_step('Step-1', max_increments=1000) Now, we prescribe the zero displacements. >>> proc.add_boundary_condition('fixed', 'Step-1', 'left', first_dof=1, last_dof=2) Since we did not specify the magnitude of the displacement components, they are zero by default. The other boundary condition prescribes different values for the horizontal and vertical components. Hence, they are given separately. >>> proc.add_boundary_condition('pulled_right', 'Step-1', 'right', first_dof=1, magnitude=2) >>> proc.add_boundary_condition('fixed_right', 'Step-1', 'right', first_dof=2, magnitude=0) Note that we can also prescribe boundary conditions at a particular node, e.g. >>> proc.add_boundary_condition('at_node', 'Step-1', 1, first_dof=2, magnitude=-1.2) where we set the vertical displacement component to -1.2 at node 1. """ if not last_dof: last_dof = first_dof if type(nodes) not in {int, str}: raise TypeError('Give either a single node or a node set.') if step in self.steps: if name in self.steps[step]['boundary_conditions']: print('Boundary condition "{0}" already exists for step {1}. Remove it first to ' 'add the new one or choose another name.'.format(name, step)) else: self.steps[step]['boundary_conditions'][name] = [str(nodes), ' ' + str(first_dof), ' ' + str(last_dof), ' ' + str(float(magnitude))] else: print('"{0}" does not exist. Boundary condition not added.'.format(step))
[docs] def read(self, inp_file): """Reads procedure data from an Abaqus .inp file. Parameters ---------- inp_file : str Abaqus input (.inp) file to be created. Returns ------- None. Notes ----- - This method is designed to read material data. Although the logic could be used to process other properties (parts, assemblies, etc.) in an input file, they are not yet implemented in this class. - This method assumes that the input file is valid. If it is, the material data can be extacted. If not, the behavior is undefined: the program can crash or return garbage. This is by design: the single responsibility principle dictates that the validity of the input file must be provided by other methods. If the input file was generated from within Abaqus CAE, it is guaranteed to be valid. The `write` method of this class also ensures that the resulting input file is valid. This design choice also makes the program logic simpler. For valid syntax in the input file, check the Input Syntax Rules section in the Abaqus user's guide. - To read material data from an input file, one has to identify the structure of .inp files in Abaqus. Abaqus is driven by keywords and corresponding data. For a list of accepted keywords, consult the Abaqus Keywords Reference Guide. There are three types of input lines in Abaqus: - keyword line: begins with a star, followed by the name of the keyword. Parameters, if any, are separated by commas and are given as parameter-value pairs. Keywords and parameters are not case sensitive. Example: *ELASTIC, TYPE=ISOTROPIC, DEPENDENCIES=1 Some keywords can only be defined once another keyword has already been defined. E.g. the keyword ELASTIC must come after MATERIAL in a valid .inp file. - data line: immediately follows a keyword line. All data items must be separated by commas. Example: -12.345, 0.01, 5.2E-2, -1.2345E1 - comment line: starts with ** and is ignored by Abaqus. Example: ** This is a comment line - Internally, the materials are stored in a dictionary. It holds the material data read from the file. The keys in this dictionary are the names of the materials, and the values are dictionaries themselves. Each such dictionary stores a behavior for the given material. E.g. an elastoplastic material is governed by an elastic and a plastic behavior. The parameters for each behavior are stored in a list. """ if self.state['read']: raise Exception('Step definition database already exists. Instantiate a ' 'new Procedure object for another database.') validate_file(inp_file, 'read') in_step = False # true when reached the step definition block in_analysis = False # true when reached an analysis definition block in_bc = False # true when reached a boundary condition definition block steps = {} # holds the materials read from the input file bc_count = 0 generalkeyword = re.compile('^\*\s?\w') # any Abaqus keyword # step = re.compile('\*step, name=([\w-]+), nlgeom=([\w-]+), inc=([0-9]+)', re.IGNORECASE) step = re.compile('\*step, ', re.IGNORECASE) end_step = re.compile('\*end step', re.IGNORECASE) analysis = re.compile('\*static', re.IGNORECASE) bc = re.compile('\*boundary', re.IGNORECASE) comment = re.compile('^[\s]?\*\*') is_greedy = self.is_greedy with open(inp_file, 'r') as file: for line_number, line in enumerate(file, 0): # read the file line by line is_generalkeyword = generalkeyword.match(line) is_step = step.match(line) is_end_step = end_step.match(line) is_analysis = analysis.match(line) is_bc = bc.match(line) is_comment = comment.match(line) is_parameter = not is_generalkeyword and not is_comment is_analysis_param = is_parameter and in_analysis is_bc_param = is_parameter and in_bc if is_step: if not in_step: # we entered the first material in_step = True begin = line_number step_name = is_step.group(1) steps[step_name] = {} steps[step_name]['boundary_conditions'] = {} nlgeom = is_step.group(2) max_increments = int(is_step.group(3)) steps[step_name]['step_options'] = {'nlgeom': nlgeom, 'inc': max_increments} elif is_analysis: in_analysis = True in_bc = False elif is_bc: bc_count += 1 in_bc = True in_analysis = False bc_name = 'BC-' + str(bc_count) steps[step_name]['boundary_conditions'][bc_name] = [] elif is_analysis_param: params = line[0:-1].split(',') if len(params) == 0: time_period = 1.0 initial_increment = time_period min_increment = min(initial_increment, 1e-5*time_period) max_increment = time_period if len(params) == 1: initial_increment = float(params[0]) time_period = 1.0 min_increment = min(initial_increment, 1e-5 * time_period) max_increment = time_period if len(params) == 2: initial_increment = float(params[0]) time_period = float(params[1]) min_increment = min(initial_increment, 1e-5 * time_period) max_increment = time_period if len(params) == 3: initial_increment = float(params[0]) time_period = float(params[1]) min_increment = float(params[2]) max_increment = time_period if len(params) == 4: initial_increment = float(params[0]) time_period = float(params[1]) min_increment = float(params[2]) max_increment = float(params[3]) params = [initial_increment, time_period, min_increment, max_increment] steps[step_name]['analysis'] = params elif is_bc_param: params = line[0:-1].split(',') if len(params) < 2: raise ValueError('') if len(params) == 2: params[1] = int(params[1]) params.extend([params[1], 0.0]) elif len(params) == 3: params[1] = int(params[1]) params[2] = int(params[2]) params.append(0.0) elif len(params) == 4: params[1] = int(params[1]) params[2] = int(params[2]) params[3] = float(params[3]) else: raise ValueError('') # node_set, first_dof, last_dof, magnitude steps[step_name]['boundary_conditions'][bc_name].append(params) elif is_end_step: in_step = False elif (is_generalkeyword or is_comment) and in_step and is_greedy: # We were previously in a step definition section but now we detected a new # keyword. In an Abaqus-generated .inp file this indicates that the step # definition section has ended: no need to search for more. end = line_number-1 break if 'end' not in locals(): # input file ends with materials end = line_number if 'begin' not in locals(): # input file does not contain materials begin = line_number+1 self.inp_file = inp_file self.steps = steps self.state['begin'] = begin self.state['end'] = end self.state['read'] = True
[docs] def write(self, output_file=None): """Writes step definition data to an Abaqus .inp file. Parameters ---------- output_file : str, optional Output file name to write the modifications into. If not given, the original file name is appended with '_mod'. Returns ------- None. Notes ----- - If the output file name is the same as the input file, the original .inp file will be overwritten. This is strongly not recommended. - The whole content of the original input file is read to memory. It might be a problem for very large .inp files. In that case, a possible implementation could be the following: 1. Remove old material data 2. Append new material data to the proper position in the file Appending is automatically done at the end of the file. Moving the material description to the end of the file is not possible in general because defining materials cannot be done from any module, i.e. the *MATERIAL keyword cannot follow an arbitrary keyword. In this case, Abaqus throws an AbaqusException with the following message: It can be suboption for the following keyword(s)/level(s): model """ if not self.steps: raise Exception('Steps do not exist. Nothing to write.') if self.state["read"]: # database was created by reading a .inp file with open(self.inp_file, 'r') as source: old = source.readlines() before = old[0:self.state['begin']] after = old[self.state['end']+1:] new = before + self.__format() + after if not output_file: name, extension = os.path.splitext(self.inp_file) output_file = name + '_mod' + extension validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(new)) else: # database was created manually if not output_file: dirname = os.path.split(os.path.abspath(__file__))[0] files = os.listdir() filename = 'steps.inp' i = 0 while filename in files: i += 1 filename = 'steps-{0}.inp'.format(i) output_file = filename validate_file(output_file, 'write') with open(output_file, 'w') as target: target.write(''.join(self.__format())) print('Step definition file saved as `{0}`.'.format(output_file))
[docs] def show(self): """Shows the text for the `step` module, as it appears in the Abaqus .inp file. Returns ------- None. """ print(''.join(self.__format()))
def __format(self): """Formats the data in the Abaqus .inp format. The internal representation of the steps data in converted to a string understood by Abaqus. Returns ------- abaqus_format : list List of strings, each element of the list corresponding to a line (with \n line ending) in the Abaqus .inp file. In case of no data, an empty list is returned. Notes ----- The output is a list so that further concatenation operations are easy. If you want a string, merge the elements of the list: :code:`output = ''.join(output)` This is what the :meth:`show` method does. """ for step_name, modules in self.steps.items(): abaqus_format = ['\n** -------------------------------------------------------------\n'] abaqus_format.append('**\n** STEP: {0}\n**\n'.format(step_name)) nlgeom = modules['step_options']['nlgeom'] increment = modules['step_options']['inc'] abaqus_format.append('*Step, name={0}, nlgeom={1}, inc={2}\n' .format(step_name, nlgeom, increment)) for module_name, module_items in modules.items(): if module_name == 'analysis': abaqus_format.append('*Static\n') abaqus_format.append(','.join(module_items) + '\n') elif module_name == 'boundary_conditions': abaqus_format.append('**\n** BOUNDARY CONDITIONS\n**\n') for bc_name, bc in module_items.items(): abaqus_format.append('** Name: {0}\n'.format(bc_name)) abaqus_format.append('*Boundary\n') abaqus_format.append(','.join(bc) + '\n') elif module_name != 'step_options': raise Exception('Currently only boundary conditions and analysis types are ' 'supported.') abaqus_format.append('*End Step\n') print(''.join(abaqus_format)) return abaqus_format def __str__(self): """Customizes how the object is printed. For detailed information, use the :func:`show` method. Returns ------- str Number of steps and their names. """ n_step = len(self.steps) if n_step in {0, 1}: display = ['{0} step defined.\n'.format(n_step)] else: display = ['{0} steps defined.\n'.format(n_step)] for step_name in self.steps: display.append(' {0}\n'.format(step_name)) display = ''.join(display) return display
[docs]def validate_file(file, caller): """Input or output file validation. Parameters ---------- file : str Existing Abaqus .inp file or a new .inp file to be created. caller : {'read', 'write', 'create'} Method name that called this function. Returns ------- None. """ if type(file) is not str: raise Exception('String expected for file name.') file_extension = os.path.splitext(file)[1] if file_extension != '.inp': print('File extension `{0}` is not the conventional `.inp` ' 'used by Abaqus.'.format(file_extension)) existing_file = os.path.isfile(file) if caller in {'read'}: if not existing_file: raise Exception('File does not exist.') empty_file = os.stat(file).st_size == 0 if empty_file: raise Exception('Input file is empty.') elif caller in {'write', 'create'}: if existing_file: print('File `{0}` already exists. It will be overwritten!'.format(file)) else: raise Exception('Only the following methods are allowed to call ' 'this function: "read"", "write" "create".')
[docs]def extract(keyword): """Obtains Abaqus keyword and its parameters. Parameters ---------- keyword : str Some examples: '*Elastic, type=ORTHOTROPIC' '*Damage Initiation, criterion=HASHIN' '*Nset, nset=Set-1, generate' Returns ------- separated : list DESCRIPTION. """ # Split the string along the commas split = keyword[1:].split(',') # Remove leading and trailing whitespaces stripped = [x.strip() for x in split] # Separate parameters from values separated = [x.split('=') for x in stripped] return separated