Sunday, November 15, 2026

PyGame Programming Sample

In the previous post, we checked out PyGame Programming Setup. PyGame is a free and open-source cross platform library for the development of video games using Python. Now that we have the building blocks to implement PyGame APIs we're in excellent position to port an existing video game to Python using PyGame!

Let’s check it out!

Simpsons Trivia
Simpsons Trivia is a retro fan-made quiz game built for the 2018 SMS Power! Coding Competition originally targeting the Sega Master System. The game was ported to the Sega Genesis, Windows, Android and iOS.

Simpsons Trivia
Simpsons Trivia port is Python 3.11.11 compatible. Launch PyCharm | New Project. Enter the following info:

 Location: ~/SimpsonsTrivia
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv lock
  uv sync

Dissection
Before jumping into the full code dissection let's document some of the support folders with their functions:

Static
Includes globally loaded game resource assets and colors definitions. Constants store core configuration with enumerations defining all strongly-typed categories as Python enums and globalize has UI text information.

Objects
Includes game data models to represent configuration, questions and text elements to rendered on-screen.

Inputs
Includes low-level input layer for game engine to convert raw PyGame input into cleaner game input APIs.

CODE
SimpsonsTrivia.py
Main game entry point similar to Program.cs: creates Game -> runs the [in]finite game loop until shutdown:
  SimpsonsTrivia.py
  def main():
      try:
          game = AnGame()
          game.Run()
      except KeyboardInterrupt:
          game.Exit()
      finally:
          game.ShutDown()

AnGame.py
Builds game infrastructure and acts as an intermediary between the game bootstrap and the service registry which delegates game init, load, update and draw game loop methods to actual game implementation code.
  AnGame.py
  class AnGame:
      def __init__(self):
          self.running: bool = True
          build_game()
  
      def Run(self):
          self.Initialize()
          self.LoadContent()
  
          while self.running:
              MyGame.Manager.EventManager.ProcessEvents()
              if MyGame.Manager.EventManager.QuitRequested:
                  self.Exit()
  
              deltaTime: int = MyGame.Manager.ClockManager.Update()
              self.Update(deltaTime)
              self.Draw()

MyGame.py
Delegates game init, load, update + draw game static methods to actual management implementation code.
  MyGame.py
  class MyGame:
      class _ManagerFacade:
          def __getattr__(self, name):
              return ServiceRegistry.get(name)
  
      Manager = _ManagerFacade()

Managers
A Manager is responsible for a particular game system or service. It owns data or functionality that is shared across multiple parts of the game. BaseManager is a foundational class, common toolbox and system helper.

LogManager
Simple wrapper around Python built-in logging system that standardizes how the game prints info messages.
  LogManager.py
  class LogManager:
      @staticmethod
      def Initialize():
          level = logging.INFO
          output: str = "[%(asctime)s] [%(module)s] %(message)s"
          datefmt: str = '%Y-%m-%d %H:%M:%S'
          logging.basicConfig(level=level, format=output, datefmt=datefmt)
  
      @staticmethod
      def Write(msg: str):
          logging.info(msg, stacklevel=2)

RandomManager
Centralised random number generator used by the game to ensure randomness is consistent across system.
  RandomManager.py
  class RandomManager:
      def Initialize(self, seed: int = None):
          if seed is None:
              seed = int(time.time_ns()) & 0xFFFF
          self._random = random.Random(seed)
  
      def Next(self, *args) -> int:
          if len(args) == 1:
              max_val: int = args[0]
              return self._random.randrange(max_val)
          elif len(args) == 2:
              min_val: int = args[0]
              max_val: int = args[1]
              return self._random.randrange(min_val, max_val)

ConfigManager
External game configuration loader and runtime object responsible for all global gaming settings and access.
  ConfigManager.py
  class ConfigManager:
      def LoadContent(self):
          file: Path = self.configRoot / "GlobalConfig.toml"
          text = file.read_text()
          data: ConfigData = tomllib.loads(text)
          data[ScreenType.__name__] = ScreenType[data[ScreenType.__name__]]
          data[OptionType.__name__] = OptionType[data[OptionType.__name__]]
          data[DifficultyType.__name__] = DifficultyType[data[DifficultyType.__name__]]
          self.ConfigData = ConfigData(**data)

EventManager
Event polling game system collecting Pygame events each frame and here is used mainly to track quit game.
  EventManager.py
  class EventManager:
      def __init__(self):
          self.QuitRequested = False
          self.Events = []
  
      def ProcessEvents(self):
          self.Events.clear()
          for event in pygame.event.get():
              self.Events.append(event)
              if event.type == pygame.QUIT:
                  self.QuitRequested = True
              if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                  self.QuitRequested = True

