3D Geometric Simulation Tool

A simulation tool we built at inspire, using 2.5 D methods to simulate

3 years Work at inspire
Python Object-Oriented Programming Software Design C++

This was software project. The aim was to build software that simulates 3D abrasive processes in performant way, while making the code base easily extendable and maintainable. For these reasons, it was decided that we would use Python to program it.

Then the main challenge was presented - 1. Make the simulation fast - 2. Make the software design great

Achievement

What we achieved after we complete this software was: 1. Entire code base is designed in object-oriented principles using Python and C++ 2. 95% Test Coverages reached 3. CI/CD pipeline setup on Github Actions 4. Automatic Software versioning and releasing on Github 5. Runtime performance was 10x faster than the V0 of the code base

Now in the following section I will briefly introduce how this was achieved.

Problem Abstraction

Surface grinding was abstracted in 3 steps as shown in the following graph.

However, doing geometric calculation of two 3D body is very costly, especially given the fact that in the real machining process, there will be 1 million interaction between grains and workpiece per second. To reasonably simulate this in any fashion required a simplification of the problem.

First we simplified the workpiece into collection of 2D planes.

Then we simplified the wheel as a collection of grains, which is of shape tetrahedron and octahedron

Through these simplifications, the problem is simplified from a two rigid body interactions to a 2.5 D simulation problem.

Software design pattern we used

The target of the software redesign was to make it: 1. Easily extendible: meaning very fast to add new features 2. Easily maintainable: debugging, maintaining the code should be fast and when core people leave the team, the knowledge should stay.

The measure of the above criteria is speed. If it's fast to refactor and debug. Then it means we have a good design.

To reach the above two targets, the most important things needed was: * Have a good test coverage * Have a modular design

How we achieved it is by applying the following points:

Architecture Overview

  • Layered Modular Architecture: Clean separation between core framework and the simulation tool box and the specific physical process these process can be applied one.

  • Domain-Driven Design: Clear separation between Geometry, PhysicalObjects, Simulation, and Visualization domains

  • Flexible Composition: Core simulation built around composable Tool, Workpiece, and Kinematics components

Key Design Patterns

Factory Pattern

This is the most fundamental pattern we used throughout the project. It enabled us to make the code base truly object-oriented.

class Shape:
    @classmethod
    def from_circle(cls, radius: float):
        return cls(vertices)

    @classmethod
    def from_rectangle(cls, width: float, height: float):
        return cls(vertices)

    @classmethod
    def from_file(cls, filepath: str):
        return cls(parsed_data)

The benefit of this pattern is:

  • Clean separation between "what kind of object" and "how to build it"
  • Each factory method handles its own validation and geometric calculations
  • Easy to add new construction methods without modifying existing code

Behavioral Patterns: Strategy Pattern via Configuration Classes

This is another pattern that we used widely in the code base.

  class SortingStrategy:
      algorithm: str  # "quicksort", "mergesort", "heapsort"
      threshold: int
      parallel: bool

  class DataProcessor:
      def __init__(self, sorting_config: SortingStrategy):
          self.config = sorting_config

      def process(self, data):
          if self.config.algorithm == "quicksort":
              return self._quicksort(data)
          elif self.config.algorithm == "mergesort":
              return self._mergesort(data)
          # Strategy selected at runtime

The benefits are:

  • Users can optimize performance vs. accuracy trade-offs without code changes
  • New algorithms can be added by extending configuration or providing new callables
  • No need for complex inheritance hierarchies (leveraging Python's functional capabilities)
  • Configuration classes document high-level strategies; callables handle low-level algorithms
  • More Pythonic

Structural Patterns: Composition & Data Organization

We even coded our own library for visualization and plotting of 3D objects. For that we introduced the entire structure for plotting to visual elements, similar to the following:

class VisualElement:
      """Base component"""
      pass

  class Circle(VisualElement):
      """Leaf - single visual element"""
      def __init__(self, x, y, radius):
          self.x, self.y, self.radius = x, y, radius

  class Line(VisualElement):
      """Leaf - single visual element"""
      def __init__(self, start, end):
          self.start, self.end = start, end

  class Drawing(VisualElement):
      """Composite - container of visual elements"""
      def __init__(self, elements: List[VisualElement]):
          self.elements = elements

      def add(self, element: VisualElement):
          self.elements.append(element)

  # Usage: treat individual elements and groups uniformly
  drawing = Drawing([
      Circle(0, 0, 5),
      Circle(10, 10, 3),
      Line((0, 0), (10, 10))
  ])

Dataclasses for Clean Data Structure

For storing the simulation results and make data analysis out of the box, we designed also the way how the simulation data is organised in the following way.

  @dataclass
  class SimulationResult:
      """Pure data container with automatic boilerplate"""
      timestamp: float
      position: Tuple[float, float, float]
      velocity: Tuple[float, float, float]
      energy: float

      # Automatic __init__, __repr__, __eq__ provided

  @dataclass
  class AnalysisMetrics:
      """Container with lazy-computed properties"""
      raw_data: List[float]
      _mean: Optional[float] = None

      @property
      def mean(self):
          if self._mean is None:
              self._mean = np.mean(self.raw_data)
          return self._mean

This enabled us to have:

  • Clear data contracts between simulation components
  • Automatic serialization/deserialization
  • Self-documenting data structures
  • Memory efficient (lazy properties only compute when accessed)
  • Less boilerplate than traditional classes

All these examples given here is to show how we actually made a massive performance improvement possibile by having really great code design.