Manipulating LS-Dyna Keyfiles in Python

Houston its not a problem, but its still annoying

As an engineer, I often want to modify an input file at a certain line. The problem is: in order to find the line and modify a value, I have to write a file parser to filter out the line. Unfortunately writing a file parser is not what I want to spend my time with as an engineer. Furthermore not only the LS-Dyna input files, but also other FEM file formats in general are proprietary, thus no standard interface for interaction exists. Thinking about this topic, it’s hard to believe we live in 2018 and not the stoneage.

One can of course use a preprocessor with scripting to modify input decks, but there are more issues ahead. Preprocessors never write the same file again, let it be that the order is different or interline comments are removed. Also using a preprocessor is often something really heavy if I just want to make a few changes and thus raises the bar in terms of “hey why don’t we just do it this way”. Modern solutions must be lighter, more direct and simple to use. That’s why we extended our qd library further.

Keyfiles in the qd python library

The qd python library supports with version 0.7.0 manipulation of LS-Dyna input files, also called Keyfiles. The main purpose was to either build up a file or manipulate an existing one, while preserving its entire structure. We tried to implement the most generic way possible since hardcoding the manual is not an option for us, but the file format gave us a hard time doing that, which resulted in 3 months of work (as you might recognize we are usually very sensitive and set high standards for our ideas 😅 ).

How simple is it to read a Keyfile?

A Keyfile can be read as follows:

>>> from qd.cae.dyna import *
>>> 
>>> # read a keyfile
>>> kf = KeyFile("path/to/keyfile", 
                 read_keywords=True, 
                 parse_mesh=True, 
                 load_includes=True)

The read_keywordsstates, that all keywords in the file shall be read. The parse_meshoption parses the textual mesh data and adds the nodes and elements into the mesh database in the backround. The mesh is also checked for consistency while parsing. Finally, with the load_includes option also the includes are loaded and are accessible via the KeyFile.get_includes()function.

We handle every Keyword generically

The Keywordclass is used to handle almost all keywords. The class saves the text of a keyword and provides comfortable means to manipulate the textual information. This allows it to save an identical file again including comments, which comes in very handy. One can check for all keywords in a file quite easily:

>>> kf.keys()
['*BOUNDARY_SPC_SET_ID', '*CONSTRAINED_INTERPOLATION_SPOTWELD', '*CONTACT_AUTOMATIC_SINGLE_SURFACE_ID', '*DATABASE_CROSS_SECTION_PLANE_ID', '*ELEMENT_SHELL', '*END', '*HOURGLASS_TITLE', '*INCLUDE', '*INITIAL_VELOCITY', '*KEYWORD', '*NODE', '*PART', '*PART_CONTACT', '*PART_INERTIA_CONTACT', '*SECTION_SHELL_TITLE', '*SET_NODE_LIST_TITLE', '*SET_PART_LIST_TITLE']

Since there can be multiple keywords of the same type, one can get a list of all keywords of a specific type via:

>>> # get all part keywords
>>> part_keywords = kf["*PART"]
>>>
>>> # Check the number of part keywords
>>> len(part_keywords)
7
>>> # select the first part_keyword
>>> kw = part_keywords[0]
>>> print(kw)
$-------------------------------------------------------------------------------
$ Parts, Sections, and Materials
$-------------------------------------------------------------------------------
*PART
$# title
engine part number one
$#     pid     secid       mid     eosid      hgid      grav    adpopt      tmid
   2000001   2000001   2000017

There are now multiple ways to manipulate the keywords such as above. Most keywords have a field size of 10 characters, but unfortunately sometimes it is also 8 or a multiple of it. There is no generic way to check this so in case of an issue, control it and set it correctly with Keyword.set_field_size. Here everything is fine, so we can simply set the pid from either the name or the indexing.

>>> # set a pid by name
>>> kw["pid"] = 500
>>> # set by indexing
>>> kw[1,0] = 500

Since we have not hardcoded the manual, the library does not know where the “pid” field actually is. Fortunately, people tend to document their files quite well, so that we can search the name “pid” in the comment line above and modify the field underneath. It doesn’t matter by the way, whether the field is aligned left or right.

Another way to access the pid field is by using indexing. The pid is in the second card and the first field of standard size 10 chars. Since counting in programming starts with 0, we can also address this field with the index of card 1 and field 0. The square brackets are a shortcut to a more generic functions, which can also be used to add a new field easily

>>> kw.set_card_valueByIndex(iCard=2, iField=0, value=123, name="custom")
>>> print(kw)
$-------------------------------------------------------------------------------
$ Parts, Sections, and Materials
$-------------------------------------------------------------------------------
*PART
$# title
engine part number one
$#     pid     secid       mid     eosid      hgid      grav    adpopt      tmid
   2000001   2000001   2000017
$custom
123