ClockManager
Very small but important system manager responsible for controlling the game timing and inject FPS pacing.
  ClockManager.py
  class ClockManager:
      def Initialize(self):
          self.clock: pygame.time.Clock = pygame.time.Clock()
          self.fps = MyGame.Manager.ConfigManager.ConfigData.FPS
  
      def Update(self) -> int:
          deltaTime: int = self.clock.tick(self.fps)
          return deltaTime

ScreenManager
State machine and game navigation system controlling screen activation and handling all screen transitions.
  ScreenManager.py
  class ScreenManager:
      def Initialize(self):
          self._import_all_screens()
          screen_classes = self._build_screen_map()
          self.screens: dict[ScreenType, BaseScreen] = {
              st: cls()
              for st, cls in screen_classes.items()
          }
          for screen in self.screens.values():
              screen.Initialize()
  
      def Update(self, deltaTime: int):
          if self.currScreen != self.nextScreen:
              self.currScreen = self.nextScreen
              self.screens[self.currScreen].LoadContent()
          tempScreen = self.screens[self.currScreen].Update(deltaTime)
          if tempScreen:
              self.nextScreen = tempScreen
  
      def Draw(self):
          MyGame.Manager.DisplayManager.Clear(self.color)
          self.screens[self.currScreen].Draw()
          MyGame.Manager.DisplayManager.Present(self.color)
  
      def _import_all_screens(self):
          for _, module_name, _ in pkgutil.iter_modules(Screens.__path__):
              if module_name == BaseScreen.__name__:
                  continue
              path_name: str = f"{const.GAME_DIRECTORY}.{ENGINE_SCREENS}.{module_name}"
              importlib.import_module(path_name)
  
      def _build_screen_map(self) -> dict[ScreenType, type[BaseScreen]]:
          screen_map: dict[ScreenType, type[BaseScreen]] = {}
          for screen_type in ScreenType:
              class_name = f"{screen_type.name}{ENGINE_SCREEN}"
              path_name: str = f"{const.GAME_DIRECTORY}.{ENGINE_SCREENS}.{class_name}"
              module = importlib.import_module(path_name)
              cls = getattr(module, class_name)
              screen_map[screen_type] = cls
          return screen_map

Resources
 ContentManager   Asset loading game system responsible for loading all game content into memory
 DisplayManager   Low-level rendering window mgt system controlling how game appears onscreen
 GraphicsManager   Rendering abstraction layer wrapping PyGame drawing calls as usuable functions
 ResolutionManager   Co-ordinate transformation system for game used for all full screen conversions

Miscellaneous
 ImageManager  Character rendering system game system responsible for image content rendering
 SoundManager  Audio game sub-system responsible loading and playing all music + sound effects
 SpriteManager   Central UI sprite renderer and game icons mapper using spritesheet for rendering
 TextManager   Central UI text rendering and layout-from-file system handling all text definitions

Finally, the QuestionManager which is the core gameplay system loading questions from files, manages quiz state, randomizes question and answers, tracks difficulty, and handles all of the question / answer rendering.

Tests
The majority of Managers have corresponding Test to code in isolation. Terminal: execute command pytest:


Screens
Includes BaseScreen to define common interface for all game screens thus ScreenManager lifecycle invokes each screen uniformly and keeps game state transitions clean and decoupled from individual screen logic.
  BaseScreen.py
  class BaseScreen(ABC):
      @abstractmethod
      def Initialize(self) -> None:
          self.Timer: int = None
  
      @abstractmethod
      def LoadContent(self) -> None:
          self.Timer = 0
  
      @abstractmethod
      def Update(self, deltaTime: int) -> ScreenType | None:
          return None
  
      @abstractmethod
      def Draw(self) -> None:
          pass

Engine
In order to fully port C#/.NET paradigms to Python we need helper scripts to register and bootstrap game:
 ServiceRegistry  Simple service locator central hub which connects all game systems together
 Bootstrap  Automatic game management discovery using reflection via dynamic imports

  Bootstrap.py
  def build_game():
      ServiceRegistry.clear()
      base_dir = Path(__file__).resolve().parent.parent
      managers_path = base_dir / const.GAME_DIRECTORY / const.ENGINE_MANAGERS
      for file in managers_path.glob(f"*{const.ENGINE_MANAGER}.py"):
          if file.stem == "__init__":
              continue
          module_name = file.stem
          path_name = f"{const.GAME_DIRECTORY}.{const.ENGINE_MANAGERS}.{module_name}"
          module = importlib.import_module(path_name)
          manager_class = getattr(module, module_name)
          ServiceRegistry.register(
              manager_class.__name__,
              manager_class()
          )
      return ServiceRegistry
