Skip to content
Public template

About

Teaching template that helps engineers solve real problems in Python instead of proprietary technical programming languages. 11 documented engineering domain classes, a unit-aware Beam dataclass with Pint, solve_ivp/curve_fit examples, a tkinter console GUI, and a pytest suite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

First Principles Code

A template Python project that teaches engineers to solve real engineering problems in Python — and shows that the open-source scientific stack (numpy, scipy, and friends) can replace expensive proprietary tools.

📖 Documentation: https://kovirlabs.github.io/first-principles-code/, with guides and an API reference generated from the docstrings.

You get:

  • main.py — a worked example that calculates displacement from acceleration and velocity three different ways (closed-form, numerical integration of constant acceleration, and numerical integration of a time-varying acceleration).
  • fpc/ — an installable package with one well-documented class per engineering domain (dynamics, statics, thermo, fluids, materials, gears, electrical, mechatronics, modal/vibration, controls), a set of radiology / medical-imaging classes, Design (root finding and optimization), Properties (cited reference tables with interpolation), and a Utility class for files, tables, and dialogs.
  • examples/ — one script per realistic problem (pump sizing, beam selection, PID tuning, ...), each going from question to answer.
  • templates/engineering_script.py — a commented skeleton to copy for your own calculation.
  • notebooks/ — Jupyter notebooks that walk through the library.
  • Guides: from calculation to tool (script -> CLI -> GUI -> web app -> installed command) and units: floats or Pint?.
  • PACKAGES.md — which Python packages to use, by engineering field.

Every method uses keyword-only arguments and carries a docstring, so the code itself doubles as a reference you can read and learn from.


Quick start

# 1. Install dependencies into a project virtual environment
uv sync

# 2. Run the displacement demo (prints results and saves displacement.png)
uv run main.py

# 3. Run the test suite
uv run pytest

Expected output:

Displacement of an object after 3.0 s
================================================
  x0 = 0.0 m,  v0 = 5.0 m/s,  a = g = 9.81 m/s^2

  1. Analytical formula        :    59.1450 m
  2. scipy (constant a)        :    59.1450 m
  4. Dynamics library class    :    59.1450 m
     -> numerical vs exact err : 2.13e-14 m

  3. scipy (a = g + 2*sin(t))  :    64.8628 m
     (no simple closed form - this is why we integrate numerically)

A short tutorial on uv

uv is a fast, all-in-one Python package and project manager (a modern replacement for pip + venv + pip-tools). It reads pyproject.toml, manages a .venv for you, and pins exact versions in a uv.lock file so your environment is reproducible.

Install uv

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

The commands you'll actually use

Command What it does
uv sync Create/update .venv to match pyproject.toml + uv.lock
uv run main.py Run a script inside the project environment (auto-syncs first)
uv run python Open a Python REPL with the project's packages available
uv add scipy Add a dependency (updates pyproject.toml and the lockfile)
uv add --dev pytest Add a development-only dependency (e.g. a test runner)
uv remove scipy Remove a dependency
uv lock Re-resolve and rewrite uv.lock without installing
uv python install 3.14 Install a specific Python version

The golden rule: prefix anything that needs your packages with uv run. You never have to manually activate a virtual environment.

Typical workflow

git clone <this-repo>
cd first-principles-code
uv sync                 # one-time setup
uv run main.py          # run the demo
uv add matplotlib       # pull in a new library when you need it
uv run python           # experiment interactively

Using the library

