Algorithm Template Guide
Overview
unitarylab_algorithms/template.py provides a minimal scaffold for implementing a new algorithm within the UnitaryLab Algorithms framework. By inheriting from BaseAlgorithm (unitarylab_algorithms.algo_base.BaseAlgorithm), your algorithm automatically gains logging, result formatting, file saving, and a unified return format.
This guide is checked line by line against the real source code of template.py and algo_base.py, and covers the full structure of the template, the role of each component, and the conventions for parameter injection on the web frontend.
Full Template Source
# unitarylab_algorithms/template.py
import os
import time
from typing import Any, Dict
import numpy as np
from unitarylab.core import Circuit
try:
from .algo_base import BaseAlgorithm
except ImportError:
import sys
_algorithms_dir = os.path.dirname(os.path.abspath(__file__))
if _algorithms_dir not in sys.path:
sys.path.insert(0, _algorithms_dir)
from algo_base import BaseAlgorithm
class ExampleAlgorithm(BaseAlgorithm):
"""Minimal template for adding a new algorithm under algorithms/."""
def __init__(self, text_mode: str = "plain", algo_dir: str = None):
if algo_dir is None:
_this = os.path.abspath(__file__)
_directory = os.path.dirname(_this)
algo_dir = os.path.join(os.getcwd(), "results", os.path.basename(os.path.dirname(_directory)), os.path.basename(_directory))
os.makedirs(algo_dir, exist_ok=True)
# Set the algorithm name and prefix
super().__init__(name="Example Algorithm", prefix="EXP", text_mode=text_mode, algo_dir=algo_dir)
def run(self, n: int = 2) -> Dict[str, Any]:
"""
Run the Example algorithm.
Parameters:
n: Number of qubits
Returns:
Dictionary containing algorithm results with fields:
- status: Execution status, 'ok' on success
- circuit_path: Local path to saved quantum circuit diagram (SVG)
- file_path: Local path to saved text file with results
"""
# First record the input parameter information, then update it; this automatically prints the parameters
input = {"Number of qubits (n)": n}
self.update_input(input)
# The normal algorithm execution flow; log messages can be output via self.log during this
self.log("Stage 1/4: Building circuit...")
qc = Circuit(n)
# Record the output information, then update it; this automatically prints the result
output = {"Result": "Example result", "Elapsed time (s)": 0}
self.update_output(output)
# Record the algorithm's execution status and summary information
self.status = "success"
self.summary = f"Execution successful. Example result is {output['Result']}."
# Save the circuit diagram and the result text
circuit_path = self.save_circuit(qc)
filename = self.save_txt()
# If there are multiple circuit diagrams, you can save them via self.save_circuit(qc, name="another")
circuit_path_1 = self.save_circuit(qc, name="example_1")
circuit_path_2 = self.save_circuit(qc, name="example_2")
circuit_path = [circuit_path_1, circuit_path_2]
# Finally, build the return dictionary, containing the algorithm's execution status (True/False), the circuit diagram path(s), the saved file path, and the quantum circuit itself
return self._build_return_dict(True, circuit_path, filename, qc)
def test(n: int = 2) -> Dict[str, Any]:
# The test function is used to test the algorithm locally; the parameter n needs a default value
# In legacy mode, rich text is used; in plain mode, plain text is used
algo = ExampleAlgorithm(text_mode="legacy")
return algo.run(n=n)
if __name__ == "__main__":
# Add a `# [PARAM]` comment after the assignment line for parameters entered in test; the variable name must match the parameter name declared in parameters.json — when the algorithm is run on the web frontend, the assignment statement marked with `# [PARAM]` will automatically be replaced with the parameter value entered by the user
# For parameters that don't need to be adjusted, simply keep the default value and do not add the `# [PARAM]` comment
n = 2 # [PARAM]
test(n=n)Step-by-Step Guide: Writing a New Algorithm
Step 1 — Create the File
Create a directory and file under unitarylab_algorithms/:
unitarylab_algorithms/
└── my_category/
└── my_algo/
├── __init__.py
├── algorithm.py
└── parameters.jsonStep 2 — Import BaseAlgorithm and Implement __init__
import os
from typing import Any, Dict
from unitarylab.core import Circuit
from unitarylab_algorithms.algo_base import BaseAlgorithm
class MyAlgorithm(BaseAlgorithm):
"""Brief description of what this algorithm does."""
def __init__(self, text_mode: str = "plain", algo_dir: str = None):
if algo_dir is None:
_this = os.path.abspath(__file__)
_directory = os.path.dirname(_this)
algo_dir = os.path.join(
os.getcwd(), "results",
os.path.basename(os.path.dirname(_directory)),
os.path.basename(_directory),
)
os.makedirs(algo_dir, exist_ok=True)
super().__init__(name="My Algorithm", prefix="MYA", text_mode=text_mode, algo_dir=algo_dir)The template does not omit
__init__— every algorithm needs to explicitly set the algorithm name and prefix viasuper().__init__(name=..., prefix=..., text_mode=..., algo_dir=...)(prefixis automatically wrapped in square brackets; if not explicitly passed, it defaults to the first 3 uppercase characters ofname, e.g.EXP). Ifalgo_diris not specified, it is automatically generated and created following the ruleresults/<parent-directory-name>/<current-directory-name>.
Step 3 — Define the .run() Method
def run(self, param_a: int, param_b: float = 1.0) -> Dict[str, Any]:
...Step 4 — Call update_input at the Start
Always log the input parameters so they appear in the formatted result and the saved text (update_input automatically prints "Starting {name}" along with the parameter list):
self.update_input({'param_a': param_a, 'param_b': param_b})Step 5 — Build and Execute the Circuit
qc = Circuit(param_a)
qc.h(0)
qc.cx(0, 1)
result = qc.execute()Step 6 — Record Execution Status and Summary
self.status and self.summary are plain attributes that must be assigned manually; they appear, respectively, in the “Status” and “Summary” sections of the output of format_result_ascii():
self.status = "success"
self.summary = f"Execution successful. Result is {some_value}."Step 7 — Save the Circuit Diagram and Result Text
circuit_path = self.save_circuit(qc, name='my_algo_circuit') # Returns the full file path (.svg)
filename = self.save_txt() # Returns only the filename (no directory!)If a single run needs to save multiple circuit diagrams, you can call save_circuit multiple times and collect the paths into a list:
circuit_path_1 = self.save_circuit(qc, name="stage_1")
circuit_path_2 = self.save_circuit(qc, name="stage_2")
circuit_path = [circuit_path_1, circuit_path_2]
save_circuitreturns the full path ({algo_dir}/{name}.svg), whereassave_txtreturns only the filename (without the directory prefix) — the two return values have different shapes, so be careful when mixing them.
Step 8 — Record the Output and Build the Return Value via _build_return_dict
self.update_output({'my_custom_field': some_value})
self.log(f"Computation complete: {some_value}")
return self._build_return_dict(True, circuit_path, filename, qc)The behavior of _build_return_dict(success, circuit_path, filepath, circuit=None) (from algo_base.py):
success: bool→ converted to the string'ok'(True) or'failed'(False), used as thestatusfield of the returned dictionary; this is a separate value from theself.statusattribute and the two are not kept in sync.- If
filepathis a string, it is automatically wrapped into a single-element list; then, for each filename,{"format": filename[-3:], "filename": filename}is constructed. For new algorithms, it’s recommended that result files use a 3-character extension (e.g..txt,.svg,.npy). circuit_pathis placed into the returned dictionary as-is (it can be a string, or the list from the previous step).- The final return value is the dictionary formed by merging
{"status", "circuit_path", "plot", "circuit"}withself.output(dict.update()returnsNone; the source code uses the expressionresult.update(self.output) or resultto return the mergedresult— this is equivalent to updating first and then returning; it is not a logic flaw, just an unusual way of writing it).
Step 9 — Add a test() Function
Every algorithm should provide a module-level test() function with sensible defaults, using text_mode="legacy" by convention (the test() in the real template explicitly uses legacy rich-text mode rather than the default plain):
def test(param_a=3, param_b=1.0):
algo = MyAlgorithm(text_mode="legacy")
return algo.run(param_a=param_a, param_b=param_b)Step 10 — The __main__ Block and the # [PARAM] Injection Convention
The template’s __main__ block is not just an ordinary local test entry point — it is tied to the web frontend’s parameter injection mechanism:
if __name__ == "__main__":
param_a = 3 # [PARAM]
param_b = 1.0 # [PARAM]
test(param_a=param_a, param_b=param_b)- For parameters that should be adjustable by users on the web frontend, add a
# [PARAM]comment at the end of their assignment line; the variable name must match the parameter name declared inparameters.json— when the algorithm is run on the web frontend, the assignment statement marked with# [PARAM]is replaced with the parameter value actually entered by the user. - For parameters that don’t need to be adjusted by the user (i.e., that don’t need to be exposed to the web frontend), simply keep the default value and do not add the
# [PARAM]comment. - This convention is not merely example code within the template: currently, 31 Python source files actually use the
# [PARAM]marker (30 algorithm modules plustemplate.py; textual mentions of the marker in the README are not counted). It is a general convention for the__main__block of every algorithm and should be followed when adding new algorithms.
BaseAlgorithm Method Reference
| Method | When to Use |
|---|---|
super().__init__(name, prefix="", text_mode="plain", algo_dir=None) | Called in the subclass’s __init__ to set the algorithm name/prefix/text mode/results directory; it also initializes self.status="", self.input={}, self.output={}, self.info=[], self.summary="" |
self.update_input(dict) | Call at the start of .run() to record input parameters; merges into self.input and automatically prints them |
self.update_output(dict) | Call after computation completes to record output values; merges into self.output and automatically prints them |
self.log(message) | Pass a string: prints and appends it to self.info; pass a dictionary: prints each entry as - key = value (does not append to self.info) |
self.status = "..." / self.summary = "..." | Direct attribute assignment, used for the “Status”/“Summary” sections in format_result_ascii(); not automatically synced to the status field of the .run() return dictionary |
self.save_circuit(circuit, name=None) | Persists the circuit as an SVG and returns the full file path; if name is omitted, the filename is generated from the algorithm name |
self.save_txt() | Writes format_result_ascii() to a text file and returns only the filename (without the directory) |
self.format_result_ascii() | Obtains the formatted result as a string (text_mode="plain" gives plain text, "legacy" adds emoji decoration) |
self._build_return_dict(success, circuit_path, filepath, circuit=None) | Builds and returns the standard return dictionary for .run(); see “Step 8” above |
Complete Minimal Example
# unitarylab_algorithms/my_category/my_algo/algorithm.py
import os
from typing import Dict, Any
from unitarylab_algorithms.algo_base import BaseAlgorithm
from unitarylab.core import Circuit
class MyAlgorithm(BaseAlgorithm):
"""Example: apply an H gate to all n qubits and output the probability distribution."""
def __init__(self, text_mode: str = "plain", algo_dir: str = None):
if algo_dir is None:
_this = os.path.abspath(__file__)
_directory = os.path.dirname(_this)
algo_dir = os.path.join(
os.getcwd(), "results",
os.path.basename(os.path.dirname(_directory)),
os.path.basename(_directory),
)
os.makedirs(algo_dir, exist_ok=True)
super().__init__(name="My Algorithm", prefix="MYA", text_mode=text_mode, algo_dir=algo_dir)
def run(self, n: int = 3) -> Dict[str, Any]:
self.update_input({'n': n})
self.log("Stage 1/2: Building circuit...")
qc = Circuit(n)
for i in range(n):
qc.h(i)
sim_result = qc.execute()
probs = sim_result.probabilities
self.update_output({'probabilities': probs})
self.status = "success"
self.summary = f"Applied an H gate to {n} qubits, uniform distribution: {len(probs)} states."
circuit_path = self.save_circuit(qc, name='h_all')
filename = self.save_txt()
return self._build_return_dict(True, circuit_path, filename, qc)
def test(n=3):
algo = MyAlgorithm(text_mode="legacy")
return algo.run(n=n)
if __name__ == "__main__":
n = 3 # [PARAM]
test(n=n)Registration and Usage
Once an algorithm is created, it can be imported and used directly, with no registration step required:
from unitarylab_algorithms.my_category.my_algo.algorithm import MyAlgorithm
algo = MyAlgorithm()
result = algo.run(n=4)
print(result['status'])The framework uses direct imports, with no centralized registration required. The web frontend’s parameter panel relies on parameters.json (parameter names, types, default values, descriptions) working together with the # [PARAM] markers in the source code; when adding a new algorithm, the parameter names in both must stay consistent.