Finally, script MyGame.pyi type stub to provide code navigation and autocompletion for coding in PyCharm!

PLAY
In PyCharm hit F5 to run Python code or Terminal enter uv run SimpsonsTrivia.py to play Simpsons Trivia!


CONFIG
Update GlobalConfig.toml configuration settings like set CheatMode or FullScreen to true vary the game play!
  GlobalConfig.toml
  FPS=50
  ScreenType="Splash"
  DifficultyType="Easy"
  SoundEnable=true
  CheatMode=true
  RandomQuestions=true
  RandomAnswers=true
  Fullscreen=true


Summary
To summarize, we have now completed to port Simpsons Trivia video game from C#/.NET to Python using PyGame. This exercise provides great foundation to further explore Farama Foundation Gymnasium project in which Classic Control, ToyText and Box2D environments are all implemented using PyGame. Experience gained here should provide solid grounding for any future work in Reinforcement Learning research space!

Tuesday, September 15, 2026

PyGame Programming Setup

In 2020, we checked out Python Setup Cheat Sheet to create development environment. However, working through code exercises to improve code skills is tedious; we would like to build video games using PyGame!

Let’s check it out!

PyGame
PyGame is a free and open-source cross-platform library for the development of video games using Python. Using Simple DirectMedia Layer to abstract most common functions makes it easier to write these programs.

Tutorials
In order to build video games in Python we first need to learn PyGame APIs. Here are some great resources:
 PyGame Docs  Comprehensive documentation reference for using Pygame to create games
 PyGame Examples  GitHub repository with sample programs that demonstrate how to use Pygame
 PyGame Coders  Beginner-friendly Pygame tutorial that teaches the game development basics
 PyGame Tutorial  Extremely thorough Pygame tutorial used to drive examples in this blog post

Example I - Hello PyGame
All examples listed are Python 3.11.11 compatible. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyGame
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv lock
  uv sync

Create the following file: main.py. Enter the command uv run main.py. Verify Hello PyGame is working OK!
  Example I - Hello PyGame
  import pygame
  pygame.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello PyGame!")
  
  font = pygame.font.Font(None, 48)  # Built-in default font
  text = font.render("Hello PyGame!", True, (255, 255, 255))
  
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      screen.fill((0, 0, 0))
      screen.blit(text, (220, 220))
      pygame.display.flip()
  
  pygame.quit()


Example II - Hello Splash
Repeat previous exercise but render splash screen. Launch PyCharm | New Project. Enter the following info:
 Location: ~/HelloSplash
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11
  uv sync

Create the following file: main.py. Enter the command uv run main.py. Verify splash screen renders OK!
  Example II - Hello Splash
  import pygame
  pygame.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello Splash")
  sms_surface = pygame.Surface((640, 480))
  
  splash = pygame.image.load("StevePro.bmp").convert()
  
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      wide: int = (640 - splash.get_width()) / 2
      high: int = (480 - splash.get_height()) / 2
      sms_surface.blit(splash, dest=(wide, high))
  
      screen.fill((0, 0, 0))
      scaled = pygame.transform.scale(sms_surface, (640, 480))
      screen.blit(scaled, (0, 0))
  
      pygame.display.flip()
  
  pygame.quit()


Example III - Hello Text
Repeat previous exercise but render font text. Launch PyCharm | New Project. Setup project as previously:
  Example III - Hello Text
  import pygame
  pygame.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello Text")
  sms_surface = pygame.Surface((640, 480))
  
  font = pygame.font.Font("emulogic.ttf", 20)
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      sms_surface.fill((0, 0, 0))
      text_surface = font.render("Hello Text!", False, (255, 255, 255))
      sms_surface.blit(text_surface, (0, 0))
      scaled = pygame.transform.scale(sms_surface, (640, 480))
      screen.blit(scaled, (220, 220))
  
      pygame.display.flip()
  
  pygame.quit()

Example IV - Hello Sprites
Repeat previous exercise but render out sprites. Launch PyCharm | New Project. Setup project previously:
  Example IV - Hello Sprites
  import pygame
  pygame.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello Sprites")
  sms_surface = pygame.Surface((320, 240))
  
  spritesheet = pygame.image.load("Sprites.bmp").convert()
  
  def get_sprite(sheet, col, row, width=32, height=32):
      rect = pygame.Rect(col * width, row * height, width, height)
      return sheet.subsurface(rect)
  
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      sms_surface.fill((0, 0, 0))
  
      sprite1 = get_sprite(spritesheet, col=0, row=0)
      sprite2 = get_sprite(spritesheet, col=0, row=1)
      sms_surface.blit(sprite1, (64, 64))
      sms_surface.blit(sprite2, (128, 64))
  
      scaled = pygame.transform.scale(sms_surface, (640, 480))
      screen.blit(scaled, (0, 0))
  
      pygame.display.flip()
  
  pygame.quit()

