Skip to Content

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.json

Step 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 via super().__init__(name=..., prefix=..., text_mode=..., algo_dir=...) (prefix is automatically wrapped in square brackets; if not explicitly passed, it defaults to the first 3 uppercase characters of name, e.g. EXP). If algo_dir is not specified, it is automatically generated and created following the rule results/<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_circuit returns the full path ({algo_dir}/{name}.svg), whereas save_txt returns 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 the status field of the returned dictionary; this is a separate value from the self.status attribute and the two are not kept in sync.
  • If filepath is 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_path is 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"} with self.output (dict.update() returns None; the source code uses the expression result.update(self.output) or result to return the merged result — 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 in parameters.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 plus template.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

MethodWhen 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.

Last updated on