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 aUtilityclass 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.
# 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 pytestExpected 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)
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.
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"| 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.
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 interactivelyImport 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, ControlsA 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")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")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]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 kNWhy 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 lengthAll quantities must come from the one shared registry in
fpc/units.py (Pint cannot combine quantities from different registries).
Two SciPy workhorses show up where formulas run out:
solve_ivpintegrates 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_fitdoes 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.
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.
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 sharingTeaching 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.anonymizeremoves 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).
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"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.
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>.
Python fundamentals
- The Official Python Tutorial
- Real Python — practical, well-edited tutorials
- Automate the Boring Stuff with Python (free online)
Scientific & numerical Python
- SciPy Lecture Notes — a full course in the scientific stack
- NumPy: the absolute basics for beginners
- Python Numerical Methods (UC Berkeley) — free online textbook
- Nature: A guide to NumPy
Engineering & for MATLAB users
- NumPy for MATLAB users — translation cheat sheet
- python-control Library docs
- Engineering with Python
Tooling
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-codefrom fpc import Fluids, PropertiesInstalling 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.
| 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.
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
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
.pyfile 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
Sessionclass) is separated from the Tk widgets, so it can be imported and scripted without a window.
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, not9.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.
Licensed under the Apache License 2.0. © 2026 Evan Gress.