Getting Started =============== Installation ------------ Clone with https: .. code-block:: bash git clone https://github.com/CompOrthoBiomech/pyfebio.git Or, Clone with ssh: .. code-block:: bash git clone git@github.com:CompOrthoBiomech/pyfebio.git **Using uv:** Install uv from `here `_ In top-level repository directory: .. code-block:: bash uv sync This will create a virtual environment and install the package. **Using pip:** In top-level repository directory: Create a virtual environment: .. code-block:: bash python -m venv .venv Activate the virtual environment: .. code-block:: bash source .venv/bin/activate Install the package: .. code-block:: bash pip install -e . If you want to run the tests, additionally install the dev group dependencies: .. code-block:: bash pip install . --group dev To verify the installation, run: .. code-block:: bash 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: .. code-block:: bash 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, .. code-block:: bash 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: .. code-block:: xml ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 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, .. code-block:: python 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, .. code-block:: python fixed_displacment = BCDisplacement(node_set="my_node_set", x_dof=1) yielding the XML element, .. code-block:: xml 1 0 0 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, .. code-block:: python 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, .. code-block:: python 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, .. code-block:: python load_curve = LoadCurve(id=1, points=CurvePoints(points=["0.0,0.0", "0.1,1.0", "1.0,1.0")]) and add a point, .. code-block:: python load_curve.points.add_point("2.0,2.0") producing: .. code-block:: xml 0.0,0.0 0.1,1.0 1.0,1.0 2.0,2.0 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)