Usage¶
The bulkdata
package loads BDF files into memory as
Deck
objects, which are collections of
Card
objects. This page will
demonstrate how to create, modify, and utilize these objects.
Card¶
Build card¶
First, let’s initialize a Card
object
with name “EXAMPLE”.
from bulkdata import Card
card = Card("EXAMPLE")
At this point, the card only has a name and no fields. If we dump the
card to its string representation in fixed format with
dumps()
, we get:
print(card.dumps("fixed"))
EXAMPLE
That’s not very useful, let’s enter some fields into the card.
# append integer field
card.append(100)
# append real field
card.append(3.14)
# append character field
card.append("string")
Now if we look at the card’s string representation we see:
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string
We can also enter blank fields.
# append a blank field using None
card.append(None)
# append a blank field using null string
card.append("")
# trailing blank fields are ingored during `dumps` call,
# so printing the card here yields the same result as the
# previous print
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string
Using the append()
method, we entered one
field at a time. But what if we want to enter a list of fields?
This is done with the extend()
method:
# append a list of integer fields
card.extend([0, 1, 2, 3, 4])
# NOTE: blank fields are there
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4
Sometimes, field entries span across two fields to allow more
characters (this is particularly common in ZAERO, where the
Large Field format doesn’t exist). Since it’s technically a
single entry, we use the append()
method
to do this while specifying the need for 2 fields, instead of
the default 1.
# append a character field spanning 2 field cells
card.append("thisislongstring", fieldspan=2)
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4 thisislongstring
There are also times when two lists of field entries have alternating
positions in the card. In this case, the easiest way to enter the
fields is with a little help from the builtin zip
function.
# append two field lists, alternating
numbers = [42, -9.99999e9, 10000000, -.0000000001]
strings = ["one", "two", "three", "four"]
for number, string in zip(numbers, strings):
card.append(number)
card.append(string)
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4 thisislongstring42 one -10.+9 two +1
+1 10000000three -1.-10 four
And if each field entry spans across two fields:
for longstring in ["123456789", "helloworld"]:
card.append(longstring, fieldspan=2)
print(card.dumps("fixed"))
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4 thisislongstring42 one -10.+9 two +1
+1 10000000three -1.-10 four 123456789 helloworld
By the way, we can also get the card’s free format representation:
print(card.dumps("free"))
EXAMPLE,100,3.14,string, , ,0,1,2,+0
+0,3,4,thisislo,ngstring,42,one,-10.+9,two,+1
+1,10000000,three,-1.-10,four,12345678,9,hellowor,ld
Printing the card object uses the dumps()
method, which defaults to fixed format if no format argument is
provided.
# these are all analogous
# print(card.dumps("fixed"))
# print(card.dumps())
print(card)
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4 thisislongstring42 one -10.+9 two +1
+1 10000000three -1.-10 four 123456789 helloworld
Modify card¶
One of the main benefits of bulkdata
is the ability to edit
existing cards, agnostic to card definitions and/or how
the card was built.
Let’s make some edits to the card we created to demonstrate what we mean. First, a simple edit to the first field of the card.
# set first field value to 99
print("First field current:", card[0])
card[0] = 99
print("First field set to 99:", card[0])
# increment first field value by 100
card[0] += 100
print("First field increment by 100:", card[0])
First field current: 100
First field set to 99: 99
First field increment by 100: 199
Now let’s update the two blank fields we set earlier to contain a character entry spanning two fields.
# the blank fields are at index 3 and 4
print("Blank fields:", card[[3, 4]])
card[[3, 4]] = "newstr"
print("One field no longer blank:", card[[3, 4]])
card[[3, 4]] = "newlongstring"
print("Both fields no longer blank:", card[[3, 4]])
Blank fields: ['', '']
One field no longer blank: ['newstr', '']
Both fields no longer blank: ['newlongs', 'tring']
Note that when we specify several indexes during the set operation, every field at that index will be cleared to make way for the new value; if the new field value does not cover every field, the leftover fields will remain blank after the set. In the first set above, the “newstr” did not require two fields, but because we specified that both index 3 and 4 fields were being set, the second field (at index 4) remained blank after.
This should clarify that the way fields are entered does not matter,
internally the card maintains a value for each field cell.
The Card
object handles the conversion of field
inputs to the appropriate field cells according to the specified
operation.
Let’s do a similar operation but with a list of new field values and indexing the set operation with slice syntax.
# we will overwrite the fields from index 10 to 16 (excluding 16)
print("Fields to overwrite:", card[10:16])
card[10:16] = [5, 6, 7, 8, 9]
print("Fields after set:", card[10:16])
Fields to overwrite: ['thisislo', 'ngstring', 42, 'one', -10000000000.0, 'two']
Fields after set: [5, 6, 7, 8, 9, '']
To remove fields, we can use the pop()
method the remove the last field…
print("Last line before pop:", card[16:])
popped_field = card.pop()
print("Popped field:", popped_field)
print("Last line after pop:", card[16:])
Last line before pop: [10000000, 'three', -1e-10, 'four', 12345678, 9, 'hellowor', 'ld']
Popped field: ld
Last line after pop: [10000000, 'three', -1e-10, 'four', 12345678, 9, 'hellowor']
… or builtin del
to remove at specified index(s)…
# delete first item of last (3rd) line
del card[16]
print("Last line after delete first:", card[16:])
# delete remaining first two items of last line
del card[16:18]
print("Last line after delete remaining first & second:", card[16:])
Last line after delete first: ['three', -1e-10, 'four', 12345678, 9, 'hellowor']
Last line after delete remaining first & second: ['four', 12345678, 9, 'hellowor']
… or resize()
, which removes fields
(or appends blank fields) until the specified size is reached.
card.resize(16)
print("Last line after resize:", card[16:])
print("Number of fields:", len(card))
Last line after resize: []
Number of fields: 16
After these modifications, we can see that the card has been updated:
print(card)
EXAMPLE 199 3.14 string newlongstring 0 1 2 +0
+0 3 4 5 6 7 8 9
The card is functionally analogous to a list
of field values.
print("Number of fields:", len(card))
print("Card field values:", card[:])
Number of fields: 16
Card field values: [199, 3.14, 'string', 'newlongs', 'tring', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '']
Alternate way of building a card¶
If we have an idea of how many fields we need, we can alternatively
initialize the card with a specified number of blank fields and
then overwrite the fields with set operations, instead of using
append()
and/or
extend()
.
# if we don't know the exact number of fields,
# we can overestimate for now and remove the excess later
numfields = 100
card = Card("EXAMPLE", size=numfields)
# indeed, our card has 100 fields
print(len(card))
100
Let’s make this card just like the one from the Build card section.
# set integer field
card[0] = 100
# set real field
card[1] = 3.14
# set character field
card[2] = "string"
# # set blank field using None
# card[3] = None
# # set blank field using null string
# card[4] = ""
# ^ that would be redundant, fields are already blank
# set list of integer fields
card[5:10] = [0, 1, 2, 3, 4]
# set character field spanning 2 field cells
card[10:12] = "thisislongstring"
# set two field lists, alternating
numbers = [42, -9.99999e9, 10000000, -.0000000001]
strings = ["one", "two", "three", "four"]
card[12:20:2] = numbers
card[13:20:2] = strings
# set field list with fields spanning 2 field cells
for i, longstring in enumerate(["123456789", "helloworld"]):
i0 = 20 + 2*i
i1 = i0 + 2
card[i0:i1] = longstring
# remove the trailing blank fields
print("Number of fields before strip:", len(card))
card.strip()
print("Number of fields after strip:", len(card))
Number of fields before strip: 100
Number of fields after strip: 24
The string representation of the card should look familar.
print(card)
EXAMPLE 100 3.14 string 0 1 2 +0
+0 3 4 thisislongstring42 one -10.+9 two +1
+1 10000000three -1.-10 four 123456789 helloworld
For more information on the Card
class,
check out the API documentation.
Deck¶
The main utility of bulkdata
is the ability to load an entire
BDF file into memory and update it with minimal effort, agnostic
to any specifications of the included cards. For this purpose,
bulkdata
provides the Deck
class.
Build deck¶
Let’s initialize a Deck
object and add
some cards.
from bulkdata import Deck
deck = Deck()
# add slight variations of the original card we created in the above section
orig_card_str = card.dumps()
for i in range(8):
# load new card from original card string
card_var = Card.loads(orig_card_str)
# just first line
card_var.resize(8)
# change name
card_var.name = "EXAMPL" + str(i)
# change first field
card_var[0] += 1
# change field i
card_var[i] = "EDITED"
deck.append(card_var)
The deck is functionally analogous to a list
of cards.
print("Number of cards:", len(deck))
print("First 3 cards:", deck[:3])
Number of cards: 8
First 3 cards: [Card("EXAMPL0", ['EDITED', 3.14, 'string', '', '', 0, 1, 2]), Card("EXAMPL1", [101, 'EDITED', 'string', '', '', 0, 1, 2]), Card("EXAMPL2", [101, 3.14, 'EDITED', '', '', 0, 1, 2])]
The dumps()
method returns the deck’s
string representation, which is the concatenation of its cards’
string representations.
Just like the Card
class, printing the
Deck
object uses the
dumps()
method,
which defaults to fixed format if no format argument is provided.
# these are all analogous
# print(deck.dumps("fixed"))
# print(deck.dumps())
print(deck)
EXAMPL0 EDITED 3.14 string 0 1 2
EXAMPL1 101 EDITED string 0 1 2
EXAMPL2 101 3.14 EDITED 0 1 2
EXAMPL3 101 3.14 string EDITED 0 1 2
EXAMPL4 101 3.14 string EDITED 0 1 2
EXAMPL5 101 3.14 string EDITED 1 2
EXAMPL6 101 3.14 string 0 EDITED 2
EXAMPL7 101 3.14 string 0 1 EDITED
And we can also specify the free format.
print(deck.dumps("free"))
EXAMPL0,EDITED,3.14,string, , ,0,1,2
EXAMPL1,101,EDITED,string, , ,0,1,2
EXAMPL2,101,3.14,EDITED, , ,0,1,2
EXAMPL3,101,3.14,string,EDITED, ,0,1,2
EXAMPL4,101,3.14,string, ,EDITED,0,1,2
EXAMPL5,101,3.14,string, , ,EDITED,1,2
EXAMPL6,101,3.14,string, , ,0,EDITED,2
EXAMPL7,101,3.14,string, , ,0,1,EDITED
Load deck¶
The driving motivation for this package is to provide the ability
to load a BDF file, generated by some external program or process,
and update its contents with minimal effort. To do this,
we use the load()
classmethod to load
the contents of a file object into a
Deck
object.
# the "usage-example.bdf" file is adapted from the pyNastran "testA.bdf" file found here:
# https://github.com/SteveDoyle2/pyNastran/blob/master/pyNastran/bdf/test/unit/testA.bdf
#
# please keep in mind that the original "testA.bdf" was created for testing purposes and
# therefore contains some "rubbish" cards (as does "usage-example.bdf")
bdf_filename = "usage-example.bdf"
with open(bdf_filename) as bdf_file:
deck = Deck.load(bdf_file)
print("Number of cards:", len(deck))
Number of cards: 143
Update deck¶
The BDF file contains an AERO card. Let’s find it.
# get all cards with name "AERO"
aero_cards = list(deck.find({"name": "AERO"}))
print("Number of AERO cards:", len(aero_cards))
aero_card = aero_cards[0]
print(aero_card.dumps("free"))
Number of AERO cards: 1
AERO, , ,1.0,1.0
The find()
method returns a generator
object with all cards from the deck matching the filter argument,
in the above case {"name": "AERO"}
. The name keyword tells
the deck to filter for cards matching the specified name.
In this case, since we know we there is only once AERO card,
we can use the find_one()
method,
which returns the first matching card directly.
Also, if we are only interested in filtering for the name, we can pass the name string directly as the filter argument, as a shortcut.
aero_card = deck.find_one("AERO")
The AERO card is defined in the NASTRAN manual as follows:
+------+-------+----------+------+--------+-------+-------+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+======+=======+==========+======+========+=======+=======+
| AERO | ACSID | VELOCITY | REFC | RHOREF | SYMXZ | SYMXY |
+------+-------+----------+------+--------+-------+-------+
| AERO | 3 | 1.3+ | 100. | 1.-5 | 1 | -1 |
+------+-------+----------+------+--------+-------+-------+
From this we can see that the AERO card in the deck has blank ACSID and VELOCITY entries; both REFC and RHOREF entries have a value of 1.0; and SYMXZ, SYMXY entries are missing (same as blank).
Let’s update it to match the example from the manual.
# ACSID
aero_card[0] = 3
# VELOCITY
aero_card[1] = 1.3
# REFC
aero_card[2] = 100.
# RHOREF
aero_card[3] = 1.0e-5
# SYMXZ
aero_card.append(1)
# SYMXY
aero_card.append(-1)
# verify that it was updated in the deck
print(deck.find_one("AERO"))
AERO 3 1.3 100. .00001 1 -1
Alternatively, we can replace the card using the
replace_one()
method.
# new AERO card
aero_new = Card("AERO")
aero_new.extend([4, 3.1, 99., 1.0e+5, -1, 1])
deck.replace_one("AERO", aero_new)
# verify that it was updated in the deck
print(deck.find_one("AERO"))
AERO 4 3.1 99. 100000. -1 1
In the case of AERO, there is only a single unique card. But what if we want to update several matching cards?
The BDF file contains several GRID cards…
grid_cards = list(deck.find("GRID"))
print("Number of GRID cards:", len(grid_cards))
Number of GRID cards: 43
… 43 to be exact. Let’s increment each GRID card’s first field (NID entry) by 1.
print("NIDs before update:")
for card in grid_cards:
print(card[0], end=" ")
# increment NID
card[0] += 1
print("\n\nNIDs after update:")
for card in deck.find("GRID"):
print(card[0], end=" ")
NIDs before update:
1 4 40 41 50 60 120 121 200 1000 1003 1004 1005 1006 1008 1009 1010 1011 1012 2573 2574 2575 2576 16411 16412 16413 16414 16415 16416 16417 16418 16419 10006 10106 10206 10306 10406 10506 10606 10706 10806 12043 31201
NIDs after update:
2 5 41 42 51 61 121 122 201 1001 1004 1005 1006 1007 1009 1010 1011 1012 1013 2574 2575 2576 2577 16412 16413 16414 16415 16416 16417 16418 16419 16420 10007 10107 10207 10307 10407 10507 10607 10707 10807 12044 31202
More on filtering¶
Until this point, we have only filtered for a name, but more complex filtering is also possible.
To find cards with specific field values, use the fields keyword:
filter_ = {
# name is MAT1
"name": "MAT1",
"fields": {
# second field
"index": 1,
# with value 3.0e7
"value": 3.0e7
}
}
for card in deck.find(filter_):
print(card, end="")
MAT1 765 3.+7
MAT1 770 3.+7
MAT1 795 3.+7
MAT1 796 3.+7
MAT1 769 3.+7
MAT1 7 3.+7
MAT1 8 3.+7
MAT1 10 3.+7
MAT1 200 3.+7
MAT1 2 3.+7
To find cards containing a specific value (in any field), use the contains keyword:
filter_ = {
# any card containing the "THRU" field
"contains": "THRU"
}
for card in deck.find(filter_):
print(card, end="")
PLOAD2 13 1. 2100001 THRU 2100003
QBDY3 34 20. 1 THRU 7 BY 2 +0
+0 10 THRU 40 BY 5 42 45 THRU +1
+1 48
QBDY3 500 50000.0 10 THRU 60 BY 10
PLOAD4 510 101 5. THRU 112
DDVAL 10 0.1 0.5 +0
+0 1.0 THRU 100. BY 1.0
ASET1 3 1 THRU 8
ASET1 3 10 THRU 16
SESET 0 1 THRU 10
Combining the name, fields, and contains filter keywords:
filter_ = {
# name is ASET1
"name": "ASET1",
"fields": {
# first field
"index": 0,
# with value 3
"value": 3
},
# contains values 1 and "THRU"
"contains": [1, "THRU"]
}
for card in deck.find(filter_):
print(card, end="")
ASET1 3 1 THRU 8
By the way, not providing a filter argument at all, will return all cards in the deck.
print(len(list(deck.find())))
143
Delete cards¶
The BDF file contains a “JUNK” card, let’s remove it from the deck.
# delete all cards with name "JUNK"
num_deleted = deck.delete("JUNK")
print("Number of cards deleted:", num_deleted)
# verify that all "JUNK" cards have been deleted
no_junk = deck.find_one("JUNK") is None
print(no_junk)
Number of cards deleted: 1
True
Just as with the find()
method,
we could delete the entire deck if we (recklessly) failed to
pass a filter argument.
It shouldn’t be necessary to demonstrate this…
Dump deck¶
When we’re ready to write our new and improved deck to file,
we use the dump()
method.
with open("usage-example-updated.bdf", "w") as bdf_file:
deck.dump(bdf_file)
Just as with the dumps()
method,
the output format of dump()
defaults
to fixed but the free format may also be specified.
with open("usage-example-updated-free.bdf", "w") as bdf_file:
deck.dump(bdf_file, format="free")
For more information on the Deck
class,
check out the API documentation.