Getting Started
Installation
Clone with https:
git clone https://github.com/CompOrthoBiomech/pyfebio.git
Or,
Clone with ssh:
git clone git@github.com:CompOrthoBiomech/pyfebio.git
Using uv:
Install uv from here
In top-level repository directory:
uv sync
This will create a virtual environment and install the package.
Using pip:
In top-level repository directory:
Create a virtual environment:
python -m venv .venv
Activate the virtual environment:
source .venv/bin/activate
Install the package:
pip install -e .
If you want to run the tests, additionally install the dev group dependencies:
pip install . --group dev
To verify the installation, run:
python -c "import pyfebio"
Testing
We rely on FEBio to check our generated models are valid. Therefore, you will need to have FEBio installed and available in your PATH.
To run all the tests, execute the following command:
cd src
pytest
For tests that depend on running finite element simulations, you can find them in the pytest tmp_path directory, which varies by operating system.
For the latest run:
on Linux,
cd /tmp/pytest-of-[USER]/pytest-current/[TEST_FUNCTION_NAME]current
General Overview
pyfebio utilizes the pydantic-xml package, which extends the powerful type checking libary pydantic to support XML serialization. An FEBio input file is an XML tree with the following top-level structure:
<febio_spec version="4.0">
<Module>
...
</Module>
<Globals>
...
</Globals>
<Control>
...
</Control>
<Material>
...
</Material>
<Mesh>
...
</Mesh>
<MeshDomains>
...
</MeshDomains>
<MeshData>
...
</MeshData>
<MeshAdaptor>
...
</MeshAdaptor>
<Initial>
...
</Initial>
<Boundary>
...
</Boundary>
<Loads>
...
</Loads>
<Contact>
...
</Contact>
<Constraints>
...
</Constraints>
<Rigid>
...
</Rigid>
<Discrete>
...
</Discrete>
<LoadData>
...
</LoadData>
<Step>
...
</Step>
<Output>
...
</Output>
</febio_spec>
The modules in pyfebio are divided and named based on this structure.
module.py
globals.py
control.py
material.py
mesh.py
meshdomains.py
meshdata.py
meshadaptor.py
initial.py
boundary.py
loads.py
contact.py
constraints.py
rigid.py
discrete.py
loaddata.py
step.py
output.py
We have two additional modules:
model.py – Assembles the XML tree
_types.py – Defines custom types used throughout the package
All XML elements of the FEBio model are defined as BaseXmlModel classes. These inherit from the pydantic BaseModel class, but add support for XML serialization and deserialization. pydantic-xml also provides the element and attr classes. These allow the definition of XML sub-elements and XML attributes of a BaseXmlModel class, respectively.
For example, the BCZeroDisplacement class that defines a zero displacement boundary condition,
class BCZeroDisplacement(BaseXmlModel, validate_assignment=True):
type: Literal["zero displacement"] = attr(default="zero displacement", frozen=True)
node_set: str = attr()
x_dof: Literal[0, 1] = element(default=0)
y_dof: Literal[0, 1] = element(default=0)
z_dof: Literal[0, 1] = element(default=0)
a boundary condition fixing x displacement is instantiated as,
fixed_displacment = BCDisplacement(node_set="my_node_set", x_dof=1)
yielding the XML element,
<bc type="zero displacement" node_set="my_node_set">
<x_dof>1</x_dof>
<y_dof>0</y_dof>
<z_dof>0</z_dof>
</bc>
Note how the XML tag is the class attribute name (this can be overridden by setting the alias argument if needed).
The Python type hint following each attribute variable is enforced by pydantic at runtime. Therefore for the BCZeroDisplacement class,
type – can only be “zero displacement”
node_set – must be a str and must also be provided at instantiation
x_dof – must be either 0 or 1
y_dof – must be either 0 or 1
z_dof – must be either 0 or 1
We could also place different constraints on the attribute values, such as requiring a float to be greater than zero, or even more elaborate constraints via a validator function.
Often, we need to define sub-elements, which have there own sub-elements and attributes. We handle these cases, by defining BaseXmlModel classes for these sub-elements.
For example, a load-curve is defined as,
class LoadCurve(BaseXmlModel, tag="load_controller", validate_assignment=True):
id: int = attr()
type: Literal["loadcurve"] = attr(default="loadcurve", frozen=True)
interpolate: Literal["LINEAR", "STEP", "SMOOTH"] = element(default="LINEAR")
extend: Literal["CONSTANT", "EXTRAPOLATE", "REPEAT", "REPEAT OFFSET"] = element(default="CONSTANT")
points: CurvePoints = element()
Notice the points attribute is of type CurvePoints, which is defined as,
class CurvePoints(BaseXmlModel, tag="points", validate_assignment=True):
points: list[StringFloatVec2] = element(default=[], tag="pt")
def add_point(self, new_point: StringFloatVec2):
self.points.append(new_point)
This has a few things to note:
The StringFloatVec2 is a custom type that enforces a regex constraint on a str such that it looks like “{float},{float}” including scientific notation.
When the type is an iterable, pydantic-xml will automatically create an element entry for each item
The add_point() function allows you to append additional points to the points attribute
Putting it all together, we can create a load curve via,
load_curve = LoadCurve(id=1, points=CurvePoints(points=["0.0,0.0", "0.1,1.0", "1.0,1.0")])
and add a point,
load_curve.points.add_point("2.0,2.0")
producing:
<load_controller id="1" type="loadcurve" interpolate="LINEAR" extend="CONSTANT">
<points>
<pt>0.0,0.0</pt>
<pt>0.1,1.0</pt>
<pt>1.0,1.0</pt>
<pt>2.0,2.0</pt>
</points>
</load_controller>
Recommendations
Use an IDE
pyfebio is type annotated, which enables an IDE (with language server protocol support) to provide intelligent code completion and error checking. Popular IDEs that will mostly work out-of-the-box (possibly requiring extensions to be installed but with little to no configuration) include:
PyCharm
VSCode
Zed
More customized solutions (where you’ll need to handle LSP, linter, fomatter installation and config) include,
neovim
emacs (recommend doom emacs variant)