Contributing#

Thank you for your interest in contributing to EngiAI! This document provides guidelines for contributing to the project.

Getting Started#

  1. Fork the repository on GitHub

  2. Clone your fork:

    git clone https://github.com/YOUR_USERNAME/engiai.git
    cd EngiAI
    
  3. Create environment and install dependencies:

    conda create -n engiai python=3.13 -y
    conda activate engiai
    pip install -e ".[dev,test]"
    

Development Workflow#

1. Create a Feature Branch#

git checkout -b feature/your-feature-name

Use descriptive branch names:

  • feature/ for new features

  • fix/ for bug fixes

  • docs/ for documentation

  • test/ for test improvements

2. Make Your Changes#

  • Write clean, readable code

  • Follow the existing code style

  • Add docstrings to new functions and classes

  • Update documentation as needed

3. Write Tests#

Add tests for your changes:

# Create test file
touch tests/test_your_feature.py

# Run tests
make test

# Run specific test
pytest tests/test_your_feature.py

4. Check Code Quality#

# Format code
make format

# Lint code — ruff check, ruff format --check, and mypy
make lint

# The full pre-push gate: everything make lint runs, plus the whole test suite
make check-all

make lint covers all three checks CI gates separately, so a green make lint means the lint job will pass. make check-all adds pytest on top.

5. Commit Your Changes#

Write clear commit messages:

git add .
git commit -m "feat: add new ArXiv search feature"

Commit message format:

  • feat: new feature

  • fix: bug fix

  • docs: documentation

  • test: tests

  • refactor: code refactoring

  • style: formatting

  • chore: maintenance

6. Push and Create Pull Request#

git push origin feature/your-feature-name

Then create a Pull Request on GitHub with:

  • Clear description of changes

  • Link to related issues

  • Screenshots if applicable

Adding New Engineering Problems#

The system uses a unified problem registry that makes adding new EngiBench problems simple:

Step-by-Step Guide#

Both edits are in src/tools/problems.py.

  1. Update the ProblemId Type:

    ProblemId = Literal["beams2d", "photonics2d", "yournewproblem"]
    
  2. Add the engibench import path to _PROBLEM_IMPORTS:

    _PROBLEM_IMPORTS: dict[str, tuple[str, str]] = {
        "beams2d": ("engibench.problems.beams2d.v1", "Beams2D"),
        "photonics2d": ("engibench.problems.photonics2d.v1", "Photonics2D"),
        "yournewproblem": ("engibench.problems.yours.v0", "YourNewProblem"),
    }
    

    The module path and class name go here as strings. Do not add a top-level from engibench import ... to this file: the classes are imported lazily on first lookup, because importing engibench eagerly puts ~2 s onto every Streamlit start and every agent process. A guard test (tests/test_tools/test_import_cost.py) fails if that regresses.

  3. That’s It! The system automatically:

    • ✅ Enables the problem in all tools

    • ✅ Updates documentation in prompts

    • ✅ Extracts objectives dynamically

    • ✅ Handles rendering and constraints

    • ✅ Works with training and sampling

What NOT to Modify#

You don’t need to edit:

  • src/tools/engibench.py - Already problem-agnostic

  • src/tools/engiopt.py - Uses registry automatically

  • src/utils/prompts.py - Auto-generates documentation

  • Agent files - They adapt automatically

Testing Your New Problem#

@pytest.mark.parametrize("problem_id", ["beams2d", "yournewproblem"])
def test_new_problem(problem_id):
    from src.tools.problems import PROBLEM_CLASSES

    problem_class = PROBLEM_CLASSES[problem_id]
    problem = problem_class()
    assert problem is not None
    assert hasattr(problem, "objectives")

Code Style#

Python#

Follow PEP 8 and use type hints:

from typing import List, Optional


def process_data(
    data: List[float], threshold: float = 0.5, normalize: bool = True
) -> Optional[List[float]]:
    """Process input data with optional normalization.

    Args:
        data: Input data points
        threshold: Minimum value threshold
        normalize: Whether to normalize results

    Returns:
        Processed data or None if invalid
    """
    # Implementation
    pass

Docstrings#

Use Google-style docstrings:

def my_function(arg1: str, arg2: int) -> bool:
    """Short description.

    Longer description if needed.

    Args:
        arg1: Description of arg1
        arg2: Description of arg2

    Returns:
        Description of return value

    Raises:
        ValueError: When invalid input is provided
    """
    pass

Testing#

Writing Tests#

Use pytest fixtures and parametrization:

import pytest
from src.agents import EngineeringAgent