Example V - Hello Input
Repeat previous exercise but with input detection. Launch PyCharm | New Project. Setup project previously:
  Example V - Hello Input
  import pygame
  pygame.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello Input")
  
  font = pygame.font.Font(None, 48)  # Built-in default font
  text = font.render("Hello Input - Press space...", True, (255, 255, 255))
  
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      screen.fill((0, 0, 0))
      screen.blit(text, (100, 220))
  
      keys = pygame.key.get_pressed()
      if keys[pygame.K_SPACE]:
          space = font.render("Space pressed!!", True, (255, 255, 255))
          screen.blit(space, (200, 260))
  
      pygame.display.flip()
  
  pygame.quit()

Example VI - Hello Sound
Example VII - Hello Music
Example VIII - Hello Audio
Repeat previous exercise but integrate all audio. Launch PyCharm | New Project. Setup project previously:
  Example VIII - Hello Audio
  import pygame
  pygame.init()
  pygame.mixer.init()
  
  screen = pygame.display.set_mode((640, 480))
  pygame.display.set_caption("Hello Audio")
  
  font = pygame.font.Font(None, 48)  # Built-in default font
  sound_01 = pygame.mixer.Sound("right.wav")
  sound_02 = pygame.mixer.Sound("wrong.wav")
  pygame.mixer.music.load("music.mp3")
  
  def render_text(text, color=(255, 255, 255)):
      return font.render(text, False, color)
  
  text1 = render_text("Hello Audio!" )
  text2 = render_text("Press Left - sound #1")
  text3 = render_text("Press Right - sound #2")
  text4 = render_text("Press Up - play music")
  text5 = render_text("Press Down - stop music")
  
  running = True
  while running:
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              running = False
  
      screen.fill((0, 0, 0))
      screen.blit(text1, (220, 140))
      screen.blit(text2, (160, 180))
      screen.blit(text3, (160, 220))
      screen.blit(text4, (160, 260))
      screen.blit(text5, (160, 300))
  
      keys = pygame.key.get_pressed()
      if keys[pygame.K_LEFT]:
          sound_01.play()
      if keys[pygame.K_RIGHT]:
          sound_02.play()
      if keys[pygame.K_UP]:
          pygame.mixer.music.play()
      if keys[pygame.K_DOWN]:
          pygame.mixer.music.stop()
  
      pygame.display.flip()
  
  pygame.quit()

  Example III - Hello Text   Example IV - Hello Sprites
  Example V - Hello Input   Example VIII - Hello Audio

Summary
To summarize, we now have the building blocks to implement PyGame APIs; we are in an excellent position to reuse existing video game content and port video game from another language to Python using PyGame! This will be the topic of the next post.

Tuesday, May 5, 2026

PyTorch Setup Cheat Sheet II

In the previous post, we checked out PyTorch Setup as an open-source machine learning framework widely used for building + training neural networks. Here we installed and configured our nVidia driver and CUDA Toolkit to build some simple PyTorch programs. However, we built PyTorch3d + TorchSparse packages from source each time. Now we'd like to pre-build custom wheels housed on localhost for future examples reuse.
Let's check it out!

Example I
All examples listed are Python 3.10.19 compatible. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyTorch3dWheels
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.10
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.10.19

In the PyCharm Terminal | Enter the following commands for UV to install packages to build custom wheels:
  uv pip install twine
  uv pip install numpy==1.26.4
  uv pip install --index-url https://download.pytorch.org/whl/cu121 \
      "torch==2.2.0+cu121" "torchvision==0.17.0"  "torchaudio==2.2.0"

Create directory to house custom wheels mkdir -p wheelhouse-cu121. Write shell scripts to code wheel logic:
 PyTorch3D chmod +x steveprobuild_pytorch3d_wheel.sh
 TorchSparse chmod +x steveprobuild_torchsparse_wheel.sh

Execute shell scripts to build custom wheels. These will be the custom wheels used locally on next examples:
 COMMAND ARTIFACT
 bash steveprobuild_pytorch3d_wheel.sh stevepropytorch3d-0.7.7-cp310-cp310-linux_x86_64.whl
 bash steveprobuild_torchsparse_wheel.sh steveprotorchsparse-2.0.0b0-cp310-cp310-linux_x86_64.whl