Import a class, create an instance, and call its methods with named arguments (this is the project's house style — calls read like sentences):

from fpc.dynamics import Dynamics
from fpc.thermo import Thermo

dyn = Dynamics()
distance = dyn.displacement_constant_acceleration(
    initial_position=0.0,
    initial_velocity=5.0,
    acceleration=9.81,
    time=3.0,
)  # -> 59.145 m

thermo = Thermo()
efficiency = thermo.carnot_efficiency(hot_temperature=800, cold_temperature=300)
# -> 0.625  (the best a heat engine between these temperatures could ever do)

You can also import everything at once:

from fpc import Dynamics, Statics, Fluids, Controls

Creating and calling objects from lists and dictionaries

A common pattern: build several objects, store them in a container, then loop over them. This is how you scale from one calculation to a whole study.

from fpc.dynamics import Dynamics

dyn = Dynamics()

# --- A list of scenarios (each scenario is a dict of keyword arguments) ---
launches = [
    {"speed": 20, "angle_deg": 30},
    {"speed": 20, "angle_deg": 45},
    {"speed": 20, "angle_deg": 60},
]

# The ** operator unpacks a dict straight into keyword arguments.
for case in launches:
    distance = dyn.projectile_range(**case)
    print(f"{case['angle_deg']:>3}deg -> {distance:6.2f} m")

# --- A dictionary that maps a name to a configured object/result ---
materials = {
    "steel": 200e9,  # Young's modulus [Pa]
    "aluminum": 69e9,
    "titanium": 114e9,
}
for name, modulus in materials.items():
    print(f"{name:>9}: E = {modulus / 1e9:.0f} GPa")

Conditional flow control and loops

from fpc.fluids import Fluids

fluids = Fluids()
re = fluids.reynolds_number(density=1000, velocity=2, length=0.05, viscosity=1e-3)

# if / elif / else: classify the flow regime
if re < 2300:
    regime = "laminar"
elif re < 4000:
    regime = "transitional"
else:
    regime = "turbulent"
print(f"Re = {re:.0f} -> {regime}")

# while loop: increase velocity until the flow turns turbulent
velocity = 0.01
while fluids.reynolds_number(density=1000, velocity=velocity, length=0.05, viscosity=1e-3) < 4000:
    velocity += 0.01
print(f"Flow becomes turbulent at about {velocity:.2f} m/s")

Lambda functions

A lambda is a small, unnamed function written inline — handy for quick math and for telling other functions how to behave (sorting, mapping, filtering).

# 1. A throwaway formula you can call like any function
kinetic_energy = lambda mass, velocity: 0.5 * mass * velocity**2
print(kinetic_energy(2.0, 3.0))  # -> 9.0

# 2. As a sort key: order parts by stress, highest first
parts = [("bracket", 120e6), ("pin", 240e6), ("plate", 80e6)]
parts.sort(key=lambda part: part[1], reverse=True)

# 3. With map/filter: convert a list of Celsius temps to Kelvin
celsius = [0, 25, 100]
kelvin = list(map(lambda c: c + 273.15, celsius))  # [273.15, 298.15, 373.15]

Modelling things with units: the Beam dataclass

fpc/beam.py shows how to model a real object as a @dataclass whose fields carry physical units (via Pint). A Beam knows its own geometry and computes its structural behaviour — and the units make the math self-checking.

from fpc.units import Q_  # Q_(value, "unit") builds a quantity
from fpc.beam import Beam

beam = Beam(
    length=Q_(2.0, "m"),
    width=Q_(50, "mm"),
    height=Q_(100, "mm"),
    modulus=Q_(200, "GPa"),  # defaults to structural steel if omitted
)

beam.moment_of_inertia.to("mm**4")  # 4.17e6 mm^4
beam.mass.to("kg")  # 78.5 kg
beam.max_deflection_point_load(load=Q_(5, "kN")).to("mm")  # 1.0 mm
beam.max_bending_stress_point_load(load=Q_(5, "kN")).to("MPa")  # 30 MPa
beam.euler_buckling_load().to("kN")  # 514 kN

Why units are worth it:

Q_(1, "m") + Q_(1, "s")          # raises DimensionalityError - caught, not silent
Beam(length=Q_(2, "s"), ...)     # raises ValueError - a time is not a length

All quantities must come from the one shared registry in fpc/units.py (Pint cannot combine quantities from different registries).

Simulation & data fitting (SciPy)

Two SciPy workhorses show up where formulas run out:

  • solve_ivp integrates systems that have no closed-form solution. Dynamics.simulate_projectile_drag(...) flies a projectile with air drag (and stops itself at ground impact using a solver event); Modal.simulate_free_vibration(...) rings down a spring-mass-damper.
  • curve_fit does the inverse — recover parameters from data. Modal.fit_damped_response(time, displacement) fits a decaying sinusoid to measured vibration and returns the natural frequency and damping ratio.
from fpc.modal import Modal

modal = Modal()

# Simulate a known oscillator...
t, x = modal.simulate_free_vibration(
    mass=2.0,
    stiffness=800.0,
    damping=8.0,
    initial_displacement=0.01,
    duration=5.0,
)
# ...then fit the response and recover the inputs (~3.18 Hz, zeta ~0.10).
modal.fit_damped_response(time=t, displacement=x, guess_frequency_hz=3.0)

That simulate-then-fit round-trip is checked in the test suite: the fit must return the parameters the simulation started with.

Design, reference data & tables

Analysis asks "how much does this beam deflect?"; design asks "what beam deflects no more than 5 mm?". These pieces answer design questions:

from fpc import Design, Fluids, Properties, Utility

# Root finding (scipy.optimize.brentq): solve equations with no algebraic answer.
Fluids().colebrook_friction_factor(reynolds=1e5, relative_roughness=1e-4)  # 0.0185
Design().rectangular_beam_height_for_deflection(
    load=5000,
    length=2.0,
    modulus=200e9,
    width=0.05,
    max_deflection=0.001,
)  # 0.100 m

# Constrained optimization (scipy.optimize.minimize, SLSQP): lightest beam
# that satisfies stress, deflection, and aspect-ratio limits.
Design().lightest_rectangular_beam(
    load=5000,
    length=2.0,
    modulus=200e9,
    density=7850,
    allowable_stress=150e6,
    max_deflection=0.005,
)  # {'width': 0.0199, 'height': 0.0795, 'mass': 24.8, 'active': ['deflection', 'aspect_ratio'], ...}

# Reference tables (pandas) with interpolation; data and sources in fpc/data/.
props = Properties()
props.water_liquid(temperature_c=37, property="dynamic_viscosity_pa_s")  # linear
props.water_saturation(temperature_c=93, property="pressure_kpa", method="cubic")
props.lightest_w_shape(required_sx_in3=30)["designation"]  # 'W16x26'

# Your own CSV/Excel files
frame = Utility().read_table(file_path="test_log.xlsx")

examples/sensor_log_analysis.py shows the full pandas workflow (clean, group, resample, filter, export to Excel), and examples/vectorization.py compares Python loops with NumPy arrays.

Units: methods that take plain numbers expect SI units (the docstring says so for every parameter); use Pint at the boundaries where units are mixed. See docs/units.md.

Radiology & medical imaging

Six classes cover the physics and image handling a radiology student (radiologic technologist, resident, or medical physics student) works with every day. Run the guided tour with uv run examples/radiology_demo.py. It prints one worked example per modality and saves examples/output/radiology.png, which shows a real CT slice in abdomen, lung, and bone windows.

Class Module What it covers
Radiation fpc/radiation.py Inverse square law, attenuation (mu, HVL, TVL, shielding), photon energy, radioactive and effective half-life, equivalent/effective dose with ICRP 103 weighting factors
Radiography fpc/radiography.py mAs, 15% rule, exposure-maintenance (SID) formula, grid conversion, heat units, magnification, geometric unsharpness, detector Nyquist limit, deviation index
CT fpc/ct.py Hounsfield units, DICOM rescale, window width/level with presets, rough HU tissue lookup, DLP and effective dose estimate
MRI fpc/mri.py Larmor frequency, T1 recovery, T2 decay, spin-echo signal, T1/T2/PD weighting from TR/TE
Ultrasound fpc/ultrasound.py Wavelength, echo depth, acoustic impedance and reflection, attenuation, axial resolution, PRF depth limit, Doppler shift and velocity
Dicom fpc/dicom.py Read DICOM with pydicom, summarize technical tags, pixels in HU, pixel spacing, basic de-identification, PNG export
from pydicom.data import get_testdata_file  # sample images, no patient data
from fpc import CT, Dicom, Radiography, Radiation

radiation = Radiation()
radiation.effective_dose(organ_equivalent_doses={"thyroid": 10.0})  # -> 0.4 mSv

Radiography().exposure_maintenance_mas(original_mas=10, original_sid=100, new_sid=180)  # 32.4

dicom, ct = Dicom(), CT()
ds = dicom.read(path=get_testdata_file("CT_small.dcm"))
hu = dicom.pixel_array_in_units(dataset=ds)  # stored values -> Hounsfield units
lung = ct.apply_window(hounsfield=hu, preset="lung")  # whole slice windowed in one call
safe = dicom.anonymize(dataset=ds)  # blank PHI before sharing

Teaching use only. These are textbook calculations for learning. They are not validated for clinical decisions, patient dosimetry, or shielding design. Use your department's protocols and a qualified medical physicist for those. Dicom.anonymize removes common direct identifiers only, which is not full de-identification under DICOM PS3.15 or HIPAA.

Python packages for medical imaging (SimpleITK, nibabel, MONAI, ...) are listed in PACKAGES.md.

Learning resources: Radiopaedia (cases and physics articles), pydicom user guide, NIST XCOM attenuation data, ICRP Publication 103, The Cancer Imaging Archive (free de-identified datasets).

Tests

The library methods are pure functions with known textbook answers, which makes them ideal to test. The suite in tests/ doubles as a pytest tutorial — it demonstrates pytest.approx (comparing floats), @pytest.mark.parametrize (table-driven tests), and the tmp_path fixture (temporary files).

uv run pytest                          # run everything
uv run pytest tests/test_dynamics.py   # run one file
uv run pytest -k projectile            # run tests whose name matches "projectile"

Plotting

main.py uses matplotlib to plot the acceleration -> velocity -> position cascade and saves it to displacement.png. The pattern — compute arrays with NumPy/SciPy, then visualize with matplotlib — is the everyday replacement for MATLAB's figures. See plot_motion() in main.py for a worked example.

Python packages for science, engineering & data work

The ten to know first. PACKAGES.md has the full list, organized by engineering field (structural, thermal/fluids, electrical, controls, CAD, optimization, medical imaging, tool building, performance).

Package Use it for
NumPy Fast N-dimensional arrays and vectorised math — the foundation of everything below
SciPy Integration, optimization, root finding, interpolation, signal processing, statistics
Matplotlib Plotting and figures (the MATLAB-style pyplot interface)
pandas Tabular data, time series, CSV/Excel I/O
SymPy Symbolic math / computer algebra (a free Maple/Mathematica)
Pint Physical units and automatic unit conversion
Jupyter Notebooks that mix code, math, plots, and prose
CoolProp Thermophysical fluid properties for thermo/HVAC work
python-control Control-systems design (a free Control System Toolbox)
pytest Testing framework to verify your calculations stay correct

Add any of them with uv add <package>.


Learning resources

Python fundamentals

Scientific & numerical Python

Engineering & for MATLAB users

Tooling


Use this library in your own project

fpc is an installable package, so your own scripts do not have to live in this repository.

# In your own uv project
uv add git+https://github.com/kovirlabs/first-principles-code

# Or with pip, from a local clone (editable: your edits take effect immediately)
pip install -e path/to/first-principles-code
from fpc import Fluids, Properties

Installing also gives you the fpc command (fpc pipe-flow --flow-l-s 12 --diameter-mm 102.3 --length-m 120; see docs/building-tools.md). To pin a version, add @<tag-or-commit> to the git URL.

Developer tooling

Command What it does
uv sync --all-groups Install everything: runtime, dev tools, notebooks, docs, apps
uv run pytest Tests, including running every example and notebook
uv run ruff check . / uv run ruff format . Lint / auto-format
uv run mypy Type-check the fpc package
uv run pre-commit install Run ruff and pytest automatically before each commit
uv run --group docs mkdocs serve Preview the API docs site, generated from the docstrings

CI runs all of these on every pull request; the docs site is published to GitHub Pages from main.

Project layout

first-principles-code/
├── main.py              # displacement demo (start here)
├── gui.py               # MATLAB-style console GUI (uv run gui.py)
├── fpc/                 # the library: one class per engineering domain
│   ├── __init__.py      # re-exports every class
│   ├── units.py         # shared Pint unit registry (ureg, Q_)
│   ├── beam.py          # unit-aware Beam dataclass
│   ├── design.py        # root finding & constrained optimization
│   ├── properties.py    # reference-table lookups with interpolation
│   ├── utility.py       # JSON/CSV/Excel/file-picker helpers
│   ├── cli.py           # the `fpc` terminal command
│   ├── data/            # cited reference tables (CSV) + sources
│   ├── dynamics.py, statics.py, modal.py, thermo.py, fluids.py,
│   ├── material.py, gear.py, electrical.py, mechatronics.py, controls.py
│   └── radiation.py, radiography.py, ct.py, mri.py, ultrasound.py, dicom.py
├── examples/            # worked problems, one script each (+ tools/ ladder)
├── templates/           # copy-and-adapt starting script
├── notebooks/           # Jupyter notebooks
├── docs/                # guides + API docs site sources (mkdocs.yml)
├── tests/               # pytest suite (uv run pytest)
├── pyproject.toml       # metadata, dependencies, tool config
├── PACKAGES.md          # Python packages by engineering field
├── SPEC.md              # original project design spec
├── ROADMAP.md           # improvement backlog
├── CONTRIBUTOR.md       # how to contribute
├── LICENSE              # Apache 2.0
└── README.md            # you are here

The console GUI

gui.py is a small tkinter app that mimics the technical computing command window interface. Launch it with:

uv run gui.py
  • Type a Python expression in the command box and press Enter (or Run). The result prints to the Session log and is recorded in the Workspace table — just like MATLAB's ans/workspace.
  • The interpreter comes preloaded with math, numpy as np, and a ready instance of every library class (dyn, statics, thermo, fluids, design, props, …), so you can compute immediately: dyn.projectile_range(speed=20, angle_deg=45).
  • Open Script loads a .py file into the command box; Run Script runs the whole box at once.
  • Export CSV / Export JSON save every recorded result to disk (via fpc.Utility).

The GUI needs a graphical display. The evaluation engine (the Session class) is separated from the Tk widgets, so it can be imported and scripted without a window.


Contributing

Contributions are welcome — this is a teaching template, so clear, well-explained code is the product. See CONTRIBUTOR.md for the development setup, coding conventions, and how to submit changes.

Quick version of the conventions:

  • Pass arguments by keyword (acceleration=9.81, not 9.81). Methods use keyword-only signatures (def method(self, *, ...)) to enforce this.
  • Every class and method gets a docstring with parameter units.
  • Comment the why of non-obvious math, not just the what.
  • Keep one engineering domain per file, one class per file.
  • Every new method/class needs a test.

License

Licensed under the Apache License 2.0. © 2026 Evan Gress.

About

Teaching template that helps engineers solve real problems in Python instead of proprietary technical programming languages. 11 documented engineering domain classes, a unit-aware Beam dataclass with Pint, solve_ivp/curve_fit examples, a tkinter console GUI, and a pytest suite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages