Developer Guide
This guide provides information for developers who want to contribute to Modan2 or understand its architecture.
Project Overview
Modan2 is a Python desktop application for geometric morphometrics built with:
GUI Framework: PyQt5
Database: SQLite with Peewee ORM
Scientific Computing: NumPy, SciPy, Pandas, Statsmodels
3D Graphics: PyOpenGL, Trimesh
Image Processing: Pillow, OpenCV
Project Structure:
Modan2/
├── Modan2.py # Main application entry point
├── MdModel.py # Database models (Peewee ORM)
├── MdUtils.py # Utility functions and constants
├── MdStatistics.py # Statistical analysis functions
├── MdHelpers.py # Helper functions
├── MdConstants.py # Application constants
├── MdLogger.py # Logging utilities
├── MdAppSetup.py # Application initialization
├── MdSplashScreen.py # Splash screen widget
├── ModanController.py # MVC controller
├── ModanDialogs.py # Legacy dialogs (being phased out)
├── ModanComponents.py # Legacy components (being phased out)
├── ModanWidgets.py # Reusable widget utilities
├── build.py # PyInstaller build script
├── migrate.py # Database migration tool
├── requirements.txt # Python dependencies
│
├── dialogs/ # Dialog modules (Phase 2+ refactoring)
│ ├── __init__.py
│ ├── base_dialog.py # Base dialog class
│ ├── analysis_dialog.py # New analysis dialog
│ ├── analysis_result_dialog.py # Analysis results
│ ├── calibration_dialog.py # Image calibration
│ ├── data_exploration_dialog.py # Data visualization & exploration
│ ├── dataset_analysis_dialog.py # Dataset analysis configuration
│ ├── dataset_dialog.py # Dataset create/edit
│ ├── export_dialog.py # Data export (TPS, Morphologika, JSON+ZIP)
│ ├── import_dialog.py # Data import (TPS, NTS, X1Y1, etc.)
│ ├── object_dialog.py # Object/specimen editor with landmarks
│ ├── preferences_dialog.py # Application preferences
│ └── progress_dialog.py # Progress tracking
│
├── components/ # Reusable components (Phase 3+ refactoring)
│ ├── __init__.py
│ ├── formats/ # File format parsers
│ │ ├── tps.py # TPS format support
│ │ ├── nts.py # NTS format support
│ │ ├── x1y1.py # X1Y1 format support
│ │ └── morphologika.py # Morphologika format support
│ ├── viewers/ # 2D/3D visualization widgets
│ │ ├── object_viewer_2d.py # 2D image viewer with landmarks
│ │ └── object_viewer_3d.py # 3D model viewer (OpenGL)
│ └── widgets/ # UI widgets
│ ├── analysis_info.py # Analysis info widget
│ ├── dataset_ops_viewer.py # Dataset operations viewer
│ ├── delegates.py # Table/tree delegates
│ ├── drag_widgets.py # Drag-and-drop widgets
│ ├── overlay_widget.py # Overlay rendering widget
│ ├── pic_button.py # Picture button widget
│ ├── shape_preference.py # Shape visualization preferences
│ └── table_view.py # Custom table view
│
├── OBJFileLoader/ # 3D OBJ file loading
│ ├── objloader.py
│ └── objviewer.py
│
├── tests/ # Automated tests (pytest)
│ ├── conftest.py # Test fixtures
│ ├── test_mdmodel.py # Database model tests
│ ├── test_mdstatistics.py # Statistical analysis tests
│ ├── test_mdutils.py # Utility function tests
│ └── ... # Additional test modules
│
├── devlog/ # Development log (142+ sessions)
├── docs/ # Sphinx documentation
├── icons/ # Application icons
├── migrations/ # Database schema migrations
├── benchmarks/ # Performance benchmarks
├── tools/ # Development tools & scripts
├── config/ # Configuration files
│ ├── pytest.ini
│ └── requirements-dev.txt
└── translations/ # i18n translation files
Architecture
High-Level Overview
Modan2 follows a modified Model-View-Controller (MVC) pattern:
┌──────────────────────────────────────────┐
│ ModanMainWindow (View) │
│ ┌────────────┐ ┌──────────────────┐ │
│ │ TreeView │ │ TableView │ │
│ │ (Datasets) │ │ (Objects) │ │
│ └────────────┘ └──────────────────┘ │
└──────────────┬───────────────────────────┘
│
├─── Signals/Slots ───┐
│ │
┌──────────────▼─────────────┐ ┌────▼──────────────┐
│ ModanController │ │ ModanDialogs │
│ - Dataset operations │ │ - ObjectDialog │
│ - Object CRUD │ │ - AnalysisDialog │
│ - Analysis coordination │ │ - Preferences │
└───────────┬────────────────┘ └───────────────────┘
│
│ Uses
│
┌───────────▼────────────────────────────────┐
│ MdModel (Model - Peewee ORM) │
│ ┌──────────┐ ┌─────────────┐ │
│ │MdDataset │ │ MdObject │ │
│ │MdImage │ │ MdAnalysis │ │
│ └──────────┘ └─────────────┘ │
│ │
│ Database: modan.db (SQLite) │
└────────────────────────────────────────────┘
│
│ Queries
│
┌────────────────▼──────────────────┐
│ MdStatistics │
│ - Procrustes superimposition │
│ - PCA, CVA, MANOVA │
│ - Missing landmark imputation │
└────────────────────────────────────┘
Database Schema
Core Models (defined in MdModel.py
):
MdDataset:
Hierarchical structure (parent/child relationships)
Stores dimension (2D/3D), description
One-to-many relationship with MdObject
MdObject:
Represents a specimen (image or 3D model)
Stores landmark coordinates as JSON string (
landmark_str
)Foreign key to MdDataset
Variable data stored as JSON (
propertyvalue_str
)
MdImage:
Links 2D images to objects
Stores file path, EXIF data, width/height
MdThreeDModel:
Links 3D models to objects
Stores file path, mesh metadata
MdAnalysis:
Stores analysis results (PCA, CVA, MANOVA)
Linked to MdDataset
Results stored as JSON
Relationships:
MdDataset (1) ──< (many) MdObject
MdDataset (1) ──< (many) MdAnalysis
MdObject (1) ──< (0 or 1) MdImage
MdObject (1) ──< (0 or 1) MdThreeDModel
Key Fields:
landmark_str
: Serialized landmark coordinates (format: “x,y\nx,y\n…”)propertyvalue_str
: Serialized variable values (JSON)
Temporary Operations: MdObjectOps
and MdDatasetOps
classes wrap database models for in-memory operations (e.g., Procrustes alignment) without modifying the database.
MVC Pattern in Modan2
Model (MdModel.py
):
Peewee ORM models
Database queries and CRUD operations
Data validation
View (Modan2.py
, dialogs/
, components/
):
ModanMainWindow
(Modan2.py
): Main application window with tree/table viewsDialog classes (
dialogs/*.py
):ObjectDialog
,NewAnalysisDialog
,DataExplorationDialog
, etc.Viewer widgets (
components/viewers/
):ObjectViewer2D
,ObjectViewer3D
Custom widgets (
components/widgets/
): UI components for analysis, data display, etc.Qt signals emitted on user actions
Controller (ModanController.py
):
Connects signals from views to model operations
Coordinates between UI and business logic
Handles analysis workflow
Example Flow:
User clicks "New Dataset" button
→ MainWindow emits signal
→ Controller receives signal
→ Controller opens DatasetDialog
→ User fills form, clicks OK
→ Controller creates MdDataset in database
→ Controller refreshes TreeView
→ TreeView displays new dataset
File Formats
TPS Format (morphometric standard):
LM=5
12.5 34.2
45.6 78.9
...
IMAGE=specimen_001.jpg
ID=1
SCALE=1.0
NTS Format (legacy):
5
12.5 34.2
45.6 78.9
...
CSV Format (custom):
object,lm1_x,lm1_y,lm2_x,lm2_y
spec_001,12.5,34.2,45.6,78.9
Internal Storage (in database):
Landmarks stored as newline-separated “x,y” or “x,y,z” strings
Parsing done by
MdObject.unpack_landmark()
Packing done by
MdObject.pack_landmark()
Development Setup
Prerequisites
Python: 3.11 or newer
Git: For version control
IDE: VSCode, PyCharm, or any Python IDE
Operating System: Windows, macOS, or Linux
Cloning the Repository
git clone https://github.com/jikhanjung/Modan2.git
cd Modan2
Virtual Environment Setup
Linux/macOS:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -r config/requirements-dev.txt
Windows:
python -m venv venv
venv\\Scripts\\activate
pip install -r requirements.txt
pip install -r config/requirements-dev.txt
Running from Source
python Modan2.py
Linux/WSL: If Qt errors occur:
python fix_qt_import.py
Development Dependencies
Installed via config/requirements-dev.txt
:
pytest
: Testing frameworkpytest-cov
: Code coveragepytest-qt
: PyQt5 testing support (future)ruff
: Linting (future)
Testing
Test Framework
Modan2 uses pytest for automated testing.
Test Structure:
tests/
├── conftest.py # Shared fixtures
├── test_mdutils.py # Utility function tests
├── test_mdmodel.py # Database model tests
└── test_statistics.py # Statistical function tests
Running Tests
Run all tests:
pytest
Run specific test file:
pytest tests/test_mdutils.py
Run with coverage:
pytest --cov=. --cov-report=html
# Open htmlcov/index.html
Verbose output:
pytest -v
Writing Tests
Example test (tests/test_mdutils.py
):
import pytest
from MdUtils import normalize_path, is_valid_dimension
def test_normalize_path():
assert normalize_path("C:\\\\Users\\\\test") == "C:/Users/test"
def test_is_valid_dimension():
assert is_valid_dimension(2) == True
assert is_valid_dimension(3) == True
assert is_valid_dimension(4) == False
Using fixtures (tests/conftest.py
):
import pytest
from peewee import SqliteDatabase
from MdModel import MdDataset, MdObject
@pytest.fixture
def test_db():
test_database = SqliteDatabase(':memory:')
with test_database.bind_ctx([MdDataset, MdObject]):
test_database.create_tables([MdDataset, MdObject])
yield test_database
test_database.drop_tables([MdDataset, MdObject])
def test_create_dataset(test_db):
dataset = MdDataset.create(name="Test", dimension=2)
assert dataset.name == "Test"
Code Style Guidelines
General Principles
Follow PEP 8 conventions
Use descriptive variable names
Add docstrings to classes and functions
Keep functions focused (single responsibility)
Naming Conventions
Classes:
PascalCase
(e.g.,ModanController
,ObjectDialog
)Functions/Methods:
snake_case
(e.g.,create_dataset
,pack_landmark
)Constants:
UPPER_SNAKE_CASE
(e.g.,PROGRAM_NAME
,DEFAULT_COLOR
)Private methods:
_leading_underscore
(e.g.,_update_view
)Qt slots:
on_<widget>_<action>
(e.g.,on_btnOK_clicked
)
Docstring Format
Use Google-style docstrings:
def estimate_missing_landmarks(self, obj_index, reference_shape):
"""Estimate missing landmarks using aligned mean shape.
The mean shape is computed from Procrustes-aligned complete specimens,
then transformed to match the scale and position of the current object.
Args:
obj_index (int): Index of object in object_list
reference_shape (MdObjectOps): Reference shape with complete landmarks
Returns:
list: Estimated landmark coordinates, or None if estimation fails
Raises:
ValueError: If obj_index is out of range
"""
# Implementation...
PyQt5 Patterns
Signal/Slot Connections:
# In __init__
self.btnOK.clicked.connect(self.on_btnOK_clicked)
# Slot method
def on_btnOK_clicked(self):
# Handle button click
pass
Wait Cursor for Long Operations:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
def long_operation(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
try:
# Perform operation
result = self.compute_something()
finally:
QApplication.restoreOverrideCursor()
return result
Contributing
Git Workflow
Fork the repository on GitHub
Clone your fork:
git clone https://github.com/YOUR_USERNAME/Modan2.git cd Modan2
Create a feature branch:
git checkout -b feature/my-new-feature
Make changes and commit:
git add . git commit -m "Add new feature: description"
Push to your fork:
git push origin feature/my-new-feature
Open a Pull Request on GitHub
Commit Message Guidelines
Follow conventional commits:
<type>: <subject>
<body (optional)>
<footer (optional)>
Types:
feat
: New featurefix
: Bug fixdocs
: Documentation changesstyle
: Code style (formatting, no logic change)refactor
: Code restructuringtest
: Adding/updating testschore
: Maintenance tasks
Examples:
feat: Add hollow circle visualization for estimated landmarks
fix: Resolve scale mismatch in missing landmark estimation
docs: Update user guide with missing landmark section
test: Add tests for Procrustes with missing data
Pull Request Process
Describe your changes clearly in the PR description
Reference related issues (e.g., “Fixes #42”)
Ensure tests pass: Run
pytest
locally before submittingUpdate documentation if adding new features
Respond to review comments promptly
Squash commits if requested (to keep history clean)
Code Review Checklist
Reviewers will check:
[ ] Code follows style guidelines
[ ] New features have tests
[ ] Documentation updated (if needed)
[ ] No breaking changes (or clearly documented)
[ ] Performance considerations addressed
[ ] No security vulnerabilities introduced
Building Executables
PyInstaller Configuration
Modan2 uses PyInstaller to create standalone executables.
Build script: build.py
Running the build:
python build.py
Output:
dist/Modan2/
- Standalone application folderdist/Modan2.exe
- Executable (Windows)dist/Modan2
- Executable (Linux/macOS)
Platform-Specific Builds
Windows:
python build.py
# Creates dist/Modan2.exe
macOS:
python build.py
# Creates dist/Modan2.app
Linux:
python build.py
# Creates dist/Modan2
Note: Cross-platform builds are not supported - build on the target platform.
InnoSetup Installer (Windows)
For Windows installers:
Install InnoSetup from https://jrsoftware.org/isinfo.php
Build executable:
python build.py
Compile installer:
iscc InnoSetup/Modan2.iss
Output:
Output/Modan2-Setup.exe
Creating Releases
Update version in
MdUtils.py
:PROGRAM_VERSION = "0.1.5"
Update CHANGELOG.md with release notes
Commit changes:
git commit -am "Release v0.1.5" git tag v0.1.5 git push origin main --tags
Build executables for Windows, macOS, Linux
Create GitHub Release:
Go to Releases → Draft a new release
Tag:
v0.1.5
Title:
Modan2 v0.1.5
Description: Copy from CHANGELOG.md
Attach built executables
Publish release
Database Migrations
Modan2 uses peewee-migrate
for schema changes.
Creating a Migration
When you modify database models:
python migrate.py create <migration_name>
Example:
python migrate.py create add_missing_landmark_flag
This creates a new migration file in migrations/
.
Edit the migration file to define changes:
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_column('mdobject', 'has_missing', BooleanField(default=False))
def rollback(migrator, database, fake=False, **kwargs):
migrator.drop_column('mdobject', 'has_missing')
Running Migrations
Apply pending migrations:
python migrate.py
Rollback last migration:
python migrate.py rollback
Advanced Topics
Custom Widgets
Creating custom PyQt5 widgets (see components/widgets/
for examples):
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal
class CustomWidget(QWidget):
# Define custom signals
valueChanged = pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
# Setup UI components
pass
def setValue(self, value):
# Custom logic
self.valueChanged.emit(value)
Examples from codebase:
components/widgets/pic_button.py
: Custom button with image supportcomponents/widgets/drag_widgets.py
: Drag-and-drop list widgetscomponents/viewers/object_viewer_2d.py
: Complex 2D viewer with landmark editingcomponents/viewers/object_viewer_3d.py
: OpenGL-based 3D viewer
Statistical Extensions
Adding new statistical methods (in MdStatistics.py
):
def perform_new_analysis(dataset_ops, options):
"""Perform new statistical analysis.
Args:
dataset_ops (MdDatasetOps): Dataset with aligned shapes
options (dict): Analysis parameters
Returns:
dict: Results including scores, statistics, etc.
"""
# Extract shape data
coords = extract_coordinates(dataset_ops)
# Perform analysis
result = compute_something(coords, **options)
return {
'scores': result.scores,
'statistics': result.stats,
}
Plugin System (Future)
Modan2 may support plugins in future versions:
# plugins/my_plugin.py
class MyPlugin:
name = "My Analysis Plugin"
version = "1.0"
def run(self, dataset):
# Plugin logic
return result
Profiling and Optimization
Profiling with cProfile:
python -m cProfile -o profile.stats Modan2.py
# Analyze with snakeviz
pip install snakeviz
snakeviz profile.stats
Memory profiling:
pip install memory_profiler
python -m memory_profiler Modan2.py
Debugging
Enable detailed logging:
# In Modan2.py
logging.basicConfig(level=logging.DEBUG)
Qt debugging:
export QT_DEBUG_PLUGINS=1
python Modan2.py
Resources
Documentation
Morphometric Analysis
Geometric Morphometrics for Biologists by Zelditch et al.
Morphometrics with R by Claude
Community
GitHub Issues: https://github.com/jikhanjung/Modan2/issues
Discussions: https://github.com/jikhanjung/Modan2/discussions
License
Modan2 is released under the MIT License.
You are free to:
Use commercially
Modify
Distribute
Sublicense
Under the condition that you include the original copyright and license notice.
See the LICENSE file for details.