Develop Custom Cards

This page shows how to add a card to Make Dataset using the current Operation/Params architecture, with the user interface kept separate from the card logic.

When adding a built-in card, use the repository’s make-dataset-card-dev skill for the full specification, implementation, documentation, and validation workflow. Use this page as a quick reference.

Architecture overview

Each card has three layers:

::
  • The UI layer contains no algorithms. It reads widgets, constructs the params object, and invokes the operation.

  • The Operation layer must not import PySide6, qfluentwidgets, or MessageManager. Report errors by raising exceptions.

Step 1: Choose the operation type

Type

Base class

Method signature

Use it for

StructureOperation

core.cards.operation.StructureOperation

run_structure(structure, params) list[Atoms]

Per-structure transformations such as strain, perturbation, supercells, and doping

DatasetOperation

core.cards.operation.DatasetOperation

run_dataset(dataset, params) list[Atoms]

Whole-dataset filtering or ordering, such as FPS

GeneratorOperation

core.cards.operation.GeneratorOperation

generate(params) list[Atoms]

Generation without an input structure, such as crystal prototypes

Step 2: Define the Params dataclass

Define it in the appropriate module under src/NepTrainKit/core/cards/ (lattice.py, alloy.py, defect.py, magnetism.py, structure.py, or filter.py):

from dataclasses import dataclass

@dataclass(frozen=True)
class MyCardParams:
    """Parameters for my card."""
    param_a: str = "default"
    param_b: float = 1.0
    use_seed: bool = False
    seed: int = 0
  • Use a frozen dataclass.

  • Give every field a default value.

  • Use snake_case names that match the keys returned by the UI’s get_params() method.

Step 3: Implement the operation

In the same module:

::

Rules:

  • Do not import UI libraries.

  • Raise an exception when parameter validation fails.

  • Use append_config_tag to add a traceable label.

  • Do not silently fall back to another behavior.

Step 4: Implement the UI card

Create a module under src/NepTrainKit/ui/views/_card/ and inherit from MakeDataCard:

::

Read legacy keys only when migrating an existing card. The template above is for a new card, so from_dict reads only params. When migrating a card already present in saved user workflows, the else branch may construct Params from legacy fields such as data["old_key"] to preserve backward compatibility.

Key rules

  • Do not override run(). The base class dispatches work to the correct thread from the type returned by create_operation().

  • Do not put algorithms in the UI. init_ui() constructs widgets, get_params() reads values, and the Operation owns all computation.

  • Do not add a new process_structure() implementation. If an existing compatibility method must remain, reduce it to one delegation line: return self.create_operation().run_structure(structure, self.get_params()).

  • Use SpinBoxUnitInputFrame for numeric values, ComboBox for enumerations, CheckBox for switches, and LineEdit for strings.

Step 5: Register, document, and test the card

  • Decorate the UI class with @CardManager.register_card.

  • Import the class in src/NepTrainKit/ui/views/_card/__init__.py and add it to __all__.

  • Add docs/source/module/make-dataset-cards/cards/my-card.md. Start its example from a diagnosed training-set problem, then explain how the card adds the missing structures. See the card documentation guide.

  • Write operation tests that do not require Qt:

    def test_my_card_operation():
        op = MyCardOperation()
        result = op.run_structure(test_atoms, MyCardParams(param_b=2.0))
        assert len(result) > 0
    

Validate

python skills/make-dataset-card-dev/scripts/run_card_checks.py --quick
python tools/docs/audit_card_docs.py

Install an external card

Built-in cards live in src/NepTrainKit/ui/views/_card/. Put an external custom card in the cards/ subdirectory of the user configuration directory; NepTrainKit discovers it at startup.