See how the library not only sets the field, but also provides the option to set a name. The values are aligned on the left here, which we dislike. The library also provides formatting utilities. First we set the global formatting to right and reformat the card.

>>> Keyword.field_alignment = Keyword.align.right
>>> Keyword.name_alignment = Keyword.align.right
>>> kw.reformat_all(skip_cards=[0])
>>> print(kw)
$-------------------------------------------------------------------------------
$ Parts, Sections, and Materials
$-------------------------------------------------------------------------------
*PART
$# title
engine part number one
$      pid     secid       mid     eosid      hgid      grav    adpopt      tmid
   2000001   2000001   2000017
$   custom
       123

There are more ways of controlling the formatting, so this is just a minor example. While reformatting we have to skip every card with non-uniform fields (here card 0). Card 0 has a name field over the entire line (dyna line limit is always 80 chars but we don’t care).  If we would set the field as before, the name would be cropped automatically to fit into the standard field size of 10 chars. One can overwrite this behavior by specifying the field size manually

>>> kw.set_card_valueByIndex(0, 0, 
                             value="Yay Im extra long but Im not cropped!", 
                             field_size=80)
>>> print(kw)
$-------------------------------------------------------------------------------
$ Parts, Sections, and Materials
$-------------------------------------------------------------------------------
*PART
$# title
                                           Yay Im extra long but Im not cropped!
$#     pid     secid       mid     eosid      hgid      grav    adpopt      tmid
   2000001   2000001   2000017
$   custom
       123

Note how the field is now aligned to the right as defined perviously.

Mesh Handling

There are specific keywords for mesh-related data.

  • NodeKeyword
  • ElementKeyword
  • PartKeyword

They are only used, if the option parse_mesh=True in the KeyFile constructor. If the mesh is not parsed, then all coordinates are promised to be identical, otherwise they will vary due to the floating point precision.

If parse_mesh=True the mesh data is not saved in the keywords as text, but in the internal mesh database. If one calls str(keyword) then the data is internally converted to a string again. One can use these classes to add for example nodes much easier:

>>> # get the first node keyword
>>> kw = kf["*NODE"][0]
>>> # to the NodeKeyword one can simply add another node
>>> node = kw.add_node(3515, x=0, y=0, z=0)
>>> nodes_of_keyword = kw.get_nodes()
>>> # Since kf is a FEMFile, we can access the mesh similar to a D3plot
>>> all_nodes = kf.get_nodes()

As an info, neither mesh keywords nor mesh data can be deleted, since for that we would have to rewrite a fraction of the engine and we simply didn’t have time for that (after all we don’t get paid for this). Be careful if parsing the mesh, since every comment line or empty line in the data block terminates the parsing and all data behind is left unloaded.

Yes, there is also include management

For include management we have another two classes in the background

  • IncludePathKeyword
  • IncludeKeyword

Every path denoted in every *INCLUDE_PATH is automatically searched when loading every *INCLUDE. One can easily add a new IncludePathKeyword as shown below

>>> # add a new keyword with a path
>>> kw = kf.add_keyword("*INCLUDE_PATH")
>>> kw.append_line("test/folder")
>>> # print the dirs 
>>> kw.get_include_dirs()
['test/folder']

One can get include files from either the respective *INCLUDE or get all includes from the KeyFile directly.

>>> # get the includes of a single include statement
>>> kw = kf["*INCLUDE"][0]
>>> kw.get_includes()
[<qd.cae.dyna_cpp.QD_KeyFile object at 0x0000021EE379BA78>]
>>> # get all include files
>>> len( kf.get_includes )
6

Every Keyword of an include is not loaded into the parent file, thus if you search a Keyword from an include, you have to search the include files (maybe we will change this). The mesh though is loaded into the parents mesh database to ensure consistency (the mesh keywords are still in the include file, but they are using the parents mesh database).

Saved files are identical

We can easily modify existing files and create identical copies. To ensure that the files are identical, we do not parse the mesh. In consequence, we also do not require to load the includes, which saves us time.

>>> # load a file
>>> kf = KeyFile("path/to/keyfile", parse_mesh=False, load_includes=False)
>>> # modify the first part keyword
>>> kw = kf["*PART"][0]
>>> kw["pid"] = 20003
>>> # write the file again
>>> kf.save("path/to/modified_keyfile")

A diff on the linux shell shows, that the files differ only on that line

diff path/to/keyfile path/to/modified_keyfile
105965c105965
<         28        28  26016102                   2         0         0
---
> 20003             28  26016102                   2         0         0

This is how our tools and software should be: fast, simple and direct.

Support Us

If you like our ideas and want us to continue, you can support us on Patreon. There are also multiple benefits:

  • Simple Supporter: You are mentioned in the backers.md in the repo
  • Sponsors: We put up your logo

 

Be the first to comment

Leave a Reply

Your email address will not be published.


*