Example II
Repeat previous exercise using locally built wheels. Launch PyCharm | New Project. Enter the following info:
 Location: ~/HelloPyTorch3dWheels
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

Copy custom wheels built from previous example: mkdir wheelhouse-cu121. Copy files into new directory:
  mkdir -p wheelhouse-cu121
  cp -r ../01-Example/wheelhouse-cu121 .

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  export MAX_JOBS=1
  export NVCC_THREADS=1
  uv lock
  uv sync

Create the following file: main.py. Enter the command uv run main.py. Verify PyTorch packages installed!

  Example II
  Hello PyTorch3d
  torch 2.2.0+cu121
  pytorch3d 0.7.7
  cuda 12.1
  cuda True
  torch_geometric 2.7.0
  torch_scatter 2.1.2+pt22cu121
  torch_sparse 0.6.18+pt22cu121
  torch_cluster 1.6.3+pt22cu121
  torch_spline_conv 1.2.2+pt22cu121
  torchsparse 2.0.0b

Example III
Repeat previous exercise but upload custom wheels. Launch PyCharm | New Project. Enter the following info
 Location: ~/HelloPyTorch3dWheels
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

Copy custom wheels built from previous example: mkdir wheelhouse-cu121. Copy files into new directory:
  mkdir -p wheelhouse-cu121
  cp -r ../01-Example/wheelhouse-cu121 .

In the PyCharm Terminal | Enter the following commands to install devpi packages to upload wheels locally:
  uv pip install devpi-server
  uv pip install devpi-client
  uv pip install devpi-web
  uv pip install twine

Launch Terminal #1. Initialize devpi-server on localhost port 3141 and start. Navigate to the homepage URL
  devpi-init					# Launch browser
  devpi-server --host 127.0.0.1 --port 3141	# Navigate http://localhost:3141

Launch Terminal #2. Connect to the devpi-client and login. Create custom cuda-wheels index and activate
  devpi use http://localhost:3141
  devpi login root --password=''
  devpi index -c cuda-wheels bases=root/pypi  
  devpi use root/cuda-wheels

Finally upload 2x custom wheel files to devpi-web available at http://localhost:3141/root/cuda-wheels
  twine upload --repository-url http://localhost:3141/root/cuda-wheels/ wheelhouse-cu121/*  



Example IV
Repeat previous exercise using uploaded wheels. Launch PyCharm | New Project. Enter the following info:
 Location: ~/HelloPyTorch3dWheels
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

In the PyCharm Terminal | Enter the following commands to install devpi packages to consume local wheels:
  uv pip install devpi-server
  uv pip install devpi-client
  uv pip install devpi-web
  uv pip install twine

Launch Terminal #1. Initialize devpi-server on localhost port 3141 and start. Navigate to the homepage URL
  devpi-init					# Launch browser
  devpi-server --host 127.0.0.1 --port 3141	# Navigate http://localhost:3141

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  export MAX_JOBS=1
  export NVCC_THREADS=1
  uv lock
  uv sync

Create the following file: main.py. Enter the command uv run main.py. Verify PyTorch packages installed!

  Example IV
  Hello PyTorch3d
  torch 2.2.0+cu121
  pytorch3d 0.7.7
  cuda 12.1
  cuda True
  torch_geometric 2.7.0
  torch_scatter 2.1.2+pt22cu121
  torch_sparse 0.6.18+pt22cu121
  torch_cluster 1.6.3+pt22cu121
  torch_spline_conv 1.2.2+pt22cu121
  torchsparse 2.0.0b

Example V
Repeat previous exercise but wrap logic as Azure ML endpoint. Launch PyCharm | New Project. Enter info:

 Location: ~/HelloAzureML
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.10
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.10.19

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  export MAX_JOBS=1
  export NVCC_THREADS=1
  uv lock
  uv sync

Create the following directories app and tests. Enter all code for app/scoring.py. Test all code using pytest.
Launch terminals. Enter following commands: build Docker image run Docker container and submit request:

 Terminal #1
  docker build -t azml-gpu-local:latest .
  docker run --gpus all -p 5001:5001 azml-gpu-local:latest

 Terminal #2
  curl --location --request POST 'http://localhost:5001/score' \
    --header 'Content-Type: application/json' \
    --data-raw '{
    "points": [
      [0.0, 0.0, 1.0],
      [0.1, 0.0, 0.99],
      [-0.1, 0.0, 0.99],
      [0.0, 0.1, 0.99],
      [0.0, -0.1, 0.99],
      [0.7, 0.7, 0.0],
      [-0.7, 0.7, 0.0],
      [0.7, -0.7, 0.0],
      [-0.7, -0.7, 0.0],
      [0.0, 0.0, -1.0]
    ]
  }'


  Example V
  {
      "num_vertices": 1524,
      "num_faces": 2892,
      "mean_pixel": 0.9926620125770569,
      "image_shape": [
  		128,
  		128,
  		3
  	],
      "device": "cuda:0"
  }

