Working with Bow Objects

Bow objects are basically containers for the kinds of DataFrames pybow uses to describe bows, plus metadata. They are not necessary to use pybow’s functions, but they can make it more convenient.

Making a Bow Object

Initialising a new bow object requires its two mandatory attributes: name and source:

>>> import pybow as pb
>>> my_bow = pb.Bow('My Bow', 'Me')

Set optional metadata attributes:

>>> my_bow.description = 'Just a bow for testing.'
>>> my_bow.material = 'hickory'

To add a limb, make its DataFrame and use the respective function:

>>> import pandas as pd
>>> df1 = pd.DataFrame([[0, 0.9, 0.75],
...                     [8, 1.58, 0.84],
                             (...)
...                     [78, 2.11, 2.45],
...                     [88, 1.9, 3.3]],
...                    columns=['l', 'width', 'thickness'])
>>> len(my_bow) # len() returns a bow object’s number of limbs
0
>>> my_bow.add_limb(df1)
>>> len(my_bow)
1

Alternatively, all attributes can be set directly at initialisation:

>>> my_other_bow = pb.Bow('My Other Bow', 'Me',
...                       description='Another Bow for testing',
...                       material='hickory'
...                       limbs=[df1])
>>> len(my_other_bow)
1
>>> my_other_bow.material
'hickory'

Querying the Bow Object

Bow objects have a few callable properties derived from its limb measurements:

>>> # Junkmanns’ measurements for the Rotten Bottom artefact
>>> rb = pb.data.junkmanns2013_rottenbottom
>>> rb.abslength # absolute length encoded in the DataFrames, in cm
113
>>> rb.ntnlength # nock-to-nock length
nan
>>> # ntnlength is only a number if a bow has two complete limbs

As a convenience, each limb’s material property returns its parent bow’s material attribute:

>>> rb.material
'yew'
>>> rb[0].material
'yew'

Bow objects yield their limbs by subscripting, like lists, or by iterating over them. Limb DataFrames are stored in a limb’s data attribute.

Subscripting:

>>> rb[0].data # DataFrame for the first limb
     l  width  thickness
0    0   0.90       0.75
1    8   1.58       0.84
         (...)
9   78   2.11       2.45
10  88   1.90       3.30

Iterating:

>>> for limb in rb:
...     print(limb.data)
     l  width  thickness
0    0   0.90       0.75
1    8   1.58       0.84
         (...)
9   78   2.11       2.45
10  88   1.90       3.30
     l  width  thickness
0   0   2.77       1.51
1   5   2.72       1.65
2  15   2.27       2.32
3  25   1.90       3.30

Iterating lends itself for filtering suitable bow limbs from a collection. Consider a limb’s boolean complete argument, denoting whether a DataFrame describes a complete limb (as opposed to a broken or not sufficiently preserved limb):

>>> rb[0].complete
True
>>> rb[1].complete
False
>>> [limb.data for limb in bow if limb.complete]
     l  width  thickness
0    0   0.90       0.75
1    8   1.58       0.84
         (...)
9   78   2.11       2.45
10  88   1.90       3.30

If you have a corpus of bow objects, you can leverage Python’s nested list comprehensions to generate a filtered list of limbs to feed into a function:

>>> my_bow_corpus = [pb.data.junkmanns2013_rottenbottom,
...                  pb.data.beckhoff1964_vrees]
>>> limbs = [limb.data
...          for bow in my_bow_corpus for limb in bow
...          if limb.complete]
>>> print(*limbs, sep='\n') # rb’s limb 1 & vrees’ only limb
     l  width  thickness
0    0   0.90       0.75
1    8   1.58       0.84
         (...)
      l  width  thickness     I  e_belly
0  14.0    2.2       1.40  0.36     0.60
1  19.3    3.0       1.55  0.59     0.65
         (...)

Note

If the order of Python’s nested list comprehension syntax confuses you, see PEP 202#the-proposed-solution for the reasoning behind it. (I am so not going to judge you.)

Running an Analysis from a Bow Object

For feeding into an analysis function, the object of choice is the limb object: Its data attribute gives you the DataFrame of measurements and its material property gives you the containing bow’s material attribute.

This is a very explicit version of the tutorial’s analysis, highlighting how the bow object’s metadata allows you to easily run analysis functions with automated input:

>>> rb[0].data
     l  width  thickness
0    0   0.90       0.75
1    8   1.58       0.84
         (...)
9   78   2.11       2.45
10  88   1.90       3.30
>>> rb[0].material
'yew'
>>> data_dict = pb.data.beckhoff1964
>>> data_dict
{'yew': {'MoE': 11770000000.0, 'MoR': 61290000.0}}
>>> pb.analysis.beckhoff1964(rb[0].data,
...                          data_dict[rb[0].material]['MoE'],
...                          data_dict[rb[0].material]['MoR'])
(drawweight     101.297062
 drawlength      84.031281
 braceheight     18.907038
 ntnlength      176.000000
 set              7.040000
 energy          32.860779
 dtype: float64,
      l  width  thickness   e_belly         I  ...
 0    0   0.90       0.75  0.318310  0.020837  ...
                     (...)
 10  88   1.90       3.30  1.400563  3.747119  ...)

The bow objects’ potential for automating analyses comes to bear most when working with a corpus of bows:

>>> import pandas as pd
>>> candidates = [limb
...               for bow in my_bow_corpus for limb in bow
...               if limb.complete]
>>> results = []
>>> for l in candidates:
...     material = data_dict[l.material]
...     series, df = pd.analysis.beckhoff1964(l.data,
...                                           material['MoE'],
...                                           material['MoR'],
...                                           name=l.name)
...     results.append(series)
>>> results_df = pd.DataFrame(results)
>>> results_df
                 drawweight  drawlength  ...
Rotten Bottom_0  101.297062   84.031281  ...
Vrees_0          194.779434   54.952892  ...

Writing to and Reading from Hard Drive

To write a bow object to disk, use its write() method:

>>> rb.write('my_bow.yaml')

This will write three files: my_bow.yaml with the bow and limb metadata, my_bow_0.csv, and my_bow_1.csv with the measurements for the limbs.

To read your bow object again, use the read() function with the bow’s YAML file’s name:

>>> rb2 = pb.read('my_bow.yaml')
>>> rb2.name
'Rotten Bottom'