@pytest.fixture
def agent():
    return EngineeringAgent()


def test_optimization(agent):
    # Works with any problem type from the registry
    result = agent.optimize(problem="beams2d")
    assert result is not None


@pytest.mark.parametrize("volfrac", [0.3, 0.4, 0.5])
def test_volume_fractions(agent, volfrac):
    result = agent.optimize(config={"volfrac": volfrac})
    assert result["volfrac"] == volfrac


@pytest.mark.parametrize("problem_type", ["beams2d", "photonics2d"])
def test_all_problem_types(problem_type):
    # System automatically adapts to any registered problem
    from src.tools.problems import PROBLEM_CLASSES

    problem_class = PROBLEM_CLASSES[problem_type]
    problem = problem_class()
    assert problem is not None

Running Tests#

# All tests
make test

# Everything except the slow ones — this is what CI runs on a PR
make test-fast

# Specific file
pytest tests/test_agents/test_arxiv_agent.py

# With coverage
make test-cov

# Verbose output
pytest -v

Three workflows gate a pull request, and between them they run everything:

Workflow

Runs

test.yml

pytest -m "not slow"

slow-tests.yml

the slow tests, minus two that take 565s of their 587s

e2e.yml

the Playwright suite

The two expensive tests and test_baseline_reproduction.py are the exceptions: the first two move to a weekly schedule, and the third needs MPS and a live W&B session, so it stays a manual pre-release check.

Markers#

Four markers are registered, in [tool.pytest.ini_options] markers in pyproject.toml. That list is the whole set — anything else is a typo:

Marker

Meaning

unit

Fast, isolated. Applied by hand; never auto-applied.

integration

Spans multiple components

slow

Excluded by make test-fast; still runs on a PR, in slow-tests.yml

e2e

Full-browser Playwright tests; needs a running Streamlit server

tests/conftest.py also applies some of these automatically, by substring match on the test’s node id — which is worth knowing before you name a file:

  • node id contains integration → integration

  • node id contains slow, long_running or performance → slow

  • node id contains /e2e/ → both e2e and slow

Warning

The match is against the whole node id, path included. The second rule is the one to watch: a test named test_performance_of_x is silently marked slow, which drops it out of make test-fast and out of the linux-test job — a much quieter failure than being mislabelled integration. It would still run on a PR, in slow-tests.yml’s quick job, so the test is not lost; it just stops running where you expect it. No test currently trips the rule, so it is dormant rather than gone.

Note also that the directory tests/integration/ contributes none of the integration-marked tests; those marks come from the substring rule matching paths elsewhere.

Note

A typo’d marker is a collection error, so you will find out immediately. This relies on pyproject.toml declaring strict_markers = true as an ini key under [tool.pytest.ini_options], rather than --strict-markers in addopts: the addopts form is silently inert on pytest 9, where an unregistered marker only raises PytestUnknownMarkWarning and the test still passes. strict_config = true has the same requirement. Both are pinned in place by tests/test_scripts/test_pytest_config.py, so please do not “tidy” them back into addopts.

The two forms are mutually exclusive across the major version — on pytest 8 the ini keys are unknown options and only addopts enforces; on pytest 9 it is the reverse — so the test extra requires pytest>=9.0. If you are on 8.x, upgrade: the config still runs, but marker validation is silently off and the two guard tests fail with ValueError: unknown configuration value.

Note that markers applied programmatically by the auto-marking rules above are added after the point where strict_markers validates decorators, so those are covered by a separate assertion in the same test file.

Documentation#

Building Docs#

# Build documentation
make docs

# Live preview
make docs-watch

Writing Documentation#

  • Use Markdown for content pages

  • Add examples and code snippets

  • Include links to API reference

  • Update the index when adding new pages

Pull Request Guidelines#

Before Submitting#

  • [ ] All tests pass

  • [ ] Code is formatted (make format)

  • [ ] No linting errors (make lint)

  • [ ] Documentation updated

PR Checklist#

Your PR should:

  • Have a clear title and description

  • Link to related issues (e.g., “Fixes #123”)

  • Include tests for new features

  • Update documentation

  • Follow the code style guide

  • Have passing CI checks

Code Review Process#

  1. Maintainers review your PR

  2. Address feedback by pushing new commits

  3. Once approved, your PR will be merged

  4. Your contribution will be credited in releases

Questions?#

  • Open an issue for bugs

  • Start a discussion for questions

  • Join our community chat (if available)

License#

By contributing, you agree that your contributions will be licensed under the same license as the project.

Thank you for contributing! 🎉