Cleanup as necessary: stop and remove the docker process esp. if that process is continued to be in use:
  docker stop $(docker ps -q)
  docker rm -f $(docker ps -q)

Summary
To summarize, we have built PyTorch3d and TorchSparse packages from source, pre-built the custom wheels housed on localhost for examples reuse. We are now in an excellent position: drive Deep Learning education!

Saturday, April 4, 2026

PyTorch Setup Cheat Sheet

PyTorch is an open-source machine learning framework widely used for building + training neural networks. While PyTorch provides the core building blocks for tensors, neural networks, and training loops, most real-world machine learning problems involve domain-specific data—images, audio, graphs + also 3D geometry!
Let's check it out!

PyTorch ecosystem extends specialized libraries for different domains [vision, audio, 3D, sparse data] go-to choice for everything from prototyping research ideas to deploying production-grade deep learning systems.

PACKAGES
 torch Core PyTorch library providing tensors, autograd and neural network building blocks
 torchaudio Tools + datasets for audio processing, waveform transformations and speech models
 torchvision Computer Vision utilities for datasets: pretrained models and image transformations
 pytorch3d 3D Deep Learning library for working with meshes, point clouds rendering pipelines
 torchsparse High-performance sparse tensor library for 3D geometry e.g. LiDAR and voxel grids
 Torch-scatter Optimized operations for scattering and aggregating neural networks tensor values
 Torch-sparse Sparse matrix operations for PyTorch + often used in 3D Geometric Deep Learning
 Torch-cluster Clustering and neighborhood learning query operations e.g. kNN and radius graphs
 Torch-spline-conv  Spline-based convolution layers for Geometric Deep Learning on graphs and meshes

CUDA
CUDA [Compute Unified Device Architecture] is a proprietary parallel computing platform and API that allows software to use GPUs for accelerated general-purpose processing in high-performance scientific computing.

Therefore, to leverage PyTorch computations on GPUs one must install nVidia GPU drivers that support your CUDA version, install CUDA Toolkit that includes nvcc compiler with all PyTorch compatible built packages.

Development setup example: Ubuntu 20.04 | Linux kernel 5.15.0-9 | nVidia GPU Driver v535 | CUDA 12.1

INSTALLATION
Complete nVidia CUDA Toolkit installation guide found here but can be summarized in the following steps:
  uname -r				# 5.15.0-139-generic

Step #1 - Remove partial CUDA installs
  sudo apt remove -y cuda* nvidia-cuda-toolkit
  sudo apt autoremove -y
  sudo rm -rf /usr/local/cuda*

Step #2 - Install nVidia Driver 535
  sudo apt update
  sudo apt install -y nvidia-driver-535
  sudo reboot

Step #3 - Verify nVidia Driver and CUDA version - nvidia-smi


Step #4 - Install CUDA 12.1 Toolkit
  sudo mkdir -p /etc/apt/keyrings
  
  curl -fsSL https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/3bf863cc.pub \
   | sudo gpg --dearmor -o /etc/apt/keyrings/cuda-archive-keyring.gpg
  
  echo "deb [signed-by=/etc/apt/keyrings/cuda-archive-keyring.gpg] \
  https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/ /" \
  | sudo tee /etc/apt/sources.list.d/cuda.list
  
  sudo apt update

Step #5 - Configure Environment Variables - source ~/.bashrc
  export CUB_HOME=~/cub
  export CUDA_HOME=/usr/local/cuda-12.1
  export PATH=$CUDA_HOME/bin:$PATH
  export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH

Step #6 - Verify CUDA 12.1 Toolkit - nvcc --version
 Kernel 5.15.0-139
 Driver nvidia-driver-535
 CUDA 12.1 /usr/local/cuda-12.1
 CUDA toolkit /usr/local/cuda-12.1/bin/nvcc

Example I
All examples listed are Python 3.10.19 compatible. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyTorch3d
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.10
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.10.19

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  export MAX_JOBS=1
  export NVCC_THREADS=1
  uv lock
  uv sync

Create the following file: main.py. Enter the command uv run main.py. Verify PyTorch packages installed!

  Example I
  Hello PyTorch3d
  torch 2.2.0+cu121
  pytorch3d 0.7.7
  cuda 12.1
  cuda True
  torch_geometric 2.7.0
  torch_scatter 2.1.2+pt22cu121
  torch_sparse 0.6.18+pt22cu121
  torch_cluster 1.6.3+pt22cu121
  torch_spline_conv 1.2.2+pt22cu121
  torchsparse 2.0.0b

Example II
Repeat previous exercise but wrap logic as Azure ML endpoint. Launch PyCharm | New Project. Enter info:

 Location: ~/HelloAzureML
 Interpreter type:  uv
 Python version: 3.10
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.10
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.10.19

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  export MAX_JOBS=1
  export NVCC_THREADS=1
  uv lock
  uv sync

Create the following files: app/scoring.py and .env. Enter the command which azmlinfsrv. Edit configurations Copy and paste the azmlinfsrv path as the script value. Enter the following info to launch Azure ML endpoint:

 script value ~/HelloAzureML/.venv/bin/azmlinfsrv
 Parameters --entry app/scoring.py
 Working directory ~/HelloAzureML
 Path to .env files ~/HelloAzureML/.env

Alternatively, launch terminals. Enter following commands to launch Azure ML endpoint and submit request:

 Terminal #1
  set -a; source .env; set +a
  ~/.venv/bin/azmlinfsrv --entry app/scoring.py

 Terminal #2
  curl --location --request POST 'http://localhost:5001/score' \
    --header 'Content-Type: application/json' \
    --data-raw '{}'

  Example II
  {
      "message": "Hello PyTorch3d",
      "torch_version": "2.2.0+cu121",
      "pytorch3d_version": "0.7.7",
      "cuda_version": "12.1",
      "cuda_available": true,
      "torch_geometric_version": "2.7.0",
      "torch_scatter_version": "2.1.2+pt22cu121",
      "torch_sparse_version": "0.6.18+pt22cu121",
      "torch_cluster_version": "1.6.3+pt22cu121",
      "torch_spline_conv_version": "1.2.2+pt22cu121",
      "torchsparse_version": "2.0.0b"
  }

Cleanup as necessary: find and kill processes using TCP port 5001 esp. if that port is continued to be in use:
  sudo fuser -k 5001/tcp		# Linux
  sudo lsof -ti tcp:5001		# MacOS

Summary
To summarize, we have installed and configured our nVidia driver and CUDA Toolkit and built some simple PyTorch programs. However, we have built the PyTorch3d + TorchSparse packages from source each time. Ideally, we would like to pre-build custom wheels once and house on localhost for future examples reuse!
This will be the topic of the next post.

Tuesday, March 17, 2026

PyBind Setup Cheat Sheet

In 2024, we checked out OpenAI Retro Cheat Sheet as an open-source project that provides an interface to interact with various retro video games for purpose of Reinforcement Learning research. This project: using C/C++ for high-performance but leverages pybind11 to create bindings for Python client code consumption.

Let's check it out!

pybind11
A lightweight header-only library that can be used to integrate C++ with Python to create bindings exposing C++ functions to Python. Client code written in Python can be consumed to invoke the underlying C++ code.

Installation
All examples here are executed on Ubuntu Linux. Therefore install pybind11 globally to begin the examples:
 sudo apt-get update
 sudo apt-get install pybind11-dev
 sudo apt install build-essential g++

Example I
Create an example that exposes C++ function to Python with pybind11. Launch terminal | Enter commands:
  mkdir -p ~/HelloPyBind
  cd HelloPyBind
  python  -m venv .venv
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.8.10
  pip install pybind11
  pip install --upgrade pip


Create the following files: example.cpp, setup.py, test.py. Enter the following C++ and Python source code:
  example.cpp
  #include <pybind11/pybind11.h>
  
  int add(int x, int y)
  {
      return x + y;
  }
  
  PYBIND11_MODULE(example, m)
  {
      // optional module docstring
      m.doc() = "pybind11 example plugin";
      m.def("add", &add, "A function which adds two numbers");
  }

  setup.py
  from setuptools import setup, Extension
  import pybind11
  
  ext_modules = [
      Extension(
          "example",
          ["example.cpp"],
          include_dirs=[pybind11.get_include()],
          language="c++"
      ),
  ]
  
  setup(
      name="example",
      version="0.1",
      ext_modules=ext_modules,
  )

  test.py
  import example
  
  result = example.add(1, 2)
  print(f"1 + 2 = {result}")

Build C++ code using setup.py build inplace. Finally execute python test.py for Python to execute C++ code!
  python setup.py build_ext --inplace		# example.cpython-38-x86_64-linux-gnu.so
  python test.py				# OUTPUT	1 + 2 = 3


Example II
Repeat previous exercise but prefer PyCharm IDE. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv add setuptools
  uv lock
  uv sync

Create the following files: example.cpp, setup.py, test.py. Enter C++ and Python code similar to Example I. Build C++ code using setup.py build install. Finally execute uv run test.py for Python to execute C++ code!
  uv run setup.py build		
  uv run setup.py install		# example.cpython-38-x86_64-linux-gnu.so
  uv run test.py			# OUTPUT	3 + 5 = 8	9 - 5 = 4


Example III
Repeat previous exercise but prefer CMake to build C++ code via CMakeLists.txt. Create PyCharm Project:
 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv sync

Create the following files: example.cpp, CMakeLists.txt, test.py. Enter code similar to Example II but update:
  CMakeLists.txt
  cmake_minimum_required(VERSION 3.16)
  project(example)
  
  # Find the Python 3.11-specific pybind11 CONFIG from pip
  execute_process(
          COMMAND ${Python3_EXECUTABLE} -m pybind11 --cmakedir
          OUTPUT_VARIABLE pybind11_DIR
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  find_package(pybind11 REQUIRED CONFIG)
  pybind11_add_module(example example.cpp)

Build C++ code using cmake and make. In the PyCharm Terminal | Enter the following commands to build:
  mkdir -p build
  cd build
  cmake -DPython3_EXECUTABLE=$(which python) ..
  make -j$(grep -c ^processor /proc/cpuinfo)

Enter commands to copy library to be used. Finally execute uv run test.py for Python to execute C++ code!
  python -c "import sysconfig, shutil, glob;		\		
  dst = sysconfig.get_paths()['platlib'];		\
  so = glob.glob('*.so')[0];				\
  shutil.copy2(so, dst)"
  cd ..
  uv run test.py			# OUTPUT	Hello, World!

IMPORTANT
The first 3x examples worked but tightly coupled Python and C++ without being able to debug separately!

Example IV
Repeat previous exercise but prefer to modify the project layout to separate top level Python and C++ code:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── api/
  │   │   │   ├── my_api.h
  │   │   │   └── my_api.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   └── test_api.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  │
  └── python/
      ├── .venv/
      │   └── lib/
      │       └── python3.11/
      │           └── site-packages/
      │               └── my_api_py.cpython-311-x86_64-linux-gnu.so
      ├── test.py
      ├── pyproject.toml
      └── README.md

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code.
 Location: ~/HelloPyBind/python
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion information:
 C++ C++ Executable
 Location: ~/HelloPyBind/cpp
 Language standard: C++17

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory

Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

IMPORTANT
CMakeLists.txt files are configured to copy shared object SO file into the Python .venv virtual environment

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py				# OUTPUT	1 + 2 = 3


Example V
Repeat previous exercise but prefer more complexity to build C++ code with classes as consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py		# OUTPUT	
  # Guitar: 'Fender' [6-string] = $1500.0
  # Guitar: 'Ibanez' [7-string] = $1200.0
  # Guitar: 'Gibson' [6-string] = $2400.0


Example VI
Repeat previous exercise but prefer more complexity to build C++ code with templates consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  # OUTPUT	
  # Container[0] = 0.0
  # Container[1] = 1.0
  # Container[2] = 2.0
  # Container[3] = 3.0
  # Container[4] = 4.0
  # OUTPUT	
  # Container[5] = 5.0
  # Container[6] = 6.0
  # Container[7] = 7.0
  # Container[8] = 8.0
  # Container[9] = 9.0


Example VII
Repeat previous exercise but prefer Visual Studio 2022 on Windows to build an increasing C++ code base:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── core/			  # Core API implementation
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── math/			  # Math-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── mesh/			  # Mesh-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   ├── test_matrix.cpp
  │   │   ├── test_vector.cpp
  │   │   ├── test_mesh.cpp
  │   │   ├── test_mesh_algorithms.cpp
  │   │   └── test_mesh_processor.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  └── python/

Launch Visual Studio 2022 | Continue without code. File | Open | CMake... Navigate to cpp/CMakeLists.txt. Build menu | Build All. Finally, choose Test menu | Test Explorer. Run All Tests in View or choose to Debug:


Summary
To summarize, we have demonstrated various PyBind examples in which C++ library code is consumed by a single Python API exclusively. However, in future there may be instances in which the C++ library may need to be consumed by multiple languages. In this case ctypes.cdll.LoadLibrary() may be better that PyBind!