# Qsolve documentation Source: https://qsolve.omlabs.co/docs --- # What is Qsolve > Qsolve is a platform where you run validated quantum and hybrid workflows at a quoted price and see their measured results against the best classical method. Section: Start here · Source: https://qsolve.omlabs.co/docs/start/what-is-qsolve · Updated: 2026-11-12 Qsolve lets you run quantum and hybrid quantum-classical algorithms without building them. You pick a **workflow** (a packaged algorithm for one kind of problem), give it your input, and get an answer together with a measured comparison against the best classical method. Every run has a price quoted before it starts. You do not need to be a quantum computing expert. You do need to be comfortable reading a result that says "matched the classical answer" or "below the classical answer", because Qsolve reports both. ## The problem it addresses Quantum algorithms are published as papers and notebooks. Whether one works on your problem, how accurate it is on today's noisy machines and what it costs are questions a notebook rarely answers. Qsolve answers them with measurements, taken the same way for every workflow: - **Accuracy against a baseline.** Every workflow declares a classical method, the **baseline**. Qsolve runs both on the same input and measures how close the quantum answer is to the baseline's. - **Fidelity and circuit size.** Qsolve records every circuit sent to the machine and every count that came back, then measures how close the output is to what a perfect machine would produce. - **Cost.** Each workflow page shows a typical cost, and each run shows a quote before it starts. The workflow's author does not write these numbers. Qsolve measures them, and each page says which numbers were measured and which the workflow reported. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ## Workflows today | Area | Workflows | Typical input | |---|---|---| | Optimisation | `generic-qubo`, `max-cut`, `portfolio-optimization` | A QUBO matrix, a weighted graph, asset returns and covariances | | Chemistry and materials | `molecular-ground-state`, `interaction-energy`, `electrolyte-solvent-screen` | A molecule name or geometry, a set of species, a list of solvents | | Machine learning | `quantum-kernel-classifier` | A small labelled table or a built-in dataset | Outside developers can publish workflows too. A workflow is listed only after it passes Qsolve's validation. To publish one, see [Build overview](/docs/build/overview). ## What Qsolve does not do It does not promise a quantum advantage. Problem sizes are small: the optimisation workflows accept up to 24 variables and the chemistry workflows up to 12 qubits per system. At those sizes a classical method can often find the exact answer (the optimisation baseline is an exhaustive search up to 22 variables), so the quantum method can at best match it. What Qsolve adds is that you see this in numbers. Results from real quantum hardware carry that hardware's noise. Qsolve measures and shows it but cannot remove it, and some workflows land below their baseline on hardware. The track record says which. Qsolve also cannot prove that a baseline is the best classical method that exists. It shows which classical methods were run and how each scored. ## Where to go next [How a run works](/docs/start/how-it-works) follows a run from submit to result. The [Quickstart](/docs/start/quickstart) makes a first run on a simulator with your free credits. The [Glossary](/docs/start/glossary) covers the vocabulary. --- # How a run works > What happens between pressing Run and reading the result: sizing, the quote, the credit hold, execution, settlement and the run record you can open afterwards. Section: Start here · Source: https://qsolve.omlabs.co/docs/start/how-it-works · Updated: 2026-11-12 A run is sized, quoted, queued, executed and settled. You see the quote before any credits are spent and pay only for what the run used. ## 1. You submit On a workflow page you enter an input, choose a machine (the **target**), optionally adjust the solver settings, and set a **spend limit**. Press **Run**. The input is checked against the workflow's input schema first, so a malformed input is rejected before anything is queued. ## 2. Qsolve sizes the run The status pill shows **Sizing**. Qsolve starts the workflow in estimate mode, which executes no quantum circuits. The workflow reports how many qubits, circuits and shots your input needs on the chosen target and how long it expects to take. This takes a few seconds. If the predicted time is longer than the workflow's time limit, the run fails here with "This run would take longer than the workflow allows. Nothing was charged." Reduce the problem size or the settings that drive run time, such as optimiser iterations or circuit depth. ## 3. You get a quote From the estimate, Qsolve computes two numbers in credits (1 credit = 1 USD): - The **quote**: the expected cost. - The **hold**: the most the run can cost. On simulators it equals the quote. On a quantum computer it is 1.5 times the quote, because the provider meters quantum time and it can vary. Qsolve compares the hold with your spend limit: | Situation | What happens | |---|---| | Hold is within your spend limit and balance | The credits are held and the run is queued | | Hold is above your spend limit | **Needs your OK**. Confirm with **Run for up to X credits** or **Cancel**. An unconfirmed quote expires after 15 minutes | | Balance too low, or a run cap or monthly cap would be passed | The run fails with a clear message and nothing is held | [Credits and pricing](/docs/use/credits-and-pricing) has the formulas and worked examples. ## 4. The run executes The status pill shows **Queued** until a machine is free, then **Running**. The workflow's code runs in an isolated environment with no network access, so it cannot send your input anywhere. When it wants to run a quantum circuit, the circuit goes to the target (a simulator or a quantum computer) through Qsolve, which counts every circuit and shot and returns the counts. Because Qsolve sees every circuit and every count, the circuit size, shot count and fidelity do not depend on what the workflow says about itself. See [Data and privacy](/docs/use/data-and-privacy). You can press **Stop** at any time. A run that has not started ends at once and its hold is released. A run already executing is stopped and billed for what it used. ## 5. The run settles When the workflow finishes, Qsolve checks that the quantum work it metered matches what the workflow's result accounts for. If the run used more than its result accounts for, it fails as an integrity mismatch and its numbers are discarded. Otherwise: - **Charge.** On simulators you pay the quote. On a quantum computer you pay the actual cost, based on the quantum seconds the provider reported. The charge is never more than the hold. - **Release.** The rest of the hold returns to your balance. **Usage** lists both entries, "Held for a run" and "Returned after a run". - **Record.** The run page shows the answer, the comparison with the baseline, the quality of the quantum run, the circuits and a receipt. See [Run records](/docs/use/run-records). --- # Quickstart > Create a Qsolve account, use the 100 free credits to run the max-cut workflow on the CPU simulator, and read the result in about five minutes. Section: Start here · Source: https://qsolve.omlabs.co/docs/start/quickstart · Updated: 2026-11-12 In five minutes you can create an account, run the `max-cut` workflow's example graph on the CPU simulator, and read the comparison with the classical baseline. The run costs a small fraction of one credit, and every new account starts with 100. ## 1. Create an account On the sign-in page choose **Create your account**. Sign up with an email and a password (at least 8 characters), continue with GitHub, or use **Email me a sign-in link** to skip the password. Qsolve sends a confirmation email, and your 100 free credits arrive when you confirm it. Each person gets one grant: variants of one Gmail address (dots, `+tags`) count as the same address. Your balance shows at the top of the page and on **Usage**. ## 2. Pick a workflow Open **Run an experiment**. Each workflow has a row of small coloured squares, one per benchmark case: green means the case matched the best classical answer, amber means it came out below it, red means it failed. Search for "graph" or choose the `max-cut` card. It splits a weighted graph into two groups so the total weight of edges between them is as large as possible. ## 3. Set up the run The form is pre-filled with the first example, a 6-node graph. The chips under **Start from** load other examples and **Blank** clears the form. To edit raw JSON, switch **Edit as** to **JSON**: ```json { "nodes": ["a", "b", "c", "d", "e", "f"], "edges": [["a","b"], ["b","c"], ["c","d"], ["d","e"], ["e","f"], ["f","a"], ["a","d", 2]] } ``` Under **Where to run**, leave **CPU simulator** selected. It is exact, noiseless and the cheapest. See [Machines and targets](/docs/use/machines-and-targets) for the others. In the **Run summary** box, the **Spend limit** is the most you are willing to pay. Leave the suggested value. You see the exact quote before anything is charged. ## 4. Run it Press **Run**. The run page moves through **Sizing**, **Queued** and **Running** to **Done**, usually within seconds, with no refresh needed. ## 5. Read the result From the top, the run page shows: - **Answer**: a one-sentence summary, then the answer itself (here, which nodes go in which group and the cut weight). - **Quantum vs classical**: a verdict such as "Matched the classical answer", "x% better than classical" or "x% from the classical answer", with both values side by side and the name of the classical method. - **Circuits**: every distinct circuit the run sent, with qubits, depth and gate counts. - **How the solver got there**: the solver's own settings and numbers. - **Cost**, in the side column: credits charged and how the hold was released. [Reading your results](/docs/use/reading-your-results) goes through each part, and [Choosing a workflow](/docs/use/choosing-a-workflow) explains the track record on the workflow page. The **Receipt** card shows the workflow version, the code commit, and hashes of the input and the result. ## Next [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise) explains the numbers. [Credits and pricing](/docs/use/credits-and-pricing) shows what runs cost on other machines. The [FAQ](/docs/start/faq) covers common messages. --- # Glossary > Plain definitions of the terms you meet in Qsolve, from shots, fidelity and post-selection to QUBO, VQE, ZNE, noise models, transpilation and active spaces. Section: Start here · Source: https://qsolve.omlabs.co/docs/start/glossary · Updated: 2026-11-12 ### Accuracy How close the quantum method's result is to the baseline's, as a percentage with an error bar. 100% means the quantum result equals the baseline. How it is computed differs by workflow and is stated on the workflow page. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ### Active space In chemistry workflows, the small set of electrons and orbitals the quantum method handles while the rest of the molecule is treated classically. A larger active space is more accurate and needs more qubits (two per orbital). The chemistry workflows accept up to 6 orbitals, which is 12 qubits. ### Baseline The classical answer a workflow's quantum result is compared with. Every workflow declares one method as its baseline, and Qsolve runs it on the same input. Qsolve cannot prove the baseline is the best classical method that exists. See [Reading your results](/docs/use/reading-your-results). ### Chemical accuracy An energy error of 1.6 millihartree (mHa) or less, the usual threshold for a chemistry result to be useful. The chemistry workflows show errors in mHa so you can compare. ### Circuit A sequence of quantum gates applied to qubits, followed by measurement. Its **depth** is the number of layers of gates, and its **two-qubit gates** are the noisiest operations. Every circuit a run sends is recorded on the run page. See [Run records](/docs/use/run-records). ### Credit The unit of spending. 1 credit equals 1 US dollar. New accounts receive 100 after confirming their email. See [Credits and pricing](/docs/use/credits-and-pricing). ### Error mitigation Classical post-processing that reduces the effect of hardware noise, such as readout correction, post-selection and zero-noise extrapolation. The workflow reports mitigated figures, and Qsolve measures the raw figures beside them. ### Fidelity How close the output distribution of a quantum run is to what a perfect, noise-free machine would produce for the same circuit. 100% is identical. Qsolve measures the raw fidelity from the recorded circuits and counts and shows the mitigated fidelity next to it. ### Hartree and millihartree The unit of energy in chemistry workflows, written Ha. A millihartree (mHa) is one thousandth of that. ### Noise model A simulation of a specific quantum device's published error rates, run on a classical computer. A "noise-model estimate" in a track record shows how a workflow behaves under that device's noise without using the device. Real devices can be noisier than their models. See [Machines and targets](/docs/use/machines-and-targets). ### Post-selection Discarding shots that fail a validity check, for example a bit string with the wrong number of ones when the problem requires exactly k. The share that remains is shown as **Shots kept**. ### QAOA Quantum approximate optimisation algorithm. A short circuit that alternates between favouring low-cost answers and mixing possibilities, with its angles tuned by a classical optimiser. Used by `generic-qubo`, `max-cut` and `portfolio-optimization`. ### Qubit The basic unit of a quantum computer, the counterpart of a bit. Each workflow states the largest number of qubits it accepts under **Limits**. ### QUBO Quadratic unconstrained binary optimisation: choose a 0 or 1 for each variable to minimise x'Qx for a matrix Q. Many scheduling, routing, selection and partitioning problems can be written this way. The `generic-qubo` workflow accepts any such matrix. ### Seed A number that fixes the workflow's random choices. The same workflow version, input, settings and seed give the same result on a simulator. See [Reproducibility](/docs/use/reproducibility). ### Shots How many times a circuit is run and measured. More shots reduce sampling noise and cost more. ### Target The machine a run executes on: the CPU simulator, the GPU simulator or a quantum computer. See [Machines and targets](/docs/use/machines-and-targets). ### Track record The accuracy, fidelity and cost of a workflow's benchmark cases, shown on its page. Simulator results count towards the headline accuracy. Hardware results are shown separately. ### Transpilation Rewriting a circuit into the gates a specific chip supports and mapping its qubits to physical ones. It usually makes the circuit longer. The **Circuits** card can show a circuit "As written" and "As run on the chip". ### VQE Variational quantum eigensolver. A circuit whose parameters a classical optimiser tunes to find the lowest energy of a molecule. The chemistry workflows train the circuit on a noise-free simulator and measure the final energy on the target. ### Workflow A packaged algorithm for one kind of problem, with an input form, declared methods, benchmark cases and a version. See [What is Qsolve](/docs/start/what-is-qsolve). ### ZNE Zero-noise extrapolation. A mitigation technique that runs a circuit at several amplified noise levels (for example by repeating gates) and extrapolates back to zero noise. The chemistry workflows use it on quantum devices. --- # Frequently asked questions > Direct answers to common questions about Qsolve: cost, failed runs, quantum hardware, accuracy against classical methods, privacy, reproducibility and limits. Section: Start here · Source: https://qsolve.omlabs.co/docs/start/faq · Updated: 2026-11-12 ## Getting started ### Do I need to know quantum computing? No. You describe your problem in the workflow's input form and read the result in domain terms, such as which assets to hold or which solvent binds lithium most strongly. You do need to read a comparison with a classical baseline, which the run page explains in words. See [Reading your results](/docs/use/reading-your-results). ### Can I buy more credits? Not in the web app. See [Credits and pricing](/docs/use/credits-and-pricing). ## Results ### Is the quantum answer better than the classical one? Not by default. On some cases it matches the classical answer and on others it falls below it, and the track record shows which. A quantum result can at best equal an exact baseline. The value of Qsolve is that you see this and can decide whether the quantum method is worth using. ### Why does my result say "x% from the classical answer"? The quantum method found a worse answer than the baseline. On a simulator that reflects the algorithm and its settings. On a quantum computer it also reflects hardware noise, which is why the workflow page shows hardware results separately. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ### Why is the fidelity low when the answer is correct? Fidelity measures how close the whole output distribution is to the ideal one, and noise spreads probability across wrong outcomes. A sampling workflow only needs the best outcome it saw, and that can still be the right one. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ### What is the difference between "measured" and "reported"? Measured numbers are computed by Qsolve from what actually went to the machine: circuits, shots, depth, raw fidelity and the accuracy against the baseline. Reported numbers are the workflow's own claims, such as its mitigated fidelity, and are labelled as such. ### Is the track record based on my data? No. It comes from fixed benchmark inputs, so it shows how the workflow behaves on problems like those, not on yours. Run a representative input of your own on a simulator before relying on it. ### Why did two runs of the same input differ? On a simulator, the same workflow version, input and settings give the same result hash. The web form runs the latest published version with the default seed, so a difference usually means the workflow was updated or a setting differs. On a quantum computer, results vary because the hardware is noisy and drifts. See [Reproducibility](/docs/use/reproducibility). ## Cost and credits ### How much does a run cost? The workflow page shows a typical cost and each run shows an exact quote before anything is charged. A small run on the CPU simulator costs a few hundredths of a credit. A quantum computer run is priced by quantum seconds and costs far more. See [Credits and pricing](/docs/use/credits-and-pricing). ### Am I charged for a failed run? Only for work that ran. A run that fails before it starts costs nothing. A run that fails after using machine time is billed for what it used, never more than the hold. ## Machines ### Can I run on a real quantum computer? If the workflow supports it and the machine is open, yes. **Where to run** offers an IBM quantum computer with a choice of device, priced by quantum seconds. It may not be open to every account yet. The **Compute** page shows availability. See [Machines and targets](/docs/use/machines-and-targets). ### How long will my run take? Once the run is sized, the run page shows "Runs like this take about N min", based on the workflow's own prediction. Waiting for a free machine, or in a quantum computer's queue, comes on top. A run predicted to exceed the workflow's time limit is rejected before it starts. ## Privacy and records ### Who can see my runs and inputs? The members of your account, and nobody else. The exception is the platform's own benchmark runs, which every signed-in user can read. The workflow's author does not receive your input, and the workflow's code cannot send it out. See [Data and privacy](/docs/use/data-and-privacy). ### How long are my runs kept? Progress events for 30 days. A run's input, settings and result are cleared 90 days after it finishes, while its status, cost and receipt remain. Download a run with **Report** if you need it longer. ## Publishing ### Can I run an older version of a workflow? No. The web form always runs the latest published version. See [Reproducibility](/docs/use/reproducibility). ### How do I publish my own workflow? Write it in Qiskit with the Qsolve SDK, test it locally, and submit its repository for validation. See [Build overview](/docs/build/overview) and [Validation and publishing](/docs/build/validation-and-publishing). --- # Choosing a workflow > How to find the right Qsolve workflow for your problem and judge it before you spend credits: search, track record, problem limits and the classical baseline. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/choosing-a-workflow · Updated: 2026-11-12 Match your problem to a workflow's input and size limits, then read its track record to see how it has performed against the classical baseline. The track record describes benchmark cases, not your data, so treat it as a guide to what to expect. ## Find candidates Open **Run an experiment**. Type a problem into the search box, for example "portfolio" or "binding energy". Search covers each workflow's title, summary, name and tags. The chips below filter by family, such as optimisation or chemistry. Each card shows a **By Qsolve** badge for workflows Qsolve maintains, a row of squares (one per benchmark case: green matches the best classical answer, amber is below it, red failed), how many cases were confirmed, and the median cost of the benchmark runs as **typical credits**. ## The workflows | Workflow | Solves | Input | Largest problem | |---|---|---|---| | `generic-qubo` | Any QUBO, optionally with "choose exactly k" | A square matrix `Q`, optional labels | 24 variables | | `max-cut` | Split a weighted graph into two groups to maximise the weight between them | Nodes and weighted edges | 24 nodes | | `portfolio-optimization` | Pick k assets, trading return against risk | Asset names, expected returns, covariance, number to select | 24 assets | | `molecular-ground-state` | Ground-state energy of a closed-shell molecule or ion | A built-in name such as H2 or H2O, or an XYZ geometry, plus an optional active space | 6 orbitals (12 qubits) | | `interaction-energy` | Binding, solvation, adsorption or reaction energy as a signed sum of system energies | A set of species, each with a geometry or name | 6 orbitals per system | | `electrolyte-solvent-screen` | Rank battery-electrolyte solvents by how strongly they bind Li+ | A list of solvents such as EC, ACN, water, or your own geometries | 8 solvents | | `quantum-kernel-classifier` | Binary classification with a quantum-kernel support vector machine | A built-in dataset or your own table | 8 features, 60 training points | The optimisation workflows use QAOA and are judged against an exhaustive search (simulated annealing above 22 variables). The chemistry workflows use VQE and are judged against the exact solution inside the chosen active space, with Hartree-Fock, MP2 and CCSD(T) shown alongside. The classifier is judged against a tuned RBF support vector machine, with logistic regression, a random forest and gradient boosting for context. The [Glossary](/docs/start/glossary) defines these terms. ## Read the workflow page Five figures at the top summarise the track record: | Figure | What it is | |---|---| | **Accuracy · simulator** | Median accuracy against the baseline over the latest simulator benchmark cases | | **Fidelity** | Median fidelity of the quantum output on noisy runs, labelled "hardware" if the workflow has real-device results and "noise model" otherwise | | **Shots kept** | Median share of shots that passed the error checks on those noisy runs | | **Typical cost** | Median credits the benchmark runs cost | | **Version** | The published version you will run | "n/a" or "none yet" means there are no results of that kind yet. The **Track record** section lists each benchmark case with its accuracy, error bar, cost and age. Open a row to see every method run on that input side by side, with a note on how each value was checked, the quantum answer, the error-mitigation figures and the circuit. Up to three tables appear: - **Track record** (simulator). These count towards the headline accuracy. Only the newest result per case is current; older ones sit in a collapsed "N earlier results" list. - **Noise-model estimate.** Small cases simulated with an IBM processor's published noise. Real devices can be noisier. - **On quantum hardware.** The same workflow on real processors. These measure the device as much as the workflow, so they do not count towards the headline accuracy. **Limits**, at the bottom, gives the largest problem the workflow accepts, which machines it runs on, its tags, and a link to its source repository (or "Maintained by Qsolve"). ## Judge a workflow before you commit 1. **Check that your problem fits.** Size limits and input format are fixed. Inputs over the limit are rejected before any credits are spent. 2. **Look at cases like yours.** A workflow that matches the baseline on 10-variable problems says little about 24-variable ones. Pick the benchmark cases closest to your size. 3. **Look for amber and red squares.** Amber means the quantum method found a worse answer than the baseline. Red means the run failed. Both are information. 4. **Read the baseline.** The methods table names the classical method the quantum result was compared with. An exhaustive search or exact solution makes a strong comparison. A heuristic (annealing above 22 variables) makes a "match" mean less. 5. **Run your own input on the CPU simulator.** It costs a few hundredths of a credit and shows how the workflow handles your data. Then decide whether a quantum computer run is worth its price. --- # Running an experiment > How to set up and launch a Qsolve run: enter your input, pick a machine, adjust solver settings, set a spend limit, then approve the quote, stop or rerun it. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/running-an-experiment · Updated: 2026-11-12 You set up a run on the workflow's page in four parts: your problem, where to run, solver settings and the run summary. Press **Run** and Qsolve sizes the run and quotes a price. Within your spend limit it starts at once. Above it, it waits for your approval. You must be signed in; otherwise the button reads **Sign in to run**. ## Your problem The input form is generated from the workflow's input schema and validates as you type. The **Run summary** box shows **Ready** when the input is valid, or "N to fix" with the problems marked in the form. **Start from** lists the workflow's example inputs, for instance "6-node graph" for `max-cut`. Choose one to load it, or **Blank** to start empty. The first example loads by default. Switch **Edit as** to **JSON** to edit the raw input. The editor reports "Valid input" or the first problem, and **Open a .json file** loads an input from disk. The input must be a JSON object under 1 MB. For a QUBO it looks like this: ```json { "Q": [[-3, 2, 0, 1, 0], [ 2,-2, 3, 0, 1], [ 0, 3,-4, 2, 0], [ 1, 0, 2,-1, 2], [ 0, 1, 0, 2,-3]], "labels": ["a", "b", "c", "d", "e"] } ``` An information icon next to **Your problem** states the workflow's limits, such as "Up to 24 binary variables per run". Inputs outside the limits are rejected before any credits are spent. ## Where to run Choose a machine under **Where to run**. Only machines the workflow supports appear: | Card | Use it for | |---|---| | **CPU simulator** | Exact and cheapest. Best for small problems | | **GPU simulator** | Faster on large circuits, costs more per run | | **IBM quantum computer** | Real hardware, priced by quantum seconds | Each card shows its availability: **Ready** (with the number in the queue), **Starts on demand** or **No machine idle**. A card that is unavailable says why and cannot be selected. For the IBM quantum computer, pick a **Device** from the chips or type another name under **Other device**. [Machines and targets](/docs/use/machines-and-targets) says when to use which. ## Solver settings If the workflow has settings, **Solver settings** shows **Tuned defaults**. Select **Adjust** to open them. Each workflow declares its own, with limits and a description behind the information icon. For `generic-qubo` and `max-cut`: | Setting | Default | Range | Effect | |---|---|---|---| | Circuit depth (`layers`) | 3 | 1 to 8 | More layers can find better answers but take longer to train | | Final shots | 4,000 | 100 to 20,000 | Shots for the final measurement. More shots cost more | | Shots per training step | 1,000 | 100 to 8,000 | Used when training samples the circuit | | Optimiser iterations | 100 | 5 to 400 | Steps of the classical optimiser | When you change a setting the summary reads "N changed", and **Reset to defaults** restores them. The workflow's author chose the defaults, and the track record was measured with them, so changing them takes you away from the numbers on the workflow page. ## Run summary and spend limit The **Run summary** box repeats the target and shows the workflow's **Typical cost**. The **Spend limit** is the most you will pay for this run, between 0.0001 and 1,000 credits. Qsolve suggests ten times the typical cost, rounded up and at least 1 credit, or 5 credits if the workflow has no cost history. The box also shows your balance and a link to **Usage**. After sizing, Qsolve shows an exact quote. If the run's hold is within your limit it starts. If not, you decide. See [Credits and pricing](/docs/use/credits-and-pricing). ## After you press Run The run page opens. A progress strip shows **Sized**, **Quoted**, **Queued**, **Running** and **Settled** (or **Failed** or **Stopped**). - **Needs your OK.** The hold is above your spend limit. The page shows the quote, the amount held while running and your limit. Choose **Run for up to X credits** to accept or **Cancel**. An unconfirmed quote expires after 15 minutes. - **Stop.** A run that has not started ends at once and nothing is charged. A running run is stopped and billed for what it used. - **Run again.** Reopens the workflow with the same input, settings and target. It runs the latest published version, which may differ from the one you ran before. See [Reproducibility](/docs/use/reproducibility). - **Report.** Downloads the run and its events as a JSON file. Your balance and recent runs are on **My runs** and **Usage**. --- # Reading your results > A guide to the Qsolve run page: the answer, the verdict against the classical baseline, fidelity and shots kept, circuits, cost, receipt and failed runs. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/reading-your-results · Updated: 2026-11-12 The run page answers three questions in order: what is the answer, how does it compare with the best classical method, and how much can you trust the quantum part. Read the verdict first, then the quality card, then the circuits if you need to check the work. ## The header The title is the workflow, with its family, version, target and run time. A status pill shows the state: **Sizing**, **Needs your OK**, **Queued**, **Running**, **Done**, **Failed** or **Stopped**. Three buttons sit at the top right: **Stop** (while the run is in progress), **Report** (downloads the run and its events as JSON) and **Run again**. ## Answer The **Answer** card starts with a one-sentence summary written by the workflow, then the answer in the workflow's own terms. Single figures appear as large numbers, lists of names as chips and lists of records as tables. | Workflow | Answer | |---|---| | `max-cut` | Group A, Group B, the weight of the cut and the total edge weight | | `generic-qubo` | The 0 or 1 assigned to each variable, the variables set to 1 and the objective value | | `portfolio-optimization` | The selected assets, their weights, expected return, risk and whether the constraint was satisfied | | `molecular-ground-state` | The energy in Hartree, kcal/mol, kJ/mol and eV with its error bar, the error against the exact reference, and whether chemical accuracy (1.6 mHa) is met | | `quantum-kernel-classifier` | Test accuracy with a 95% interval, the best classical test accuracy, cross-validation accuracy and a confusion matrix | ## Quantum vs classical This card is the verdict. It names the classical baseline and shows both objective values on one axis. | Verdict | Meaning | Colour | |---|---|---| | Matched the classical answer | The two values are equal | Green | | x% better than classical | The quantum value is lower (better) than the baseline's. Possible when the baseline is a heuristic | Green | | x% from the classical answer, under 1% | Slightly worse than the baseline | Amber | | x% from the classical answer, 1% or more | Clearly worse than the baseline | Red | The percentage is the quantum objective minus the baseline, divided by the baseline's magnitude. A lower objective is better. An information icon beside the classical value says how sure that value is: exact, a heuristic, or a sampled estimate with an error bar. In the **My runs** list the same comparison appears in the **Result** column as "Matched classical", "x% better" or "x% off". For chemistry the objective is an energy, a large number, so a gap of a few thousandths of a percent can still be several millihartree. Read the error in mHa as well: 1.6 mHa is chemical accuracy. For the classifier the objective is the test error rate, and a small test set gives a wide interval. ## Quality of the quantum run This card appears when the run sent circuits to a noisy target or reported mitigation figures. - **Fidelity.** How close the quantum output is to the ideal noise-free output. The label shows where the number comes from: "Fidelity (measured)" when no mitigation was reported, or "Fidelity after mitigation (reported)" with the raw measured figure beside it. A note gives the best fidelity shot noise alone allows. - **Shots kept.** The share of shots that passed the workflow's error-detection checks. - **Circuit.** Depth, number of two-qubit gates and qubits. The workflow page's **On quantum hardware** table shows real-device runs. They are added only when the workflow's developer chooses to run them (from their own credits), so a workflow can have an excellent simulator record and no device results yet. Each row is marked with its workflow version when it comes from an earlier one. A line below the figures says what was measured on: real hardware, a device noise model or an ideal simulator. On an ideal simulator there is no noise to mitigate, so these figures say little about hardware. [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise) explains why a correct answer can come with low fidelity. ## Error mitigation, step by step For chemistry workflows on a noisy target, a table shows the error against the exact answer in millihartree after each step, all from the same measured shots: raw, after readout and electron-count correction (with the share of shots kept), and after the final correction (zero-noise extrapolation or similar, or "not applied" with the reason), plus the error bar. ## Circuits Every distinct circuit the run sent is listed with its qubits, depth, two-qubit gates, how many times it was sent and with how many shots. Select a row to draw it. On hardware, a switch shows the circuit "As written" or "As run on the chip". See [Run records](/docs/use/run-records). ## The side column | Card | Contents | |---|---| | **Cost** | Credits charged (or quoted, while running), your limit, the quote, the amount held and the amount returned | | **Execution** | Target, device, qubits, circuits, shots, quantum seconds (hardware), expected time, wall time and seed | | **Receipt** | Workflow and version, code commit, SDK version, and hashes of the input and the result | The receipt lets you check a run later. See [Reproducibility](/docs/use/reproducibility). ## When a run fails A failed or stopped run shows the reason in a red card, for example "The run hit the workflow's time limit and was stopped" or "Your balance is too low for this run. Nothing was charged." **Technical log** expands to the end of the workflow's error output. **Edit input and retry** reopens the workflow with the same input. The **Cost** card shows what you were charged; [Credits and pricing](/docs/use/credits-and-pricing) lists the rules. One failure is specific to Qsolve's measuring. If a run used more quantum work than its result accounts for, it is stopped as an integrity mismatch, because its numbers cannot be trusted. You pay only for what ran. --- # Accuracy, fidelity and noise > What Qsolve's accuracy, fidelity and shots-kept numbers mean, why correct hardware answers can have noisy distributions, and what is measured versus reported. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/accuracy-fidelity-and-noise · Updated: 2026-11-12 Accuracy says whether the answer is as good as the classical baseline's. Fidelity says how closely the quantum output matches a perfect, noise-free machine. They measure different things, so a run on real hardware can have a correct answer and a low fidelity at once. ## Accuracy Accuracy compares the quantum method's result with the baseline's on the same input. 100% means they are equal. Each workflow declares which of three definitions applies and states it in a note on its page. | Kind | Used by | How it is computed | |---|---|---| | Relative gap | `generic-qubo`, `max-cut`, `portfolio-optimization` | 100% minus how much worse the quantum objective is, as a fraction of the baseline's | | Relative error | `molecular-ground-state`, `interaction-energy`, `electrolyte-solvent-screen` | 1 minus the energy difference to the exact active-space answer, as a fraction of that answer, with an error bar | | Error rate | `quantum-kernel-classifier` | The share of held-out test points classified correctly, with a 95% interval | A result **matches** the baseline when any one of these holds: the values are equal; the difference is within a tolerance the workflow declared (`molecular-ground-state` and `interaction-energy` declare 1.6 mHa, chemical accuracy); or the difference is statistically indistinguishable from zero given the result's error bar. The verdict reads either "Matches the best classical answer" or "Below the best classical answer". Chemistry accuracy figures look very high because total energies are large numbers: an error of a few millihartree is a few thousandths of a percent. Read the error in mHa and compare it with 1.6. ## Fidelity Fidelity runs from 0% to 100% and says how close the measured output distribution is to the ideal one. Qsolve computes the **raw fidelity** itself. It simulates each circuit noise-free (up to 20 qubits) and compares the ideal distribution with the counts that came back, weighted by shots and covering only the circuits that compute something, not calibration or mirror circuits. If some circuits are too large to score, the share of shots that could be scored is recorded as coverage. Two companion figures sit beside it: - **Fidelity floor.** Even a perfect machine only samples, so its measured fidelity is a little under 100%. The floor is the best score possible at your shot count. A result at the floor is as good as it can be. - **Shots kept.** The share of shots that passed the workflow's validity checks. Discarding invalid shots is called post-selection. For a portfolio problem that must pick exactly 4 assets, a shot with 5 assets selected is invalid. A workflow usually applies **error mitigation** to the counts: readout correction, post-selection and, in the chemistry workflows, zero-noise extrapolation. The mitigation steps are the workflow's own. Qsolve recomputes the resulting fidelity against the ideal output, but the run page still labels it "Fidelity after mitigation (reported)" and shows the raw figure beside it. ## Why a correct answer can come with a noisy distribution The QAOA workflows (`generic-qubo`, `max-cut`, `portfolio-optimization`) measure a circuit many times and take the best outcome among the samples. An ideal circuit puts most of its probability on good solutions. A noisy chip spreads probability onto wrong outcomes too. The whole distribution ends up far from the ideal one, so fidelity is low, yet the best outcome that appeared can still be optimal, and accuracy against the baseline is 100%. In an illustrative case, if the correct answer has 40% probability on a perfect machine and 3% on a noisy one, fidelity drops a lot, but with 4,000 shots it still appears about 120 times and is still the one returned. Chemistry is the opposite. VQE reads the energy as an average over the whole distribution, so noise moves the energy directly. Error mitigation does most of the work there, and the step-by-step table on the run page shows what each step recovers. Chemistry results on hardware can miss chemical accuracy even after mitigation. For sampling workflows, accuracy is the number that matters and fidelity tells you how fragile it was. For averaging workflows both matter, and fidelity predicts the error. ## Where the noise came from Every fidelity figure is labelled with the kind of machine behind it: | Label | Meaning | |---|---| | Real hardware | Measured on a physical quantum processor | | Noise-model estimate | A simulator loaded with an IBM device's published error rates, run through the same circuits and mitigation. Real devices can be noisier than their models | | Ideal simulator | Noise-free, so there is nothing to mitigate and fidelity says little | Only simulator results count towards a workflow's headline accuracy. Hardware results get their own table, because on today's devices they measure the device as much as the workflow. ## Measured or reported Qsolve computes accuracy, the match verdict, the error bar, qubits, depth, two-qubit gates, circuits, shots and raw fidelity itself. It re-scores each method's solution with the workflow's scoring function, and where a method returns no solution to re-score (a measured energy, say) the value is labelled "Reported by the workflow". Mitigated fidelity, shots kept and mitigation steps are reported by the workflow, and Qsolve recomputes the fidelity. The workflow supplies the text that describes each method and how accuracy is defined. It does not write the numbers. As a further check, Qsolve compares the shots and circuits it metered with those the result accounts for, and a mismatch stops the run as an integrity failure. --- # Machines and targets > The three Qsolve targets (CPU simulator, GPU simulator, quantum computer), when to use each, what they cost, and how to read availability on the Compute page. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/machines-and-targets · Updated: 2026-11-12 Start on the CPU simulator. Use the GPU simulator when a circuit is too large for the CPU to finish in reasonable time. Use a quantum computer when you want to see how a workflow behaves on real hardware noise, and you accept a higher price, a wait in the queue and a noisier answer. A **target** is the machine a run's quantum circuits execute on. ## The three targets | | CPU simulator | GPU simulator | Quantum computer | |---|---|---|---| | What it is | A noiseless quantum simulator on processors | A noiseless quantum simulator on graphics hardware | A real IBM quantum processor | | Noise | None | None | Real hardware noise | | Result | Exact up to sampling | Exact up to sampling | Noisy, mitigated by the workflow | | Price | Lowest | Higher per run | By quantum seconds, the highest | | Repeatable | Yes, with the same seed | Meant to be, with the same seed | No, devices drift | | Best for | Small and medium circuits, first runs, checking your input | Large circuits | Seeing real-device behaviour | | Hold | Equal to the quote | Equal to the quote | 1.5 times the quote | Only the targets a workflow supports appear on its page. All Qsolve's own workflows list CPU, GPU and IBM, but an outside developer's workflow may support fewer. ## CPU simulator The default and cheapest option. It simulates your circuits exactly, so you see the algorithm's behaviour without hardware noise. It is a good first check that the workflow handles your input and matches the baseline, and the headline accuracy on each workflow page is measured on it. Simulation cost grows quickly with qubits. A workflow's page states its largest problem, for example 24 qubits for the optimisation workflows and 12 for each chemistry system. ## GPU simulator The same noiseless simulation on graphics hardware, for larger circuits. It costs more per run, so it pays off only when the CPU run would be slow. If a run is predicted to exceed the workflow's time limit, switching target will not fix it. Reduce the problem or the settings that drive run time instead. ## Quantum computer The card reads **IBM quantum computer**. Pick a **Device** from the chips (the form lists `ibm_fez`, `ibm_marrakesh`, `ibm_kingston` and `ibm_torino`) or type another device name. Expect these differences from a simulator: - **Noise.** Results are noisier. Workflows apply error mitigation, and the run page shows raw and mitigated fidelity. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). - **Smaller circuits.** A workflow may reduce its circuits on hardware to keep them faithful. `generic-qubo`, for example, uses at most 2 layers on a quantum computer even if you ask for more. - **Queueing.** Your jobs wait in the provider's queue. That wait does not count against the workflow's compute time limit, though it is bounded. If a run ends without its results, its queued provider jobs are cancelled, so you are not billed for results nobody reads. - **No exact repeat.** Two runs of the same input can differ. - **Cost.** Priced by quantum seconds, with a hold of 1.5 times the quote. See [Credits and pricing](/docs/use/credits-and-pricing). Some workflows fall below their classical baseline on hardware. That is a property of today's devices, and the **On quantum hardware** table on the workflow page shows what to expect. ## The Compute page **Machines** lists each target with a status and two counts: **Machines** (how many are connected) and **Waiting** (jobs in the queue). It refreshes every 10 seconds. | Status | Meaning | |---|---| | Idle and ready | A machine is free. Your run starts as soon as it is sized | | Starts on demand | No machine is running, but one starts when you submit. Allow a few minutes | | No machine idle | Machines are busy. Your run waits in the queue | | Coming soon | The target is not open yet and cannot be selected | The quantum computer may not be open to every account yet. If its card is greyed out with "Coming soon" or "Not connected on this platform yet", use a simulator. --- # Credits and pricing > How Qsolve prices a run: the quote and hold formulas, how charges and returns are settled, spend limits and monthly budgets, with worked examples in credits. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/credits-and-pricing · Updated: 2026-11-12 One credit is one US dollar. Before a run starts you see a **quote** (the expected cost). While it executes, Qsolve **holds** its worst-case price from your balance. When it ends you are **charged** for what it used and the rest is **returned**. A per-run **spend limit** and an optional **monthly budget** cap what you can spend. ``` balance 100.00 quote ........ 5.46 expected cost, shown before anything happens hold ......... 8.20 taken from the balance when the run is queued balance 91.80 while the run executes charge ....... 5.12 what the run actually used returned ..... 3.08 hold minus charge balance 94.88 after the run settles ``` This is the single-circuit IBM example worked out below. A run whose hold is more than your balance fails before it starts, with nothing held. ## Free credits Each new account receives 100 credits once its email is confirmed, one grant per person (Gmail dots and `+tags` do not create a new grant). The web app has no purchase screen. If you need more credits, ask the Qsolve team. ## How a quote is computed Qsolve asks the workflow how many qubits, circuits and shots your input needs on the chosen target, then prices that. Rates can change, so treat the figures below as the current defaults and the quote on screen as the authority. ### Simulators ``` cost = per_run × complexity factor + per_million_shots × shots / 1,000,000 ``` | Target | `per_run` | `per_million_shots` | |---|---|---| | CPU simulator | 0.01 | 0.05 | | GPU simulator | 0.05 | 0.10 | The **complexity factor** reflects how heavy the workflow's classical computation is, as the workflow declares it: 1 for low, 2 for medium, 5 for high. On simulators the hold equals the quote. ### IBM quantum computer ``` quantum seconds = 0.015 × circuits + 0.00025 × shots cost = 3.84 + 1.60 × quantum seconds ``` Every quantum-computer job pays a fixed 3.84 credits (about 2.4 seconds of machine time), then 1.60 credits per quantum second. Seconds are estimated at 15 milliseconds per circuit and 0.25 milliseconds per shot. The hold is 1.5 times the quote, because the provider meters the real quantum time and it can differ from the estimate. Quotes round up to six decimal places and are never below 0.0001 credits. The app shows small amounts rounded, for example 0.0102 as 0.01. ## Worked examples These use the default rates and the circuits and shots shown. Your run's own estimate is on the run page under **Execution**. | Run | Calculation | Quote | Hold | |---|---|---|---| | CPU simulator, low complexity, 1 circuit, 4,000 shots | 0.01 + 0.05 × 0.004 | 0.0102 | 0.0102 | | IBM, 1 circuit, 4,000 shots | 3.84 + 1.60 × (0.015 + 0.00025 × 4,000 = 1.015 s) | 5.46 | 8.20 | A successful simulator run is charged the quote. A quantum computer run is charged 3.84 plus 1.60 for each quantum second the provider reports. If the IBM run above used 0.8 quantum seconds, it costs 3.84 + 1.60 × 0.8 = 5.12 credits, and 8.20 − 5.12 = 3.08 credits return to you. ## Spend limit The **Spend limit** on the run form is the most you will pay for one run, between 0.0001 and 1,000 credits. The form suggests ten times the workflow's typical cost. Qsolve compares it with the **hold**, not the quote: - Hold within your limit: the credits are held and the run is queued. - Hold above your limit: the run waits in **Needs your OK** with nothing held. Choose **Run for up to X credits** (the hold, rounded up to the cent) or **Cancel**. If you do nothing it expires after 15 minutes and nothing is charged. With a limit of 5, the single-circuit IBM run above has a hold of 8.20, so it waits for your OK. The same run on the CPU simulator starts immediately. ## Other limits | Limit | Value | What happens | |---|---|---| | Per-run cap | 150 credits | A run with a hold above it is refused: "This run is larger than the per-run limit." Nothing is held | | Runs at once | 3 per account | A further run is refused until one finishes | | Monthly budget | None until the account owner sets one | See below | ## Monthly budget The account owner can set a **monthly cap** on **Usage** (**Set a monthly cap**, **Change cap**, **Remove**), from 0.01 to 1,000,000 credits per UTC calendar month. Other members cannot change it. Spend in a month counts what finished runs were charged plus what active runs hold. A run counts in the month it was created. A run is refused if the month's spend has already reached the cap, or if spend plus its hold would pass it: "Monthly budget reached: 15 of 20 credits used this month; this run needs 8.20." Nothing is held. ## Hardware cases of a workflow If you are the developer of a workflow that lists `hardware_cases`, the cases run from your own account. You see each price before anything starts, you confirm once, and the credits are held and settled like any quantum run. Your free credits can pay for the small cases; a larger case may need more credits than you have. The per-run cap also applies, so a case whose held amount would pass it cannot be run. See [Benchmarks](/docs/build/benchmarks). ## What you pay when a run does not succeed | Outcome | Charged | |---|---| | Fails before it starts: invalid input, low balance, over a cap, device unavailable, platform error | Nothing | | You press **Stop** before it starts | Nothing, and the hold is released | | You press **Stop** while it executes | What it already used | | Time limit, out of memory, or the workflow crashes | What it already used | | Needs more than its hold ("budget exhausted") | What it used; the run is stopped at the hold | | Integrity mismatch (it used more quantum work than its result accounts for) | What it used | The charge never exceeds the hold. A run is metered while it executes and stopped once its cost would pass the hold. --- # Run records > What Qsolve stores for every run, how to browse your run history, and how to read the evidence on a run page: receipt, circuits, events and credit movements. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/run-records · Updated: 2026-11-12 Every run leaves a record on your account: what you submitted, what came back, what it cost and the evidence of how it was produced. Browse them on **My runs** and open any one for the full detail. The record is how you audit a result later. ## Run history **My runs** lists your account's runs, newest first, up to 100 at a time. Each row shows the workflow, a short run ID, a status pill, the result against the baseline ("Matched classical", "x% better" or "x% off"), the target, run time, credits and start time. The page refreshes every few seconds while anything is in progress. The filter offers **All**, **In progress**, **Done** and **Failed** (which includes stopped runs). A summary line above it gives the number of runs, credits spent and how many are in progress. Everyone in the same account sees the same runs. ## What is stored for each run Each record holds your submission (input, settings, target, device, seed, spend limit), status and timing, the workflow's estimate and your quote, the result (answer, every method's objective, comparison with the baseline, accuracy, fidelity and circuit figures), cost, the progress log, the circuits sent, the receipt and, if the run failed, the error message with the tail of the workflow's error output. **Report** on the run page downloads the run and its events as a JSON file. ## The receipt The **Receipt** card in the side column records exactly what produced the result. | Field | Meaning | |---|---| | Workflow | The workflow name and version | | Code | The commit of the workflow's repository that ran. Select it to copy | | SDK | The version of the Qsolve SDK that executed the workflow | | Input hash | A fingerprint of the input | | Result hash | A fingerprint of the result | | Run ID | The run's identifier | The full provenance also records the target and device, usage (shots, circuits, quantum seconds), the hardware queue wait, and on hardware a snapshot of the device's calibration. It also holds an integrity check: the shots the workflow's result reports against the shots Qsolve metered. If they disagree, the run is stopped. A **Benchmark run** badge marks the platform's own benchmark runs. Every signed-in user can read these, because their receipts are the evidence behind a workflow's track record. Your own runs are visible only to your account. ## Evidence for the quantum part ### Circuits The **Circuits** card lists every distinct circuit the run sent: | Column | Meaning | |---|---| | # | Order in which the circuit was first sent | | Qubits, Depth, Two-qubit | Size of the circuit as written | | On the chip | On hardware, the two-qubit gate count after the circuit was rewritten for the chip, and which physical qubits it used | | Sent × shots | How many times it was sent, and with how many shots each time | Select a row to draw it. For hardware runs a switch shows **As written** or **As run on the chip**; the chip version is usually longer because the circuit was rewritten (transpiled) into the processor's gates and mapped onto its physical qubits. Qsolve records circuits as they are sent, so this is the circuit that actually reached the machine, not the workflow's description of it. Up to 2,000 distinct circuits are summarised per run, and the full gate list is kept for only the first eight; the rest show "summary only". ### Solver details **How the solver got there** shows the settings and numbers the solver reported, and **Everything the solver reported** expands to the raw JSON. These are the workflow's own statements, unlike the measured figures elsewhere on the page. ## Retention Progress events are deleted after 30 days. Ninety days after a run finishes, its input, solver settings and result are cleared, while status, cost, timings and receipt remain. A cleared run cannot be used to rerun the same input. See [Data and privacy](/docs/use/data-and-privacy). --- # Reproducibility > How to reproduce a Qsolve run: the seed, workflow version and settings that fix a simulator result, what the receipt lets you compare, why hardware differs. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/reproducibility · Updated: 2026-11-12 On a simulator, the same workflow version, input, solver settings and seed give the same result, and you can confirm it by comparing the result hash on the receipt. On a quantum computer, repeats differ, because the device is noisy and drifts between runs. The web form always runs the latest published version with the default seed, so a rerun matches an earlier run only while the workflow has not been updated. ## What fixes a result | Ingredient | Where to find it | |---|---| | Workflow version | Receipt: **Workflow** (for example `max-cut v1.0.1`) | | Code commit | Receipt: **Code** | | SDK version | Receipt: **SDK** | | Input | **What you submitted**, and the **Input hash** on the receipt | | Solver settings | **What you submitted**, under **Solver settings** | | Target and device | Header, and **Execution** | | Seed | **Execution**: **Seed** | Published workflow versions never change, so a version number plus a commit identifies the code exactly. ## Seeds Every run is seeded, and a workflow must derive all its randomness (optimiser starting points, sampling, data splits) from that seed. Qsolve checks this when it validates a workflow by running the first benchmark case twice with the same seed and requiring identical result hashes. The seed of any run is shown under **Execution**. ## Check a result with the receipt The receipt holds two fingerprints: - **Input hash.** Two runs with the same input hash were given the same input. - **Result hash.** If two runs have the same workflow version, input hash, settings, target and seed, and the same result hash, the result reproduced exactly. Select a hash to copy it. ## Rerun a run Press **Run again** on a run page. The workflow page opens with that run's input, solver settings and target (and device), and confirms "Loaded the input from your earlier run." Press **Run**. The new run uses the latest published version with the default seed. If the workflow has been updated, the result may not match. The receipt shows which version the old run used. Rerunning needs the original input. It is cleared 90 days after a run finishes, after which **Run again** cannot reload it. Download the run with **Report** if you need to keep it. See [Data and privacy](/docs/use/data-and-privacy). ## Why hardware runs differ A quantum computer does not return the same counts twice. Noise is random, and the device's calibration (its measured error rates) shifts over hours. For a hardware run Qsolve records the device and a snapshot of its calibration, so you can see the conditions, but it cannot make two runs equal. Compare quality numbers instead of exact output: raw and mitigated fidelity, shots kept, and accuracy against the baseline. A stable workflow gives similar values across repeats. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ## Running a workflow locally Workflow source is open, and the SDK runs the same code the platform runs. To reproduce a run offline, see [Build overview](/docs/build/overview) and [CLI](/docs/build/cli). A local simulator run matches only with the same workflow version, SDK version and input, and the platform's result remains the reference record. --- # Data and privacy > What data you give Qsolve when you run a workflow, what is stored and for how long, who can see it, and what a workflow's code can do with your input. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/data-and-privacy · Updated: 2026-11-12 When you run a workflow, your input is stored in the run record, visible to the members of your account. The workflow's code runs with no network access, so it cannot send your input anywhere. If you run on a quantum computer, the circuits built from your input go to the hardware provider. ## What you submit Your input (the form or JSON editor), the solver settings, and the target, device and spend limit. At sign-up you give an email, a display name and a password, or you use your GitHub identity. Input is checked against the workflow's input schema and rejected if it does not match. ## What is stored Every run keeps the record described in [Run records](/docs/use/run-records): input and settings, result, cost, progress events, circuits and receipt. Your account also keeps your profile, credit history and monthly budget. Two things you might not expect: - **Circuits encode your problem.** The circuits a workflow sends are built from your input. For a QUBO, the matrix entries typically appear as rotation angles. Qsolve records these circuits so you can inspect them, which means your problem is stored there in another form. - **Workflows can write to the log.** A workflow may emit progress events, and if it fails, the tail of its error output is stored and shown under **Technical log**. If a workflow prints part of your input in an error message, that text is stored. ## Who can see it | Data | Who can read it | |---|---| | Your runs, inputs, results, events, circuits and credit history | The members of your account | | Benchmark runs | Every signed-in user. These are the platform's own runs on its own benchmark inputs, and their receipts are the evidence behind a track record | | Workflow listings, benchmark results and costs | Anyone who can open the platform | A workflow's author does not receive your runs. The author publishes code, and Qsolve runs that code. ## What a workflow's code can and cannot do A workflow is untrusted code written by someone else. It runs on Qsolve's machines with no network access and no access to the platform's files, and it is stopped if it exceeds its memory, CPU or time limits. Provider credentials for quantum hardware are never available to it. Code that imports modules outside a fixed set fails validation before it is published. Your input does enter that environment, because the workflow must read it to work. It can compute with it, as intended, but it cannot send it out. Every circuit the workflow sends passes through Qsolve, which counts shots and circuits and keeps the record you see on the run page. A workflow cannot hide quantum work: if its result accounts for fewer shots than were metered, the run is stopped as an integrity failure. ## What happens on a quantum computer On a simulator, your circuits run on Qsolve's machines. On a quantum computer they are sent to IBM through Qsolve's own account with IBM, and the counts come back. Because the circuits encode your problem, a hardware run shares your problem with the provider. If that is not acceptable, use a simulator. ## How long data is kept | Data | Kept | |---|---| | Progress events | 30 days | | Input, solver settings and result of a finished run | Cleared 90 days after the run finishes | | Status, cost, timings, receipt and credit history | Kept | These periods can change. Use **Report** on a run page to download the run, with its input and events, if you need it longer. A cleared run still appears in **My runs**, but **Run again** can no longer reload its input. --- # Qsolve for enterprises > How an enterprise team can evaluate a Qsolve workflow, control spend with limits and budgets, audit runs, and what its results can and cannot be relied on for. Section: Using Qsolve · Source: https://qsolve.omlabs.co/docs/use/for-enterprises · Updated: 2026-11-12 Qsolve gives an enterprise team three things it can check: a measured track record for each workflow against a classical baseline, a price quoted before every run with hard caps on spend, and a record of every run for later audit. It does not promise a quantum advantage, and it does not guarantee that a workflow will perform on your data as it did on its benchmarks. ## Evaluate a workflow before you adopt it Work through these steps in order, cheapest first. 1. **Check the fit.** On the workflow page, **Limits** gives the largest problem it accepts and which machines it runs on. If your real problems are bigger, the workflow cannot take them as they are. The optimisation workflows stop at 24 variables, so a larger problem has to be split first. 2. **Read the track record.** Look at the benchmark cases nearest your problem's size and type, including the amber and red squares. See [Choosing a workflow](/docs/use/choosing-a-workflow). 3. **Open benchmark rows.** Each shows every method run on that input side by side, the classical baseline by name, the quantum answer, the mitigation figures and the circuit. You can open the benchmark run itself and read its receipt. 4. **Judge the baseline.** An exhaustive search or an exact solver is a strong baseline. A heuristic is weaker, and Qsolve cannot prove any baseline is the best classical method. If you have a classical method you trust, run it on the same input and compare. 5. **Run your own representative input on the CPU simulator.** It is the cheapest test and shows how the workflow behaves on data from your domain, which the track record cannot. 6. **Check hardware separately.** If you plan to use a quantum computer, read the **Noise-model estimate** and **On quantum hardware** tables and run a small case on the machine. Hardware results do not count towards the headline accuracy because they measure the device as much as the workflow. See [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). ## Control cost Each run shows a quote before credits are taken. The submitter sets a **Spend limit** per run, above which the run waits for approval. Qsolve refuses any run above a per-run cap, and the account owner can set a **monthly budget** per UTC month, which refuses runs that would pass it. Monthly spend counts charged credits plus what active runs hold, so concurrent runs cannot overshoot the budget. A run never costs more than its hold. The cap values, concurrency limit and worked examples are in [Credits and pricing](/docs/use/credits-and-pricing). **Usage** shows the balance, spend against the monthly cap, spend per month for the past year, spend by workflow and by target, and every credit movement, which makes it a ready view for budget reviews. ## Accounts and teams Credits and runs belong to an **account**, which is personal or an organisation. A member's role is **owner** or **member**. Everyone in an account shares its balance, monthly budget and run history, and only the owner can set the monthly budget. The **Account** page shows the account's name, plan, your role, status (Active or Suspended), balance and monthly use. A suspended account cannot start runs. You cannot create an organisation account or invite members yourself. If your team needs a shared account, ask the Qsolve team. ## Audit a run Each run keeps the exact input and settings (with an input hash), the workflow version, code commit and SDK version, the target and device (plus a calibration snapshot on hardware), every distinct circuit sent, the quote, hold, charge and return, and a result hash. **Report** downloads all of it. See [Run records](/docs/use/run-records). Records are not permanent: progress events are deleted after 30 days, and 90 days after a run finishes its input and result are cleared. Download reports if you need a longer audit trail. See [Data and privacy](/docs/use/data-and-privacy). ## What you cannot rely on | Limit | Detail | |---|---| | Quantum advantage | At these sizes classical baselines can often find the exact answer. The best a quantum method can do is match it | | The baseline being the best classical method | Qsolve shows the baseline and other classical methods that were run, and does not prove they are the best | | Mitigated figures | They are the workflow's own, labelled "reported", beside Qsolve's measured raw figures | | Benchmarks predicting your data | The track record describes benchmark cases only | | Simulator accuracy predicting hardware accuracy | Simulators are noiseless | | Hardware reproducibility | Real-device runs vary with the device's state at the time | | Hardware availability | The quantum computer may not be open to every account and has queue waits. Check **Machines** | | Large problems | Each workflow caps problem size, for example 24 variables or 12 qubits | | Privacy on hardware | Circuits that encode your problem go to the hardware provider | A sensible pilot starts with simulator runs on your own representative problems, sets a monthly budget, reviews the receipts, and tries hardware only for the cases where it matters. --- # What a Qsolve workflow is > A Qsolve workflow is a plain Qiskit program in a public Git repository plus a qsolve.yaml manifest; the SDK bridges to Qsolve, which runs and measures it. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/overview · Updated: 2026-11-12 A Qsolve workflow is a Qiskit program in a GitHub repository (public, or private through the Qsolve GitHub app), plus a manifest called `qsolve.yaml` that says what the program takes in, what it returns and which methods it compares. You write the quantum algorithm, the error mitigation and the classical baseline in ordinary Qiskit. Qsolve runs the program, measures every number it publishes, and lists the workflow with its track record against the best classical method you declared. ## The three pieces | Piece | What it is | Who writes it | |---|---|---| | Workflow | A Git repository: Python code, `qsolve.yaml`, two JSON Schemas, benchmark cases | You | | `qsolve-sdk` | A Python package: the contract your class implements, a Qiskit backend and primitives, a local test harness and the `qsolve` command line | Qsolve | | Qsolve platform | Runs your workflow on simulators and quantum computers and publishes the result | Qsolve | The SDK contains no quantum algorithm and no solver. Everything algorithmic is your code, built on Qiskit, NumPy, SciPy, scikit-learn and PySCF. [Project layout](/docs/build/project-layout) lists every importable module. ## What a workflow looks like The workflow is one class that subclasses `qsolve.Workflow`. This is the scaffold `qsolve init` generates, trimmed to its signatures: ```python from qsolve import Context, Outcome, Workflow class Qubo(Workflow): def __init__(self, input, params): ... # parse the input; raise WorkflowError for mistakes def size(self) -> int: ... # qubits or variables def quantum(self, ctx: Context) -> Outcome: # plain Qiskit: build circuits, run them through ctx counts = ctx.sampler.run([(circuit, params, shots)]).result()[0].data.meas.get_counts() ... def reference(self, ctx) -> Outcome: ... # the classical answer you are judged against def classical_annealing(self, ctx) -> Outcome: ... # any other classical method you declare def evaluate(self, solution) -> float: ... # the objective of a solution, lower is better def answer(self, outcome) -> dict: ... # the domain answer people read ``` `qsolve.yaml` declares the methods and describes each in plain words; Qsolve shows that text next to the numbers it measures. See [Writing the workflow](/docs/build/writing-the-workflow) for the contract and [The manifest](/docs/build/manifest) for the file. ## What Qsolve checks, and what it cannot Workflow code is untrusted, so Qsolve measures instead of trusting. Circuits, shots, depth and fidelity come from what crosses the backend; accuracy comes from the baseline you declare with `role: reference`. In addition: - Each method's objective is recomputed with your `evaluate`. A method that claims a different number fails validation. - A quantum method that sends no circuit fails validation. A classical method that tries to send one is refused. - Your `estimate()` is compared with the circuits and shots the run actually sent. Qsolve cannot prove that your classical baseline is the best one, or that your error mitigation is legitimate. Mitigated figures are labelled reported and appear beside the measured raw figures, so a reader sees both. See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). See [Your first workflow](/docs/build/your-first-workflow) to build one, [The examples](/docs/build/examples) for seven published ones, and [Validation and publishing](/docs/build/validation-and-publishing) for every check. Importable modules are listed in [Project layout](/docs/build/project-layout). --- # Your first workflow > Build, test and submit a Qsolve workflow from scratch: install the SDK, scaffold with qsolve init, run, validate, lint, benchmark, push to GitHub and submit. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/your-first-workflow · Updated: 2026-11-13 `qsolve init` generates a passing workflow that solves a small QUBO with QAOA. Run it unchanged first, submit it, then swap in your own problem. Every command used here is specified in [The qsolve command line](/docs/build/cli), and every Python name in the [SDK reference](/docs/build/sdk-reference). ## 1. Install the SDK You need Python 3.12 or newer. The `ibm` extra installs the device noise models that `qsolve test` uses for its noise-model benchmark case, so install it even if you never run on hardware. ```bash pip install "qsolve-sdk[ibm]" qsolve --version qsolve --help ``` With the extra you can also run on a real device from your machine with `--target ibm:` and your own token in `QSOLVE_IBM_TOKEN`. Simulators and noise-model targets need no token. ## 2. Scaffold a workflow ```bash qsolve init depot-picker --summary "Pick the best depots" cd depot-picker ``` The name is 2 to 63 lowercase letters, digits and dashes, starting with a letter or digit. It becomes the workflow's identifier on Qsolve. The summary is written into `qsolve.yaml` unquoted, so avoid a colon followed by a space and ` #` in it. The command creates: ```text depot-picker/ qsolve.yaml README.md benchmarks/cases.yaml examples/qubo_5.json schemas/input.json schemas/output.json src/__init__.py src/workflow.py tests/test_workflow.py ``` `src/workflow.py` is about 120 lines of Qiskit: it builds a QAOA circuit, trains the angles on a local statevector simulator, samples the final circuit through `ctx.sampler` and compares the result with an exhaustive search. The class is a `qsolve.Workflow` subclass ([The Workflow class](/docs/build/sdk-workflow-class)). [Project layout](/docs/build/project-layout) explains each file. `tests/test_workflow.py` is a pytest file that runs the example through `qsolve.run_workflow` and asserts the quantum result matches the best classical answer; run it with `pytest`. ## 3. Validate the manifest and inputs ```bash qsolve validate . ``` ```text OK depot-picker v0.1.0 ``` `validate` loads `qsolve.yaml`, reads both schemas and checks every example input against the input schema. ## 4. Lint ```bash qsolve lint . ``` ```text WARNING W051 no LICENSE file found 0 error(s), 1 warning(s) ``` Lint is the static check the platform runs before it executes any of your code. The scaffold sets `license: Apache-2.0` but does not create the file, hence the warning: add a `LICENSE` file with the license text to clear it. Every code is explained in [Lint codes](/docs/build/lint-codes); [Troubleshooting](/docs/build/troubleshooting) covers common fixes. Errors exit 1, warnings do not. ## 5. Run it locally ```bash qsolve run . -i examples/qubo_5.json --seed 1 ``` ```text depot-picker v0.1.0 on aer-cpu ┌───────────────────────────────┬─────────────────────┐ │ Metric │ Value │ ├───────────────────────────────┼─────────────────────┤ │ Objective (quantum) │ -10 │ │ Objective (Exhaustive search) │ -10 │ │ Gap vs baseline │ 0.000% │ │ Uncertainty │ exact (stderr None) │ │ Shots │ 4000 │ │ Run time (s) │ 2.22 │ └───────────────────────────────┴─────────────────────┘ QAOA (quantum approximate optimisation): -10; Exhaustive search: -10 (accuracy 100.00%). ``` Override a parameter with `-p layers=1`; write the full JSON result with `--out result.json`. The file holds a `result` and a `provenance`, described in [Results and events](/docs/build/sdk-results-and-events). The same seed gives the same result every time. ## 6. Check the cost estimate ```bash qsolve estimate . -i examples/qubo_5.json ``` ```json { "qubits": 5, "circuits_on_target": 1, "shots_on_target": 4000, "classical_cost_class": "low", "runtime_s": null, "notes": [], "by_default": false } ``` Nothing runs. The gate `estimate_honest` fails if this is off by more than a factor of two from what a run sends. `qsolve estimate` has no `--param`, so it uses the manifest defaults. The numbers come from the `estimate()` method of your class ([Metering and cost](/docs/build/sdk-metering-and-cost#estimate)). ## 7. Run the gates and benchmarks ```bash qsolve test . ``` ```text Gates ┌──────────────────┬────────┬────────────────────────────────────────────┐ │ Gate │ Result │ Detail │ ├──────────────────┼────────┼────────────────────────────────────────────┤ │ manifest │ pass │ depot-picker v0.1.0 │ │ example_inputs │ pass │ 1 valid │ │ benchmark_cases │ pass │ 3 cases defined │ │ methods_declared │ pass │ 3 methods declared │ │ fidelity_checked │ pass │ a case sets min_fidelity on a noisy target │ │ cases_pass │ pass │ 3/3 passed │ │ reproducible │ pass │ identical result hash │ │ estimate_honest │ pass │ estimate within 2x of what was sent │ └──────────────────┴────────┴────────────────────────────────────────────┘ Cases on sim-cpu ┌────────────────────┬────────┬───────┬──────────┬──────────┐ │ Case │ Result │ Gap │ Fidelity │ Time (s) │ ├────────────────────┼────────┼───────┼──────────┼──────────┤ │ unconstrained-5 │ pass │ 0.00% │ 0.998 │ 1.5 │ │ dense-random-8 │ pass │ 0.00% │ 0.982 │ 0.4 │ │ noise-model-qubo-5 │ pass │ 0.00% │ 0.974 │ 10.1 │ └────────────────────┴────────┴───────┴──────────┴──────────┘ ALL GATES PASSED ``` These are the gates and cases the platform runs on submission. (`reproducible` and `estimate_honest` need at least one case that ran, so a workflow whose cases all fail shows six gates.) `qsolve test` does not lint, so run `qsolve lint .` as well. The third case runs on `fake:fez@1.6`, a device noise model with every error rate multiplied by 1.6. [Benchmarks](/docs/build/benchmarks) explains each gate. ## 8. Make it yours Change these in order, running `qsolve test .` after each step: 1. `schemas/input.json` and `examples/`: the input a user provides. 2. `__init__`, `size` and `evaluate` in `src/workflow.py`: parse the input and score a solution. Raise `WorkflowError` for a bad input and `InvalidSolution` for a solution that breaks a rule. 3. `quantum`: your circuits, run through `ctx.sampler`. See [Writing the workflow](/docs/build/writing-the-workflow). 4. `reference` and any `classical_` methods: the strongest classical method that is practical. 5. `qsolve.yaml`: method descriptions, metric and envelope ([The manifest](/docs/build/manifest)). 6. `benchmarks/cases.yaml`: cases with known answers, plus one noise-model case ([Benchmarks](/docs/build/benchmarks)). 7. `schemas/output.json` and `answer()`: what people read. Also update `tests/test_workflow.py` so `pytest` keeps passing. Bump `version` in `qsolve.yaml` whenever anything changes after a submission; see [Versioning](/docs/build/versioning). ## 9. Push to GitHub ```bash git init git add . git commit -m "First version" git branch -M main git remote add origin https://github.com//depot-picker.git git push -u origin main ``` The repository can be public, or private if you connect the Qsolve GitHub app ([Private repositories](/docs/build/private-repositories)). It needs `qsolve.yaml` at the root, no symbolic links or submodules, and must be under 50 MB. ## 10. Submit Sign in to Qsolve, open the Publish a workflow page, paste `https://github.com//depot-picker` into the repository field and press Submit. The branch, tag or commit field is optional and defaults to the default branch. Qsolve pins the exact commit it fetched. The submission moves from Waiting to Checking, then Published or Needs changes; open it to see lint findings and gates. A published workflow is runnable by its link at once and listed publicly once approved. See [Validation and publishing](/docs/build/validation-and-publishing). --- # Project layout > The files and folders of a Qsolve workflow repository, what each one is for, which are required, what is left out of the published snapshot and the size limits. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/project-layout · Updated: 2026-11-13 Qsolve reads `qsolve.yaml` at the root of the workflow, and the two schemas, the example inputs and the entrypoint module at the paths `qsolve.yaml` gives. `benchmarks/cases.yaml` is read at that fixed path. Everything else is yours. The workflow does not have to be the whole repository: when you submit, you can name a subfolder (for example `workflows/max-cut`), and that folder is then the root described here. It must be a relative path of letters, digits, `.`, `_`, `-` and `/`, never absolute, never `..`, never starting with a dot. If it holds no `qsolve.yaml` at the chosen commit, the submission is refused with `No qsolve.yaml in the folder '' of this repository at that commit.` ## The layout This is the published `max-cut` example, which every example follows: ```text max-cut/ qsolve.yaml required README.md recommended LICENSE recommended schemas/ input.json required output.json required src/ __init__.py keep it: the examples all have one workflow.py your Workflow class qubo/ your own modules (QAOA, classical solvers) examples/ ring_6.json required: at least one example input benchmarks/ cases.yaml required to publish inputs/ larger benchmark inputs tests/ your own tests, not published requirements.lock optional ``` | Path | Required | Purpose | |---|---|---| | `qsolve.yaml` | Yes | The manifest: identity, schemas, parameters, methods, limits. See [The manifest](/docs/build/manifest). | | `schemas/input.json` | Yes (path is set by `inputs.schema`) | JSON Schema (draft-07) for the input. Every input is validated against it and the input form is built from it. See [Input and output schemas](/docs/build/input-and-output-schemas). | | `schemas/output.json` | Yes (path is set by `outputs.schema`) | JSON Schema for the answer `answer()` returns. The answer is validated and rendered from the schema's titles and units. | | `src/` | Yes | Your code. The manifest's `entrypoint` points into it. The lint looks for the entrypoint's module both at the repository root and under `src/` (`src.workflow` is `src/workflow.py` or `src/workflow/__init__.py`); a missing module or class is `E030`. | | `examples/*.json` | At least one (`E024`) | Example inputs listed under `inputs.examples`; the file names and folder are your choice. Each is validated against the input schema and shown to researchers as a starting point. | | `benchmarks/cases.yaml` | To publish (`E050`) | Known-answer cases the platform runs and records. See [Benchmarks](/docs/build/benchmarks). | | `README.md` | Recommended | What the workflow solves and its limits. Lint warns (`W060`) when missing. | | `LICENSE` | Recommended | Lint warns (`W051`) when no `LICENSE`, `LICENSE.md`, `LICENSE.txt` or `COPYING` exists. The manifest's `license` field is required either way. | | `tests/` | No | Your own `pytest` tests. Never shipped or run on the platform, and exempt from the import check. | | `requirements.lock` | No | Documents your packages. It installs nothing; its only effect is that lint reports an import of a package listed there as `W042` instead of `E040`. | ## Entrypoint and package name The manifest's `entrypoint` is `package.module:ClassName`. The examples use a package called `src`, so the entrypoint is `src.workflow:MaxCut`. Keep `src/__init__.py` and import your own modules relatively: ```python from .qubo import Qubo, plan_qaoa, sample_qaoa from qsolve import Context, Estimate, InvalidSolution, Outcome, Workflow, WorkflowError ``` ## Importable modules Workflows run with no network and no package installs, and your own Dockerfile is ignored. Your code may import: - the Python standard library; - `qsolve`; - `numpy`, `scipy`, `sklearn`, `qiskit`, `qiskit_aer` and `pyscf`; - `pydantic`, `jsonschema`, `yaml`, `typer` and `rich`. Anything else is lint error `E040`. A package pinned in `requirements.lock` but unavailable produces warning `W042`, and the run rejects the import, so a workflow that needs another package cannot be published today. Files under `tests/` are exempt. ## Data files Ship the data your workflow needs inside the repository and read it from disk, as `electrolyte-solvent-screen` does with `src/geometries.json`. A run sees only the repository and a scratch directory. Data-preparation scripts that need other packages belong outside the repository or in a folder your code never imports; the examples keep them in a separate `tools/` folder. ## Left out of the published snapshot Publishing stores an immutable copy of the repository. These are skipped: - directories named `__pycache__`, `.git`, `.venv`, `.pytest_cache`, `.ruff_cache`, `.mypy_cache`, `node_modules` and `tests`, wherever they are in the tree, and any file or directory whose name starts with `~`; - `.pyc` and `.pyo` files, `.DS_Store`, `Thumbs.db` and `desktop.ini`. Everything else, including the README, the LICENSE and your example inputs, is in the snapshot, so changing any of it needs a new version, and changing only what is skipped (for example a test) does not. See [Versioning](/docs/build/versioning). ## Limits and rejected content | Rule | Limit | Lint code | |---|---|---| | Files in the repository | 500 | `E011` (the snapshot step refuses the same limit with `workflow too large`) | | Total size of the files | 20 MB | `E011` (same) | | Each schema file | 256 KB | `E022` | | Repository size as fetched | 50 MB | rejected at fetch | | Symbolic links | None | `E010` | | Git submodules | None | rejected at fetch | | Files that look like credentials (`.env`, `*.pem`, `*.key`, `id_rsa*`, `credentials*.json`, `.netrc`) | None | `E012` | | Text that looks like a committed secret (private keys, cloud and GitHub tokens, `sk_live_` style keys) | None | `E013` | The lint counts everything except `__pycache__`, `.git`, `.venv`, `.pytest_cache`, `.ruff_cache`, `.mypy_cache` and `node_modules`, so it counts `tests/`, while the snapshot step does not. The secret scan skips files over 1 MB and common binary types (`.png`, `.jpg`, `.gif`, `.pdf`, `.zip`, `.npy`, `.npz`). Never put a provider token in the repository; your code never needs one. Every lint code is in [Lint codes](/docs/build/lint-codes). --- # The manifest > Complete reference for qsolve.yaml: every field, its type, default, valid values and limits, the error each mistake raises, how the platform uses it, and annotated examples from published workflows. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/manifest · Updated: 2026-11-13 `qsolve.yaml` sits at the root of a workflow repository. It names the workflow, points to its schemas and class, declares its parameters and methods, and sets its size and resource limits. It is the only place these are declared, and the platform stores a copy of it with every published version. ## How the manifest is checked Three checks read it, in this order: 1. **Load** (`qsolve validate`, and the first thing every other command does). The file must be valid YAML and match the model below. Failure prints `Invalid workflow:` followed by the reason and exits 1. On submission and in `qsolve lint` the same failure is reported as lint error `E002` (`invalid workflow definition: ...`); a missing file is `E001`. 2. **Schema files.** The two schema paths are read. See [Input and output schemas](/docs/build/input-and-output-schemas). 3. **Lint and gates.** Further manifest rules (examples, license, hardware cases) are lint codes; see [Lint codes](/docs/build/lint-codes). Rules that apply at every level: - **Unknown keys are rejected**, in the top level and in every nested object. A typo such as `enveloppe:` fails the load with the validator's `Extra inputs are not permitted` message naming the key. - **Required keys that are missing** fail with `Field required`. Wrong types fail with a message naming the field; for example, `version: 1.0` is read by YAML as a number and rejected, so write `1.0.0`. - **Values are not coerced.** A string where a number is expected, or a number where a string is expected, is an error. The platform also re-validates the stored manifest every time it accepts a run, with the same code. ## A complete manifest This is the manifest of the `max-cut` example (version `1.0.2`), which uses every section. Long prose strings are shortened here; the full file is in `examples/max-cut/qsolve.yaml`. ```yaml schema_version: 1 name: max-cut version: 1.0.2 family: qubo_optimization summary: Split a weighted graph into two groups to maximize the total weight of edges between them (network partitioning, clustering, circuit layout) license: Apache-2.0 tags: [graph, partitioning, qubo, qaoa, logistics] inputs: schema: schemas/input.json examples: [examples/ring_6.json] parameters: - {name: layers, label: Circuit depth, type: int, default: 3, min: 1, max: 8, description: "More layers can find better answers but take longer to train"} - {name: shots, label: Final shots, type: int, default: 4000, min: 100, max: 20000, description: Final sampling shots} - {name: train_shots, label: Shots per training step, type: int, default: 1000, min: 100, max: 8000, description: Shots per optimizer step when training is sampled} - {name: optimizer_iters, label: Optimiser iterations, type: int, default: 100, min: 5, max: 400, description: COBYLA iterations} - {name: train_on, label: Train angles on, type: enum, default: sim, choices: [sim, backend], description: "sim trains noise-free and uses the device only to measure"} outputs: schema: schemas/output.json entrypoint: src.workflow:MaxCut category: Networks and logistics objective: {label: Negative cut weight, unit: "", decimals: 3} metric: {kind: relative_gap, note: "100% means the quantum objective equals the best classical objective. It falls by the relative amount the quantum objective is worse."} methods: - id: qaoa role: quantum title: QAOA (quantum approximate optimisation) what: A short quantum circuit that alternates between favouring low-cost answers and mixing possibilities. strength: Samples good answers directly from a quantum state. limit: On today's noisy chips only small circuits stay faithful. - id: exhaustive-or-annealing role: reference title: Exhaustive search (annealing beyond 22 variables) what: Tries every possible answer and keeps the best one. strength: Provably the best answer whenever it can run exhaustively. limit: Beyond 22 variables it is a heuristic with no guarantee. - id: annealing role: classical title: Simulated annealing what: Starts from a random answer and keeps trying small changes. strength: Fast, scales to large problems, and usually lands close to the best answer. limit: No guarantee that it finds the best answer. envelope: max_qubits: 24 max_variables: 24 notes: "Up to 24 nodes per run" targets: simulators: [cpu, gpu] qpu: [ibm] min_qubits_qpu: 4 resources: timeout_s: 1800 memory_mb: 1536 network: none runtime: python: "3.12" dependencies: requirements.lock ``` ## Top-level fields | Field | Type | Required | Default | Valid values and errors | |---|---|---|---|---| | `schema_version` | integer | Yes | | Exactly `1`. Any other value fails the load. | | `name` | string | Yes | | `^[a-z0-9][a-z0-9-]{1,62}$`: lowercase letters, digits and dashes, 2 to 63 characters, starting with a letter or digit. Error: `name must be lowercase letters, digits and dashes (2-63 chars)`. It becomes the workflow's address (its slug) and is its identity: a different `name` is a different workflow. A name already owned by another developer, or by a workflow you deleted, is refused at publish. See [Versioning](/docs/build/versioning). | | `version` | string | Yes | | `^\d+\.\d+\.\d+$`: three whole numbers, no pre-release suffix, no leading `v`. Error: `version must be semver, e.g. 0.1.0`. | | `family` | string | Yes | | Any string; the platform does not check it against a list and accepts an empty one. Use a stable snake_case name such as `qubo_optimization`, `electronic_structure` or `quantum_ml`. It is shown in the listing and used for filtering, and it is refreshed from the newest version at every publish. | | `summary` | string | Yes | | Any string, no length limit in the model. One sentence on what the workflow solves; shown in the listing and refreshed at every publish. | | `license` | string | Yes | | Any string, but blank or whitespace-only is lint error `E051`. Also add a `LICENSE` file, or lint warns `W051`. | | `tags` | list of strings | No | `[]` | Free-form search tags. No limit on count or length is enforced. Refreshed at every publish. | | `inputs` | object | Yes | | See [Inputs and outputs](#inputs-and-outputs). | | `parameters` | list | No | `[]` | See [Parameters](#parameters). | | `outputs` | object | Yes | | See [Inputs and outputs](#inputs-and-outputs). | | `entrypoint` | string | Yes | | `^[A-Za-z_][\w.]*:[A-Za-z_]\w*$`, that is `package.module:ClassName`. Error: `entrypoint must look like 'package.module:ClassName'`. Lint `E030` if the module or class is not found, `E031` on a syntax error. | | `category` | string | No | `""` | At most 60 characters. The field the workflow belongs to as a person would say it, for example `Finance`. Shown in the marketplace. | | `objective` | object | No | `{label: Objective, unit: "", decimals: 4}` | See [`objective`](#objective). | | `metric` | object | No | `{kind: relative_gap, ...}` | See [`metric`](#metric). | | `methods` | list | Yes | | At least 2 entries; see [Methods](#methods). | | `envelope` | object | Yes | | The largest problem the workflow accepts; see [`envelope`](#envelope). | | `targets` | object | No | `{simulators: [cpu], qpu: []}` | See [`targets`](#targets). | | `resources` | object | No | see below | See [`resources`](#resources). | | `runtime` | object | No | see below | See [`runtime`](#runtime). | ### Things that are not manifest keys Because unknown keys are rejected, these will fail the load if you add them: - **Visibility, first-party status, ownership.** The platform sets them: a new developer workflow starts `unlisted` and becomes public when an admin approves it (or when auto-publish is on). There is nothing to declare. - **Pricing and cost hints.** There is no price field. The quote for a run comes from what your `estimate()` returns and the platform's rate card; see [Credits and pricing](/docs/use/credits-and-pricing) and [Writing the workflow](/docs/build/writing-the-workflow). - **Error mitigation.** Mitigation is code you write, and you report its result at run time with `ctx.report_mitigation(...)`. There is no manifest switch; see [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). - **Benchmark cases and hardware cases.** They live in `benchmarks/cases.yaml`; see [Benchmarks](/docs/build/benchmarks). ## Inputs and outputs Both have the same shape: | Field | Type | Required | Default | Meaning | |---|---|---|---|---| | `schema` | string | Yes | | Path to a JSON file inside the repository, relative to the repository root. A path that resolves outside the repository fails with `schema path escapes workflow directory: `. A missing file fails with `schema file not found: `. A file that is not a JSON object fails with ` must contain a JSON object`. | | `examples` | list of strings | No | `[]` | Paths to JSON example files. | For `inputs`, at least one example is required (lint `E024`), and each must read and validate against the input schema (lint `E023`). The example files are copied into the version's record and shown to researchers as starting points. For `outputs`, `examples` is accepted but not used. The schema files themselves are checked by lint `E020` to `E022` (see [Input and output schemas](/docs/build/input-and-output-schemas)). ## Parameters Each entry declares one setting a user can change. Every field, the type rules and how values reach your code are in [Parameters](/docs/build/parameters). In short: | Field | Type | Required | Default | Constraints | |---|---|---|---|---| | `name` | string | Yes | | Unique across the list: `parameter names must be unique`. | | `type` | string | Yes | | `int`, `float`, `bool`, `str` or `enum`. | | `default` | any | Yes | | Must pass the parameter's own type, range and choices checks, or the manifest does not load. | | `label` | string | No | `""` | At most 60 characters. | | `description` | string | No | `""` | No length limit. | | `min`, `max` | number | No | none | Applied to `int` and `float`; ignored for other types. | | `choices` | list | For `enum` | none | The allowed values; ignored for other types. | ## `objective` Describes the number each method reaches, so every value is shown with the right label and unit. The platform never writes this text itself. | Field | Type | Default | Constraints | |---|---|---|---| | `label` | string | `Objective` | No length limit. | | `unit` | string | `""` | For example `Ha`. | | `decimals` | integer | `4` | 0 to 10 inclusive. | ## `metric` Picks one of three accuracy formulas. You choose a kind and explain it in words; you do not supply a formula. [Benchmarks](/docs/build/benchmarks) gives the formulas and the rule for when a result matches the baseline. | Field | Type | Default | Constraints | |---|---|---|---| | `kind` | string | `relative_gap` | `relative_gap` (lower-is-better objective, only a worse result loses accuracy), `relative_error` (distance to the reference, with the quantum value's error bar), `error_rate` (the value is a share of mistakes, with a 95% interval). Any other value fails the load. | | `note` | string | `""` | At most 600 characters. Says what accuracy means for this workflow. | | `tolerance` | number | `0.0` | 0 to 1 inclusive. Accuracy within this of 100% counts as matching the reference. | | `tolerance_abs` | number or null | `null` | At least 0. A difference to the reference up to this, in the objective's own units, counts as matching, for example `0.0016` Ha (chemical accuracy). | ## Methods `methods` lists every method Qsolve runs and compares on each input. You need exactly one `quantum` and exactly one `reference`, any number of `classical`, and at least two methods in total. The platform runs the classical methods first and the quantum method last. | Field | Type | Required | Default | Constraints | |---|---|---|---|---| | `id` | string | Yes | | Same pattern as `name` (`method id must be lowercase letters, digits and dashes`). Unique within the list. | | `role` | string | Yes | | `quantum`, `reference` or `classical`. | | `title` | string | Yes | | At most 80 characters. | | `what` | string | No | `""` | At most 500 characters. What the method does, for a reader who does not know it. | | `strength` | string | No | `""` | At most 300 characters. | | `limit` | string | No | `""` | At most 300 characters. | List-level errors: `method ids must be unique`, and `declare exactly one method with role 'quantum'` (or `'reference'`). A list with fewer than two methods also fails the load. Each method is implemented by a method of your `Workflow` class. The role decides the name, and the `id` only matters for `classical`: | Role | Python method it calls | |---|---| | `quantum` | `quantum(self, ctx)` | | `reference` | `reference(self, ctx)` | | `classical` | `classical_(self, ctx)`, with dashes in the id turned into underscores: id `annealing` calls `classical_annealing`, id `greedy-swap` calls `classical_greedy_swap` | A declared method with no matching Python method stops the run with `method 'x' is declared in the manifest but the workflow has no `name` method`. A `reference` method that touches the quantum backend fails the run with `QuantumNotAllowed`; a `classical` one that does is shown as unavailable. Qsolve shows `title`, `what`, `strength` and `limit` as written and never writes its own text about your workflow, so write for someone who does not know the method. ## `envelope` The largest problem the workflow accepts. | Field | Type | Required | Default | Constraints | |---|---|---|---|---| | `max_qubits` | integer | Yes | | 1 to 1000. | | `max_variables` | integer | Yes | | 1 to 100000. | | `notes` | string | No | `""` | Shown in the rejection message, for example "Up to 24 nodes per run". | How it is enforced: - After your `__init__` runs, Qsolve calls `size()` and refuses the input if the result is greater than either `max_variables` or `max_qubits`, before any circuit runs and before anything is charged. The message is `Problem has variables; this workflow supports at most ()`. In practice the smaller of the two is the limit. - `max_qubits` is also a hard cap in the sandbox: a circuit with more qubits than this is refused by the executor with `circuit exceeds the allowed qubit count`. - The input form shows `notes` next to "Your problem". - It is separate from your input schema bounds. The schema (`maxItems`, `maximum`, ...) rejects a malformed input with `Invalid input at : `; the envelope rejects a well-formed input whose size, as your `size()` reports it, is too big. Set both: schema bounds so the form can show limits early, and the envelope as the size limit that holds whatever shape the input takes. ## `targets` | Field | Type | Default | Valid values | Meaning | |---|---|---|---|---| | `simulators` | list | `[cpu]` | `cpu`, `gpu` | A run on `sim-cpu` is accepted only if `cpu` is listed, and on `sim-gpu` only if `gpu` is listed. Any other entry fails the load. | | `qpu` | list | `[]` | `ibm` | A run on `ibm:` needs `ibm` listed. No other provider is accepted: Amazon Braket is not available yet, and `braket` in this list fails the manifest load. | | `min_qubits_qpu` | integer or null | `null` | any integer | Accepted and stored, but **nothing reads it**: it has no effect on which runs are accepted, quoted or run. Lint reports it as warning `W052` so nobody relies on it. | A run requesting an unlisted target is refused with HTTP 422 `target_not_supported` and the message `This workflow does not support target 'x'.`, with the workflow's lists attached. For `ibm:`, the device name after the colon must match `^[A-Za-z0-9_.-]{1,64}$`. Noise-model targets (`fake:...`) are not accepted by the run API, so they are not listed here; they exist for benchmark cases and local runs. If `hardware_cases` are declared in `benchmarks/cases.yaml`, `qpu: [ibm]` is required (lint `E054`). See [Backends and targets](/docs/build/backends-and-targets) and [Machines and targets](/docs/use/machines-and-targets). ## `resources` | Field | Type | Default | Valid values | Meaning | |---|---|---|---|---| | `timeout_s` | integer | `1800` | 1 to 86400 | Wall-clock limit for a run, in seconds. | | `memory_mb` | integer | `4096` | 256 to 65536 | Memory limit for the sandbox, in MB. A run that exceeds it stops with `exceeded the MB memory limit`. | | `network` | string | `none` | `none` only | Workflows have no network. Any other value fails the load. | How the limits are applied: - **Timeout.** The sandbox stops a run at `timeout_s` with `exceeded its s compute limit`. The platform also caps every run at its own maximum, 4 hours by default, whatever the manifest asks. Time spent waiting for a quantum computer does not count against it: the limit is extended by the queue wait, up to a platform cap (6 hours by default). - **Up-front check.** If the run time predicted from your `estimate()` (its `runtime_s`, to which the SDK adds 20 s of run overhead) is above the effective limit (the smaller of `timeout_s` and the platform cap), the run is refused before it starts with `run_too_long`. - **Validation.** The benchmark run during validation uses a time limit of `timeout_s` raised to at least 300 seconds and capped at 7200, and memory capped at 8192 MB. A heavy workflow therefore gets its whole `timeout_s` up to 2 hours while validating. ## `runtime` | Field | Type | Default | Meaning | |---|---|---|---| | `python` | string | `3.12` | Informational: the value is stored but not read when a run starts. Runs use the Python of the platform image. | | `dependencies` | string | `requirements.lock` | Informational: stored but not read. The package set is fixed by the platform image; see [Project layout](/docs/build/project-layout). | ## Examples of choices A chemistry workflow measures energies, so it uses an absolute tolerance in the objective's units. From `molecular-ground-state`: ```yaml objective: {label: Ground-state energy, unit: "Ha", decimals: 6} metric: {kind: relative_error, tolerance_abs: 0.0016, note: "1 minus the energy difference to the exact active-space answer, as a fraction of that answer. Total energies are large numbers, so read the error in mHa too: chemical accuracy is 1.6 mHa."} ``` A classifier's objective is an error rate. From `quantum-kernel-classifier`: ```yaml objective: {label: Test error rate, unit: "", decimals: 3} metric: {kind: error_rate, note: "Share of held-out test points classified correctly, with its 95% interval. Fewer test points mean a wider interval."} ``` An error-rate workflow must put the number of test cases in its quantum method's `Outcome.details` as `n_test`, so the interval can be computed. See [Writing the workflow](/docs/build/writing-the-workflow). Resources scale with the work. `max-cut` (a 24-variable QAOA) asks for the defaults of 1800 s and 1536 MB; `molecular-ground-state` runs a chemistry solver, so it asks for `timeout_s: 3600` and `memory_mb: 2048`; `electrolyte-solvent-screen` runs up to eight chemistry systems and asks for `timeout_s: 7200`. The envelope scales the same way: 24 qubits for the graph problems, 12 for the chemistry ones (6 active orbitals), 8 for the kernel classifier (one qubit per feature). ## Validate it ```bash qsolve validate . ``` A manifest error prints `Invalid workflow:` with the field and reason, and exits 1. After the manifest loads, `validate` also checks that every example validates against the input schema. Common errors are in [Troubleshooting](/docs/build/troubleshooting) and every lint code is in [Lint codes](/docs/build/lint-codes). --- # Writing the workflow > The contract of src/workflow.py: the Workflow class, what each method receives and returns, the Outcome object, solver details and how answers are rechecked. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/writing-the-workflow · Updated: 2026-11-13 A workflow is one Python class that subclasses `qsolve.Workflow`, named by the `entrypoint` in `qsolve.yaml`. Qsolve builds it from a validated input, runs every declared method and recomputes each objective with your `evaluate`. The manifest side is in [The manifest](/docs/build/manifest). This page is the guided tour; every name, parameter and exception is in the exact reference, [The Workflow class](/docs/build/sdk-workflow-class), and the rest of the API is mapped in the [SDK reference](/docs/build/sdk-reference). ## The class ```python from qsolve import Context, Estimate, InvalidSolution, MethodUnavailable, Outcome, Workflow, WorkflowError class MyWorkflow(Workflow): def __init__(self, input, params) # parse and validate; raise WorkflowError; call super().__init__ def size(self) -> int # qubits or variables def quantum(self, ctx) -> Outcome # the method with role quantum def reference(self, ctx) -> Outcome # the method with role reference def classical_(self, ctx) -> Outcome # one per method with role classical def evaluate(self, solution) -> float # the objective of a solution; lower is better def answer(self, outcome) -> dict # the domain answer, validated by outputs.schema def estimate(self, target) -> Estimate # optional: circuits and shots to expect def summary(self, quantum, reference) -> str # optional: one sentence for the result page ``` You never call these methods yourself: Qsolve calls them. `quantum`, `reference`, `evaluate`, `answer` and `size` are required (the base class raises `NotImplementedError` for them). A declared method with no matching class method fails with "method 'x' is declared in the manifest but the workflow has no `classical_x` method". In `classical_`, dashes in the manifest id become underscores: `exhaustive-or-annealing` would be `classical_exhaustive_or_annealing`. The roles `quantum` and `reference` always use those plain names. ## What you receive ### `__init__(self, input, params)` `input` is the user's input as a dictionary, already validated against `schemas/input.json`. `params` holds every manifest parameter: defaults, with any overrides applied and checked. Call `super().__init__(input, params)`, which stores them as `self.input` and `self.params`. Parse the input once here and raise `WorkflowError("message")` for anything the user got wrong. The user sees the message as written. ```python if len(set(self.nodes)) != len(self.nodes): raise WorkflowError("node names must be unique") ``` A `ValueError`, `KeyError`, `TypeError` or `IndexError` from `__init__` becomes "Could not set up the problem from this input: ...". Any other exception is a crash. ### `size(self) -> int` The number of qubits or variables. Qsolve compares it with the manifest's `envelope` and rejects the input above `max_variables` or `max_qubits`, with the envelope's `notes` in the message. ### `ctx` (a `Context`) | Field | Type | Purpose | |---|---|---| | `seed` | int | The run's seed. Use it for all randomness. | | `target` | str | The target string, for example `sim-cpu` or `fake:fez@1.6`. | | `backend` | Qiskit `BackendV2` | Runs circuits. See [Backends and targets](/docs/build/backends-and-targets). | | `sampler` | Qiskit `SamplerV2` | Runs circuits and returns counts. | | `estimator` | Qiskit `EstimatorV2` | Measures expectation values. | | `emit(type, **data)` | function | Sends a progress event, for example `ctx.emit("train_step", step=3, value=-1.2, best=-1.4)`. Pass the type positionally. Never affects the result and costs nothing when nobody listens. | | `report_mitigation(circuits, distribution, methods=(), kept=None, steps=())` | function | Declares mitigated output. See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). | Only the quantum method may use the backend. The `ctx` given to `reference` and `classical_*` has a backend that raises `QuantumNotAllowed`, a subclass of `WorkflowError`, as soon as one of its `run` calls is made. The fields are specified in [The Workflow class](/docs/build/sdk-workflow-class#context); `sampler`, `estimator` and `backend` in [Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge). ## What you return ### `Outcome` Every method returns an `Outcome`: ```python Outcome(solution, objective, stderr=None, samples=None, details={}) ``` | Field | Meaning | |---|---| | `solution` | JSON-compatible data in whatever form your `evaluate` accepts. Use `None` for a result that cannot be re-evaluated, such as a measured energy or one taken from a classical library; it is then labelled reported instead of recomputed. | | `objective` | The value the method reports for its solution. Lower is better. Must be finite. | | `stderr` | One standard deviation, when the value is a measurement. Must be finite and not negative. | | `samples` | For a sampling method: a list of `{"solution": ..., "p": probability}`, optionally with a `"label"` string. Give all of them. | | `details` | A free-form dictionary kept with the run (default: an empty one). | For a maximisation problem, return the negative: `max-cut` returns the negative cut weight and labels its objective "Negative cut weight". Your `answer()` turns it back into a cut weight for people. ### `evaluate(self, solution) -> float` The objective of a solution, lower is better. Qsolve calls it on every method's solution and on each entry of `samples`, so keep it exact and fast. Raise `InvalidSolution("reason")` for a solution that breaks a rule of the problem. ```python def evaluate(self, solution): x = np.asarray(solution, dtype=float) if x.shape != (self.n,) or not np.isin(x, (0, 1)).all(): raise InvalidSolution("a solution is one 0 or 1 per variable") return float(x @ self.Q @ x) ``` ### `answer(self, outcome) -> dict` The answer people read, shaped by `schemas/output.json`. Qsolve calls it on the quantum method's outcome, which must succeed, and on every other method's outcome so the answers can be shown side by side. Every method's answer is validated against the output schema. A mismatch in the quantum answer fails the run with "Workflow produced an invalid answer: ...". For a classical method (or the reference), a failing `answer()` or a mismatch marks only that method unavailable, with the reason (`answer() failed: ...` or `its answer breaks the output schema: ...`) and the run continues; if it is the reference, the run fails because there is nothing to compare with. ### `estimate(self, target) -> Estimate` Optional but strongly recommended. It returns what a run will send to the target: ```python Estimate( qubits=5, circuits_on_target=1, shots_on_target=4000, classical_cost_class="low", # "low", "medium" or "high": local CPU work runtime_s=None, # predicted seconds for the whole run, or None notes=[], ) ``` The estimate becomes the cost quote shown before a run. Without one, Qsolve assumes one circuit and `params["shots"]` shots and marks the estimate as a default; `estimate_honest` then passes with a note. With one, it is compared with what the first benchmark case sent: circuits and shots must each be within a factor of two. A `runtime_s` above the manifest's `resources.timeout_s` makes the run refused. The helpers the examples use to predict simulation time (`statevector_seconds`, `simulator_threads`, `S_PER_TRANSPILE`) are in `qsolve.sim`; see [Metering and cost](/docs/build/sdk-metering-and-cost). ### `summary(self, quantum, reference) -> str` Optional. One sentence for the result page, truncated to 500 characters. The default reads "QAOA (quantum approximate optimisation): -10; Exhaustive search: -10 (accuracy 100.00%)." ## Writing the quantum method Build circuits with Qiskit, train however you like, and send what you want measured through `ctx`. Local statevector training is not metered. There are three ways to send circuits, all Qiskit-native: ```python # 1. the Sampler: bit strings and counts counts = ctx.sampler.run([(circuit, parameter_values, shots)]).result()[0].data.meas.get_counts() # 2. the Estimator: expectation values, no mitigation, precision sets the shots evs = ctx.estimator.run([(circuit, observable)], precision=0.02).result()[0].data.evs # 3. the backend: plain Qiskit run() counts = ctx.backend.run(circuit, shots=2000).result().get_counts() ``` Every circuit you send is copied, recorded and measured; all circuits of one `sampler.run` with the same shot count are sent as one batch (on a quantum computer, one job and one queue wait). Circuits need measurements for the sampler and the backend. Send circuits as you built them; Qsolve transpiles for the device. Each pub result carries per-circuit metadata in `pub_result.metadata["qsolve"][k]`, including `dispatch` (the index to cite in `report_mitigation`) and, on noise-model and hardware targets, `readout` errors and `two_qubit_gates`. This is the final-sampling step from the scaffold: This is the final-sampling step from the scaffold: ```python measured = qc.copy() measured.measure_all() counts = ctx.sampler.run([(measured, theta, shots)]).result()[0].data.meas.get_counts() samples = [ {"solution": [int(c) for c in reversed(bits)], "p": n / shots} for bits, n in counts.items() ] best = min(samples, key=lambda s: self.evaluate(s["solution"])) return Outcome( solution=best["solution"], objective=self.evaluate(best["solution"]), samples=sorted(samples, key=lambda s: -s["p"])[:10], details={"theta": theta.tolist(), "layers": p}, ) ``` Qiskit puts qubit 0 on the right of a bitstring, so the code reverses them to put variable 0 first. A quantum method that sends no circuit fails validation: "quantum method qaoa sent no circuit to the backend". ## Writing the classical methods `reference` is the method you are judged against. Use the strongest standard method that is practical, such as an exact solver where the size allows, and describe it honestly in the manifest. Add any number of `classical_` methods for comparison. Raise `MethodUnavailable("why")` from a method that does not apply to an input; it is shown as unavailable with that reason and the run continues. A `classical_*` method that raises anything else, a `WorkflowError` included, is also shown as unavailable ("failed: ..."), and a case where that happens fails validation: use `MethodUnavailable` for "does not apply" and keep real errors out. If the quantum method or the reference raises, the run fails (a `WorkflowError` reaches the user as written; any other exception is a crash). The reference must produce a value: otherwise the run fails with "the quantum method or the reference produced no result". A classical method with no solution to re-evaluate, such as an energy from a chemistry library, returns `solution=None` and can raise `MethodUnavailable` when the number does not exist for an input. This is from `molecular-ground-state`, where the references are classical energies computed before the run: ```python def _reference_energy(self, kind, name): energy = self.chemset.reference(kind) if energy is None: raise MethodUnavailable(f"{name} was not computed for this molecule") return Outcome(solution=None, objective=energy) def classical_hartree_fock(self, ctx): return self._reference_energy("hf", "Hartree-Fock") ``` ## How Qsolve checks your answer Qsolve does not trust a method's own number. For each method: 1. If it returned a solution, Qsolve calls `evaluate(solution)` and uses that value as the objective, labelled recomputed. 2. If your reported `objective` differs from the recomputed value by more than one millionth (relative, with a floor of 1), the method is flagged with the claimed number and fails validation: "method qaoa claimed X, its solution gives Y". 3. If `evaluate` raises `InvalidSolution` or returns a non-finite value, the method is shown as unavailable. 4. If it returned `solution=None`, its objective is accepted but labelled reported. 5. For methods with `samples`, Qsolve evaluates every sample, reports the share that is valid and the share on the baseline's answer, and shows the five most frequent outcomes. 6. The headline accuracy and verdict come from your manifest's metric kind, using the recomputed values. See [Benchmarks](/docs/build/benchmarks). Re-evaluation shows a number is consistent with its solution, not that your `evaluate` is correct. Test it. ## Reporting solver details Put diagnostics a reader might want in the quantum method's `details`: trained angles, layer counts, per-system numbers, anything JSON-compatible. ```python return Outcome(run.solution, run.energy, samples=run.samples, details=run.details) ``` Qsolve keeps them with the run under `solver`, converting NumPy arrays and scalars to plain JSON. Details larger than 200,000 bytes serialised are replaced by a note giving their size. They are not part of the result hash. For an `error_rate` metric, `details` must contain `n_test`, the number of test cases, so the 95% interval can be computed. ## Testing it with pytest `qsolve init` creates `tests/test_workflow.py`. It calls the same function the platform runs: ```python from qsolve import run_workflow out = run_workflow(str(ROOT), inp, params={"layers": 2}, seed=1) assert out.result.metrics is not None and out.result.metrics.matches ``` `run_workflow` returns a `RunOutput` with a `result` and a `provenance`; fields are in [Results and events](/docs/build/sdk-results-and-events). Use `estimate_run` to test your `estimate()` and `qsolve.benchmark.run_benchmarks` to run the gates from Python ([Runner and manifest](/docs/build/sdk-runner-and-manifest), [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks)). To test one method in isolation, build the class yourself and call `evaluate` and `answer` directly; the methods that need a `Context` are best tested through `run_workflow`. ## Determinism The same input, parameters and seed must give the same result. The `reproducible` gate runs your first benchmark case twice and compares result hashes with floats rounded to a grid of 1e-6. Use `ctx.seed` for every random number generator. Runs use one BLAS thread so summation order is fixed, and results that differ only at floating-point noise (relative 1e-8) pass. ## Next - [Input schema reference](/docs/build/input-schema-reference) and [Output schema reference](/docs/build/output-schema-reference), or the overview in [Input and output schemas](/docs/build/input-and-output-schemas) - [Parameters](/docs/build/parameters) and [The manifest](/docs/build/manifest) - [SDK reference](/docs/build/sdk-reference) for every public name - [Lint codes](/docs/build/lint-codes) for what `qsolve lint` flags (imports the sandbox allows, for example) - [Backends and targets](/docs/build/backends-and-targets) - [Examples](/docs/build/examples) for complete working classes --- # Input and output schemas > The two JSON Schemas of a workflow, the rules both must follow, alternatives for the input, and where to find the full references. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/input-and-output-schemas · Updated: 2026-11-13 A workflow has two JSON Schema files. `schemas/input.json` describes what a user provides; Qsolve validates every run's input against it and builds the input form from it. `schemas/output.json` describes the answer `answer()` returns; Qsolve validates the answer against it and draws it from the schema's titles and units. Both use JSON Schema draft-07 with a few restrictions. | Page | What it covers | |---|---| | This page | The shared rules, alternatives, and the check commands. | | [Input schema reference](/docs/build/input-schema-reference) | Every input keyword and `x-ui` hint, and how the form is built. | | [Output schema reference](/docs/build/output-schema-reference) | How the answer is validated and drawn, and every output hint. | ## Rules both schemas must follow `qsolve lint` and the platform reject a schema that breaks these rules: | Rule | Lint code | |---|---| | It is valid draft-07 (`"$schema": "http://json-schema.org/draft-07/schema#"`) | `E020` | | It does not use `pattern` or `patternProperties` anywhere, because regular expressions can hang validation. Use `enum`, `minLength` or `maxLength` | `E021` | | Every `$ref` is local, starting with `#`, for example `#/definitions/point` | `E021` | | It does not use `"format": "regex"` | `E021` | | It serialises to at most 256 KB | `E022` | | Every example input in `inputs.examples` validates against the input schema | `E023` | | It is a JSON object, and its path stays inside the repository | manifest load | `E022` is measured on the schema's JSON text, in characters, and applies to both files. `E023` applies to the input schema only, and an input schema with no example is `E024` (see [The manifest](/docs/build/manifest#inputs-and-outputs)). Declare bounds so the form and the validator catch mistakes early: `minItems`, `maxItems`, `minimum`, `maximum`, `maxLength`. Match them to your manifest's `envelope`: `max-cut` allows 24 nodes in the manifest and says `"maxItems": 24` in the schema. Set `additionalProperties: false` at the top of the input schema so a misspelled field is an error rather than silently ignored. A failing input is refused with `Invalid input at : `, for example `Invalid input at nodes: [] is too short`. Checks a schema cannot express, such as "node names must be unique", belong in `__init__`, which raises `WorkflowError` with a message for the user. The output schema is stricter in effect than it looks: an answer that does not match fails the run with `Workflow produced an invalid answer: `. See the [output schema reference](/docs/build/output-schema-reference#validation). ## Presentation hints with `x-ui` `x-ui` is an extra object you may add to a property. It only affects how the form and the answer are drawn; the validator ignores it, so a wrong hint never rejects an input or an answer. The input form reads many hints (`widget`, `labels_from`, `slider` and more); the answer view reads only `unit` and `format`. Both are documented in full in the two reference pages above. Give every property a `title`: without one, the form and the answer show the property name with underscores turned into spaces. ## Alternatives: one input, two descriptions A top-level `anyOf` of `required` lists lets a user choose how to describe the problem. `portfolio-optimization` accepts either expected returns with a covariance matrix, or a history of returns: ```json "anyOf": [ {"title": "Expected returns and covariance", "required": ["expected_returns", "covariance"]}, {"title": "Historical returns", "required": ["returns_history"]} ] ``` The form shows these as alternatives, titled by each entry's `title`. Details, including the `also` hint, are in the [input schema reference](/docs/build/input-schema-reference). ## Check your schemas ```bash qsolve validate . # loads both schemas and validates the example inputs qsolve lint . # applies the restrictions above ``` --- # Input schema reference > Complete reference for authoring schemas/input.json: which JSON Schema keywords the run form honours, every x-ui key with accepted values and effects, worked widget examples, validation rules and a pre-publish checklist. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/input-schema-reference · Updated: 2026-11-13 `schemas/input.json` does two jobs. It is the contract Qsolve validates every run's input against, and it is the blueprint the run form is drawn from. This page is the exact reference for the second job: what each JSON Schema type and keyword turns into on screen, every `x-ui` key, and where the form and the server differ. For the rules that apply to both schemas (draft-07, no `pattern`, local `$ref` only, 256 KB) and for output schemas, see [Input and output schemas](/docs/build/input-and-output-schemas). Everything below was checked against the form's source (`schema.ts`, `schema-form.tsx`, `widgets.tsx`, `validate.ts`), the SDK (`runner.py`, `lint.py`, `manifest.py`) and the API (`runs.py`). Where behaviour is surprising it is called out as a note. Anything not verified is marked **unverified**. ## How the pieces fit ``` schemas/input.json --publish--> stored with each property's order (x-order) | | | run form is built from it | (types, keywords, x-ui) v form checks the input in the browser (Ajv) ---> POST /v1/runs | server checks again (jsonschema) -> 422 or run starts | SDK checks again before __init__ (jsonschema) ``` - The schema (standard JSON Schema) decides what is valid. It is the only thing that can reject an input. - `x-ui` only changes how a field is drawn. The server and the SDK ignore it. In the browser form a few `x-ui` rules (`max_from`, `symmetric`, `square`) are also checked before Run, as listed under [Validation](#validation-form-vs-server). - The form starts every field at its schema `default` and sends it (see [Defaults](#defaults)). Besides the schema, the form itself checks `max_from`, `symmetric`, `square` and the dataset limits, and blocks Run with an inline message; the server does not know these rules. ## Quick start A complete, minimal input schema with one of each common control: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["labels", "Q"], "additionalProperties": false, "properties": { "labels": { "title": "Variable names", "description": "One name per variable", "type": "array", "minItems": 2, "maxItems": 12, "items": {"type": "string", "minLength": 1, "maxLength": 32} }, "Q": { "title": "Q matrix", "description": "Paste it from a spreadsheet", "type": "array", "items": {"type": "array", "items": {"type": "number"}}, "x-ui": {"widget": "matrix", "labels_from": "labels", "symmetric": true} }, "cardinality": {"title": "Number of ones", "type": "integer", "minimum": 0}, "mode": {"title": "Mode", "type": "string", "enum": ["fast", "accurate"], "default": "fast"}, "offset": {"title": "Constant offset", "type": "number", "default": 0, "x-ui": {"advanced": true}} } } ``` The user sees: a chip box for the variable names (type a name and press Enter, or paste a list); a grid whose rows and columns are those names and that stays symmetric; a whole-number box; a two-option switch (fast / accurate); and, behind a "More options" link, the offset box. Rules of thumb: 1. Put `"additionalProperties": false` on every object so a misspelled field is an error, not silently ignored. 2. Give every property a `title` and a `description`. The title is the label; the description appears in a small info popover next to the label. 3. Declare bounds (`minItems`, `maxItems`, `minimum`, `maximum`, `maxLength`) so mistakes are caught before a run is created. 4. Add at least one example input (`inputs.examples` in the manifest). The run page opens pre-filled with the first one. ## Common behaviour of every field | Aspect | What happens | |---|---| | Label | `title`. With no title, the property name with `_` and `-` turned into spaces and the first letter capitalised (`num_to_select` becomes "Num to select"). Names of two characters or fewer are left as written. | | Help text | `description` appears in an info popover next to the label, not under the field. Keep it short and put units there. | | "optional" tag | Shown next to the label of every property not in the object's `required` list. Inside an `anyOf` mode (see [Alternatives](#anyof-alternatives-modes)), the fields of the selected mode count as required. | | Order | The order you wrote the properties in. The platform records it as `x-order` at publish because the database sorts object keys. Do not write `x-order` yourself: it is overwritten at publish. | | Errors | Shown in red under the field and linked to the control with `aria-describedby` (the drop-down list for long enums is the one exception). Before the user presses Run the first time, "This is required" messages and top-level errors are hidden; after that all errors show. Arrays show the first error found anywhere inside them; objects show only errors on the object itself, their fields show their own. | | `$ref` | A local `#/definitions/...` reference is followed; each path segment is decoded as a JSON Pointer (`~1` is `/`, `~0` is `~`, `%20` is a space). Keywords written next to the `$ref` (title, description, x-ui) override the target's; `x-ui` objects are merged key by key, with the sibling winning. A reference loop stops after 16 steps. | | Unknown shape | A property the form cannot draw (see [Fallbacks](#fallback-a-json-box)) becomes a small JSON text box, so every valid schema stays usable. | | Keywords the form ignores | `format`, `const`, `allOf`, `oneOf`, `not`, `if/then/else`, `dependencies`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `uniqueItems`, `minLength` (except for the message), `additionalItems`, `contains`, `propertyNames`. The server still enforces the ones the validators support (not `format`, see [Validation](#validation-form-vs-server)). | ## Properties by JSON Schema type ### string | Keyword / shape | Honoured | Control and behaviour | |---|---|---| | plain `type: "string"` | yes | Single-line text box. | | `enum` (up to 5 values) | yes | Segmented buttons (one choice, no default selected unless the value is set). | | `enum` (6 or more values) | yes | Drop-down list. | | `x-ui.option_labels` | yes | Maps each enum value to a display label; the stored value is unchanged. | | `x-ui.widget: "code"` or `maxLength` above 200 | yes | Multi-line monospace text area, spell-check off, grows from 4 to 10 rows. | | `x-ui.placeholder` | yes | Placeholder text in the text box or text area. Not used for numbers or enums. | | `examples` | yes | Clickable chips under a single-line text box. Clicking one sets the value. Not shown for enums, code widgets or long (over 200) strings. | | `maxLength` | indirect | Switches to the text area above 200. Violations are reported as "Keep this under N characters." No live counter. | | `minLength` | indirect | Violations are reported as "This can't be empty." for `minLength` 1 and "Use at least N characters." for N above 1. | | `default` | yes | The form starts with the default typed into the box (a real value, sent with the run). A required enum with no default starts unselected. | | `format`, `pattern` | no | `pattern` is banned by lint. `format` is ignored by the form and the server. | Empty text: for an optional field, clearing the box removes the key from the input. For a required field the key stays as `""` (so `minLength: 1` then reports the error). `type: ["string", "null"]` draws as a string. The form can never produce `null`. `type: ["string", "number"]` (or `"integer"`) draws as a text box that accepts a number or text: text that reads as a number and prints back identically (`12`, `1.5`, not `007` or `1.0`) is stored as a number, anything else as a string. With `"integer"` and no `"number"`, decimals stay text. ### integer and number | Keyword / shape | Honoured | Control and behaviour | |---|---|---| | `type: "number"` | yes | Number box. The user can type freely (`0.`, `-`, `1e-3`); only valid numbers are stored. Typing text that is not a number leaves the stored value unchanged. | | `type: "integer"` | yes | Same box; typed decimals are rounded to the nearest whole number (`2.5` becomes `3`). | | `minimum`, `maximum` | yes | A value outside the range turns the box border red as soon as it is typed; the value is kept, not clamped. A hint next to the box shows only the bounds that exist: "1 to 5", "at least 1" or "at most 5". The error: "Must be at least N." / "Must be at most N." | | `default` | yes | Starts in the box as a real value and is sent. If the user clears the box the key is removed and the default is shown greyed as a placeholder (multiplied by 100 with `percent`). | | `x-ui.slider` | yes | Adds a slider, see [slider](#slider). | | `x-ui.percent` | yes | Shows and accepts percent, see [percent](#percent). | | `x-ui.max_from` | yes | Shown as a hint and checked by the form. See [max_from](#max_from). | | `enum` on a number | no | An enum of numbers is drawn as a plain number box. Use a string enum with `option_labels`, or `minimum`/`maximum`. (Solver settings in the run composer are different: a numeric enum choice is sent as the number.) | | `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` | no | Enforced as a generic error on Run, with no hint on screen. State the rule in the `description`. | Clearing the box removes the key. `type: ["number", "null"]` draws as a number; `null` cannot be entered. ### boolean A switch. With a `default` the form starts with that value written; without one the switch shows off but the key is not written until the user flips it, so read it with `input.get("flag", False)`. ### array The form picks a control by looking at the shape of `items`, in this order. The first match wins. | Order | Shape | Control | |---|---|---| | 1 | `x-ui.widget: "table"` with `x-ui.columns` | [Table](#table) of fixed-length rows | | 2 | array of arrays of `number` or `integer` (or of untyped items with `x-ui.widget: "matrix"`) | [Matrix grid](#matrix); integer cells round what is typed or pasted | | 3 | array of `number` or `integer` with `x-ui.labels_from`, or with `x-ui.widget: "vector"` | [Vector](#vector): one box per label (with the widget and no labels: numbered boxes, with Add value / Remove last unless `minItems` equals `maxItems`) | | 3b | array of `number`/`integer`, `minItems` equal to `maxItems`, at most 4 | Fixed row of boxes labelled x, y, z, w | | 3c | other array of `number`/`integer` | Number chips ("Type a number and press Enter") | | 4 | array of strings with `items.enum` | Toggle buttons, pick any number; `option_labels` supported | | 5 | array of strings | Text chips ("Type a name and press Enter, or paste a list") | | 6 | array of objects | A card per item with an Add button and a remove cross | | 7 | anything else (arrays of arrays of strings, arrays of booleans, tuple-form `items: [...]`) | [JSON box](#fallback-a-json-box) | Keywords on arrays: | Keyword | Effect | |---|---| | `minItems` | Not drawn as a hint; reported as "Add at least N." on Run. Matrix grids stop offering row removal at `minItems` (1 if unset). | | `maxItems` | Chips stop accepting input at the limit and cut pasted lists to it. Toggle buttons disable unselected options at the limit. Cards and matrices hide the Add button at the limit. A table or a pasted block can still exceed it; the error "At most N allowed." appears on Run. | | `uniqueItems` | Not enforced by the form (the server enforces it). String chips silently drop duplicates; number chips keep them. Toggle buttons are unique by construction. | | `default` | Written into the input at start (a copy). Cards: new items start from the item schema's defaults and required fields. | Chip boxes split what you type or paste on commas, tabs and new lines. A string containing a comma cannot be entered as one chip. Pasting a column from a spreadsheet adds one chip per cell. A trailing `%` on a number chip divides by 100. ### object | Shape | Control | |---|---| | `x-ui.widget: "dataset"`, or an object with properties `X` and `y` | [Dataset editor](#dataset) | | no `properties`, `additionalProperties` is a schema | A map: [`keys_from`](#additionalproperties-map-keys_from) grid of numbers, or an editable list of named entries | | has `properties` and is required | The fields drawn inside a shaded panel | | has `properties` and is optional and absent | A dashed "Set ..." button (using the title in lower case). Pressing it creates the object from its `default`, or from its required fields. An "Use default" link removes it again | | top-level object | The fields drawn as the form | | `anyOf` with `required` lists | [Alternatives](#anyof-alternatives-modes) | | `additionalProperties: true` or omitted, and no `properties` | [JSON box](#fallback-a-json-box) | An object with both `properties` and a schema in `additionalProperties` shows only the named properties; the extra keys cannot be edited in the form. ### Fallback: a JSON box Any value the form cannot draw becomes a text area holding raw JSON. It parses as the user types and marks itself red when the JSON is invalid (the last valid value stays in the input). Properties with only `anyOf`/`oneOf`/`allOf`, `type` lists of several non-null types other than string with number (for example `["string", "boolean"]`, drawn as a string), and `type`-less schemas end up here or are mis-drawn. Prefer one concrete `type` per property. ## `x-ui` reference `x-ui` is an object on a property (or on an `anyOf` entry for `also`). The validators and `qsolve lint` do not look at it: a wrong value never rejects an input and lint does not warn. A key used on the wrong shape is silently ignored. | Key | Type | Applies to | Default | Exact effect | |---|---|---|---|---| | `widget` | `"matrix"`, `"vector"`, `"table"`, `"dataset"`, `"code"` | see rows below | none | An explicit hint wins over the shape guess. `matrix` and `vector` also work when the cell or item type is not declared. Other values are ignored. | | `widget: "matrix"` | | array of arrays | | Grid of number boxes. Also the automatic choice for any array of arrays of `number`/`integer`. | | `widget: "vector"` | | array of numbers | | One box per value. With `labels_from` the boxes carry the names; without it they are numbered and the list can grow (Add value) unless `minItems` equals `maxItems`. | | `widget: "table"` | | array of arrays | | Needs `columns`. Without `columns` it is ignored. | | `widget: "dataset"` | | object | | Draws the dataset editor. Also applied automatically to any object with `X` and `y` properties. | | `widget: "code"` | | string | | Multi-line monospace area. Also applied automatically when `maxLength` is above 200. | | `labels_from` | property name (string) | array of numbers (vector) or matrix | none | Names a sibling array property whose items label this value. A matrix uses it for rows and columns; a vector for its boxes. The two are kept the same length. See [labels_from](#labels_from). | | `column_labels_from` | property name | matrix | none | Labels the columns from a sibling array; rows stay free (the user can add and remove them). Keeps the number of columns in sync. | | `row_label` | string | matrix | `"Row"` | Name used for rows when there are no row labels: "Period 1", "Period 2", and "Add period". | | `symmetric` | boolean | matrix | `false` | Editing or pasting cell (r, c) also sets (c, r). The lower triangle is dimmed. Checked by the form: a matrix that is not equal to its transpose is reported ("Must be symmetric: A / B is 0.5 but B / A is 0.4.") and Run is blocked. A "Make symmetric" link under the grid copies the upper triangle onto the lower one. Not checked by the server. | | `square` | boolean | matrix | `false` | Hides the add-row and remove-row controls so rows cannot be added or removed alone, and the form requires rows equal to columns ("Must be square: 3 rows, but row 2 has 4 columns."). Not checked by the server. | | `columns` | list of column objects | `widget: "table"` | none | Table columns, see [table](#table). | | `columns[].title` | string, required | each column | none | Column header (also used as a key; keep titles unique). | | `columns[].type` | `"string"` or `"number"` | each column | text | `number` gives a number box; anything else a text box. With `options_from` the type is ignored (a drop-down). | | `columns[].options_from` | property name | each column | none | A sibling array property; its values are the drop-down choices for that column. A value not in the list is still shown. | | `columns[].optional` | boolean | each column | `false` | Marks "(optional)" in the header. Empty trailing optional cells are removed from the row (`[a, b]`, not `[a, b, null]`). New rows start without trailing optional columns; an optional column that comes before a required one is filled with `0` or empty text so positions stay right. Still, put optional columns last. | | `columns[].placeholder` | string | each column | none | Placeholder in that column's boxes (not for drop-downs). | | `preview` | `"graph"` | `widget: "table"` | none | Draws a live graph beside the table. Nodes are the values of the first column's `options_from` list; the first two columns are the endpoints and a numeric third column is the line weight. Needs at least 2 nodes. Shown only when there is room (wide layout). | | `max_from` | property name | integer or number | none | The maximum becomes the smaller of `maximum` and the length of the named sibling array; the form shows it and rejects a larger value. See [max_from](#max_from). | | `slider` | `[min, max, step]` | number or integer | none | Adds a slider with those three numbers next to the number box. | | `percent` | boolean | number | `false` | Box shows and accepts percentages; stored value is the fraction. | | `keys_from` | property name | object map | none | Keys of the sibling object property become fixed labelled number boxes. See [map](#additionalproperties-map-keys_from). | | `key_title` | string | object map | `"Name"` | Label of the key box of each entry and base of the add button ("Add system" for `"System name"`). With `keys_from` and `value_title` it is the first half of the column heading. | | `key_placeholder` | string | object map | none | Editable list: placeholder in the key box. With `keys_from`: placeholder in each number box (default: the value schema's `default`, else its first `examples` entry, else none). | | `value_title` | string | object map | none | Title of the value column: heading above a `keys_from` grid ("System → Weight"), and the label of the value editor for map values that are not objects. | | `add_label` | string | `widget: "table"` | `"Add row"` | Text of the button that adds a row, for example `"Add connection"`. | | `option_labels` | object of value to label | string enum, array of string enums | none | Display names for the enum values. | | `placeholder` | string | string | none | Placeholder for text box or text area. | | `advanced` | boolean | any property | `false` | Moves the field into a collapsed "More options" section at the bottom of its object. See [advanced](#advanced). | | `also` | list of property names | inside an `anyOf` entry | none | Extra, optional fields shown with that alternative. See [alternatives](#anyof-alternatives-modes). | Output-only keys `unit` and `format` are not read by the input form (they are documented under output schemas). What goes wrong when misused: | Mistake | Result | |---|---| | `labels_from` or `options_from` names a property that is missing or not an array | Treated as no labels or no options. A matrix without labels shows "Add names first" if it is empty. | | `widget: "matrix"` or `"vector"` on something whose declared cell type is not numeric | Ignored. | | `widget: "table"` without `columns` | Ignored; the array is drawn by its shape. | | `slider` with the wrong number of entries | Undefined behaviour (the slider uses whatever is in positions 0, 1, 2). Unverified beyond that. | | `max_from` naming a property that is not an array | Ignored (no cap shown or checked). | | An unknown `x-ui` key | Ignored without warning. Misspellings are invisible, so check the form in the preview. | ## Defaults The form starts with each field's schema `default` written into the input, at any depth (objects inside objects, items of cards, map values), and sends it with the run. A user sees the default as a normal value in the box, switch, slider or drop-down. | Situation | What happens | |---|---| | Form opens | The first example is loaded, then every property it lacks that has a `default` gets that default. With no example the form starts from the defaults alone. | | Required field with no `default` | Starts empty (and reports "This is required." after the first Run). A required object without a default starts as an object holding its children's defaults, if any. | | Optional object with no `default` | Absent, shown as a dashed "Set ..." button. Its children's defaults apply only after it is set. | | Optional object with a `default` | Starts set to a copy of that default. | | "Start from" an example, "Blank" | Same rule: the example (or nothing) plus defaults for what is missing. Blank is therefore the defaults, not `{}`. | | `anyOf` alternatives | Only the defaults of the active alternative's fields are written; the other alternatives' fields stay out so `additionalProperties: false` keeps passing. | | Switching alternative, adding a card or a map entry, "Set ..." | New values start from the field's `default`, else its required fields' defaults, else the first enum value, an empty text, `0` for numbers or zeros sized to the label list. | | User clears a field | The key is removed; the number box then shows the default as a grey placeholder. Your code's default still applies. | | Server | The API and the SDK use `jsonschema`, which does not fill defaults: they receive exactly what the form sent. | A default is copied, never shared, so editing a list or object does not change the schema. Keep repeating each `default` in your code (`input.get("risk_aversion", 0.5)`), because API and SDK callers do not go through the form. ## Worked examples ### matrix From `portfolio-optimization` (covariance, symmetric, labelled by `assets`): ```json "covariance": { "title": "Covariance matrix", "description": "Annualised. Paste it from a spreadsheet; it is kept symmetric", "type": "array", "items": {"type": "array", "items": {"type": "number"}}, "x-ui": {"widget": "matrix", "labels_from": "assets", "symmetric": true} } ``` The user sees: a scrollable grid with sticky headers. The row and column headers are the asset names. Typing in a cell above the diagonal also fills the mirrored cell, and the lower triangle is dimmed. A footer says "Paste straight from Excel or Sheets into any cell". An empty cell is stored as `0`. If an example or a loaded run is not symmetric, the form reports the first differing pair and the footer offers "Make symmetric (copy the upper triangle)". With `type: "integer"` cells the boxes take whole numbers (typed and pasted values are rounded). Enter moves to the cell below, Shift+Enter to the one above, Tab to the next cell. Paste rules (matrix and vectors): the first cell where you paste is the top-left corner of the pasted block. Separators are detected in this order: tab, comma, semicolon, whitespace. A first row that has no numbers is treated as a header and dropped; a first column that has no numbers is treated as row labels and dropped. A trailing `%` divides by 100. Cells that are not numbers are skipped. Anything beyond the right edge is ignored; rows below the bottom are ignored unless rows can grow. Decimal commas are not supported. Three row/column modes, decided by the keys: | Keys | Rows | Columns | Row add/remove | |---|---|---|---| | `labels_from: "assets"` | one per label | one per label | no (follow the labels) | | `column_labels_from: "assets"` | free | one per label | yes | | neither | free | the length of the first row | yes (unless `square`) | `returns_history` in the same workflow is the second kind: ```json "returns_history": { "title": "Historical returns", "description": "One row per period, one column per asset. Paste from a spreadsheet", "type": "array", "minItems": 3, "items": {"type": "array", "items": {"type": "number"}}, "x-ui": {"widget": "matrix", "column_labels_from": "assets", "row_label": "Period"} } ``` The user sees columns headed by the asset names, rows named "Period 1", "Period 2", ..., a cross at the end of each row (disabled at 3 rows, the `minItems`) and an "Add period" link. If the label list is empty the grid is replaced by "Add names first; the grid follows them." The generic QUBO example (`generic-qubo`) uses `labels_from: "labels"` with `square: true`, where `labels` is optional: a blank form shows the message until the user adds variable names, while a loaded example with a full `Q` and no labels shows the grid with numbered rows. ### vector From `portfolio-optimization`: ```json "expected_returns": { "title": "Expected annual returns", "description": "As fractions: 0.12 means 12%", "type": "array", "items": {"type": "number"}, "x-ui": {"widget": "vector", "labels_from": "assets"} } ``` The user sees one small number box per asset, captioned with the asset name, in a wrapping grid. Pasting several values into a box fills that box and the following ones. Empty boxes are stored as `0`. `integer` items give whole-number boxes. Without `labels_from` and without the widget, a number list is a chip box, or for `minItems == maxItems <= 4`, a fixed row labelled x, y, z, w. With `widget: "vector"` and no `labels_from` you get numbered boxes ("1", "2", ...) with "Add value" and "Remove last" links (hidden when `minItems` equals `maxItems`, and bounded by `minItems`/`maxItems`). ### table From `max-cut` (edges between named nodes, with a live graph): ```json "edges": { "title": "Connections", "type": "array", "minItems": 1, "items": {"type": "array", "minItems": 2, "maxItems": 3, "items": {"type": ["string", "number"]}}, "x-ui": { "widget": "table", "columns": [ {"title": "From", "type": "string", "options_from": "nodes"}, {"title": "To", "type": "string", "options_from": "nodes"}, {"title": "Weight", "type": "number", "optional": true, "placeholder": "1"} ], "preview": "graph", "add_label": "Add connection" } } ``` The user sees a table with From and To drop-downs (filled from the node chips) and an optional Weight box, a cross per row and an add link ("Add row" by default; the `add_label` key below changes it to "Add connection"). New rows start with the first node in both drop-downs. Enter in a text or number cell moves to the same column of the next row. Leaving Weight empty stores `["a", "b"]`; entering 2 stores `["a", "b", 2]`. Beside it (wide screens) a ring of node circles with a line per connection; line thickness follows the weight. The row schema is not used to draw the table: only `columns` is. Keep `items` consistent with the columns, since the server validates against `items`. ### dataset From `quantum-kernel-classifier`: ```json "definitions": { "table": { "type": "object", "additionalProperties": false, "required": ["X", "y"], "x-ui": {"widget": "dataset"}, "properties": { "X": {"type": "array", "minItems": 4, "maxItems": 400, "items": {"type": "array", "minItems": 1, "maxItems": 8, "items": {"type": "number"}}}, "y": {"type": "array", "items": {"type": "integer", "minimum": 0, "maximum": 1}} } } }, "properties": { "train": {"$ref": "#/definitions/table", "title": "Training set", "description": "Feature columns and a 0/1 label"} } ``` The user sees an empty text area ("Paste rows from a spreadsheet. Features first, a 0/1 label last"), a "Use these rows" link and an "Upload CSV" button (`.csv`, `.tsv`, `.txt`). Pasting parses immediately; typed text needs "Use these rows". After a successful parse it shows a summary ("40 rows, 4 features, labels 18 x 0 and 22 x 1"), a preview of the first 6 rows and a "Replace data" link. The value stored is `{"X": [[...]], "y": [...]}`. The editor takes its limits from the schema: rows from `X.minItems` / `X.maxItems`, features per row from `X.items.minItems` / `X.items.maxItems`, feature values from `X.items.items.minimum` / `maximum`, and the label from `y.items` (`minimum`, `maximum`, `enum`; `integer` means whole numbers). Where the schema says nothing the old numbers apply: at least 4 rows, 1 to 8 features, label `0` or `1`. The last column is always the label, a header row is skipped if its first row contains any non-number, every row must have the same width, and a message names the offending row. In the example above that means 4 to 400 rows, 1 to 8 features and a label of 0 or 1. Keep `X`/`y` as the property names: the editor is chosen for any object with both, even without `widget`. ### code From `interaction-energy` / `electrolyte-solvent-screen` (atom coordinates as text): ```json "xyz": { "title": "Geometry", "description": "One atom per line: symbol x y z, in Ångström", "type": "string", "maxLength": 4000, "x-ui": {"widget": "code", "placeholder": "O 0.000 0.000 0.117\nH 0.000 0.757 -0.469\nH 0.000 -0.757 -0.469"} } ``` The user sees a monospace text area (4 to 10 rows, grows with the text) with the placeholder as a greyed sample. The stored value is the raw string including line breaks. Parsing and checking its content is your workflow's job (raise `WorkflowError`). `maxLength` above 200 gives the same area even without the widget. ### slider From `portfolio-optimization`: ```json "risk_aversion": { "title": "Risk aversion", "type": "number", "minimum": 0, "default": 0.5, "x-ui": {"slider": [0, 2, 0.05]} } ``` The user sees a range slider from 0 to 2 in steps of 0.05, a small number box next to it, and the current value in the header. The slider starts at `default` (here 0.5), which is already in the input. The slider itself does not enforce `minimum`/`maximum`: typing 5 in a field with `maximum` 2 turns the box red and blocks Run (and the thumb clamps to the end). Make `slider` and the schema bounds agree. For an `integer` field use step 1. ### percent From `quantum-kernel-classifier` (slider and percent together): ```json "test_fraction": { "title": "Held out for testing", "type": "number", "minimum": 0.1, "maximum": 0.85, "default": 0.33, "x-ui": {"slider": [0.1, 0.85, 0.01], "percent": true} } ``` The user sees a slider whose header reads `33%`, and a box showing `33` with a `%` sign. Typing `40` stores `0.4`. Slider numbers and schema bounds stay as fractions. `percent` can be used without `slider`, giving just the percent box. The header rounds to a whole percent, so use steps of 0.01 or coarser. This is the input key `percent` (a boolean); the output schema's `x-ui.format: "percent"` is a different key and is not read by the input form. ### max_from From `portfolio-optimization`: ```json "num_to_select": {"title": "Assets to pick", "type": "integer", "minimum": 1, "x-ui": {"max_from": "assets"}} ``` The user sees "1 to N" next to the box (with only `minimum` 1 and no `maximum`, N is the number of assets entered), and the form rejects a larger value with "Must be at most N, the number of assets you listed." and blocks Run. Only the browser form knows this rule: the server and the SDK accept a larger value, so also check it in `__init__` and raise `WorkflowError`. ### labels_from Two or more fields stay the same size when the label list changes. In `portfolio-optimization`, `assets` labels `expected_returns` (vector) and `covariance` (matrix); `returns_history` follows with its columns. - Removing the third asset removes the third entry of the vector, the third row and third column of the matrix, and the third column of the history. - Adding an asset appends a `0` entry, row and column. - Any other edit (renaming, reordering by retyping) does not insert or delete: values are only trimmed or padded with `0` at the end. Rename means the same position; removal is detected only when exactly one item disappears and all the others stayed in order. A matrix cannot grow rows or columns on its own; it follows its labels. ### advanced From `generic-qubo`: ```json "offset": {"title": "Constant offset", "type": "number", "default": 0, "x-ui": {"advanced": true}} ``` The user sees the field hidden behind a "More options" link at the bottom of the object it belongs to; clicking expands every advanced field there. It works at any nesting level, hides only the display (the field is still valid, validated and kept), and a required field can be advanced too. Errors inside a collapsed section are not visible: do not mark a `required` field advanced unless the example input already fills it. `advanced` on a field that belongs to an `anyOf` mode is ignored; the mode box wins. ### anyOf alternatives (modes) From `quantum-kernel-classifier`: ```json "anyOf": [ {"title": "Sample dataset", "required": ["dataset"], "x-ui": {"also": ["test_fraction"]}}, {"title": "My data", "required": ["data"], "x-ui": {"also": ["test_fraction"]}}, {"title": "My train and test sets", "required": ["train"], "x-ui": {"also": ["test"]}} ] ``` The user sees a box headed "Describe it with" and a row of buttons titled by each entry. Below it only the fields of the selected alternative: its `required` fields plus its `also` fields. The box sits where the first alternative field appears in your property order; fields that belong to no alternative are drawn normally. | Detail | Behaviour | |---|---| | Title | The entry's `title`. Without one: the titles of its required fields joined by "and", or "Option N". | | Which is selected at start | The first alternative whose required fields are all present in the loaded input; else the first with any of them present; else the first. | | Choosing | A manual choice sticks until another alternative becomes fully filled (for example by loading an example) while the chosen one is not. | | Switching away | The fields of the old alternative that are not in the new one are removed from the input, but remembered so switching back restores them. Fields outside any alternative stay. This keeps the input valid with `additionalProperties: false`. | | Switching in | Each required field of the new alternative gets its remembered value, an existing value, or a starting value (`default`, first enum value, empty object, label-sized zeros). | | Fallback | If any alternative has no `required` and no `also` fields, or the object has no `properties`, the whole `anyOf` is ignored by the form: all fields are drawn flat. The validator still applies it. | | Server | `anyOf` is an ordinary schema rule: any one satisfied entry passes. A user who fills two alternatives still passes. The error for none is "Fill in the fields for the option you chose." | Alternatives work on every object the form draws, including the values of a map (see below), not only the top level. Use `anyOf` of `required` lists only; entries with their own `properties`, or `oneOf`, are not turned into tabs. ### additionalProperties map (keys_from, key_title, key_placeholder, option_labels) An object with no `properties` and a schema in `additionalProperties` is a map from names to values. Two forms. Editable list of entries, from `interaction-energy`: ```json "systems": { "title": "Systems", "type": "object", "minProperties": 1, "maxProperties": 6, "x-ui": {"key_title": "System name", "key_placeholder": "complex"}, "additionalProperties": { "type": "object", "additionalProperties": false, "anyOf": [ {"title": "Known molecule", "required": ["name"]}, {"title": "XYZ coordinates", "required": ["xyz"]} ], "properties": { "name": {"title": "Molecule", "type": "string"}, "xyz": {"type": "string", "x-ui": {"widget": "code"}} } } } ``` The user sees one card per entry. The card header is a text box for the key (spaces become `_`; renaming to an empty or existing name is rejected; Enter or leaving the box commits), a remove cross, and the value's own fields below. "Add system" (from `key_title`, with a trailing " name" removed and lower-cased; `"Name"` gives "Add name") adds an entry named `system_1`, `system_2` and so on. `maxProperties` hides the Add button at the limit. If the value schema is not an object (numbers, text, a switch, a list) each entry shows one editor for the value, labelled with `value_title` (default "Value"); a new number entry starts at `0` (or the `minimum`), text at an empty string. Fixed grid of numbers keyed by another property, from `interaction-energy`: ```json "terms": { "title": "How to combine the energies", "type": "object", "additionalProperties": {"type": "number"}, "x-ui": {"keys_from": "systems", "key_title": "System", "value_title": "Weight"} } ``` The user sees a heading "System → Weight" (from `key_title` and `value_title`, shown when `value_title` is set) and one number box per key found in `systems`, captioned with the key. A placeholder in each box comes from `key_placeholder`, else the value schema's `default`, else its first `examples` entry, else none. If `systems` is empty: "Add systems first." (using the title of the keys property). Emptying every box removes the whole property. Values are drawn as number boxes (whole numbers when the value schema is `integer`), whatever else the value schema says. Enum labels, from `electrolyte-solvent-screen`: ```json "solvents": { "type": "array", "uniqueItems": true, "minItems": 1, "maxItems": 7, "items": {"type": "string", "enum": ["EC", "FEC", "DMC", "DME", "THF", "ACN", "water"]}, "x-ui": {"option_labels": {"EC": "Ethylene carbonate (EC)", "water": "Water"}} } ``` The user sees toggle buttons (stored values are `EC`, `water`, ...) in the enum's order and cannot pick more than `maxItems`. Unlabelled values show their raw text. For a single string enum use `option_labels` the same way (6-31g* shows as "6-31G*"). ### Objects, optional sections and lists of objects From `electrolyte-solvent-screen`: an optional object that starts hidden and advanced. ```json "active_space": { "type": "object", "additionalProperties": false, "required": ["n_electrons", "n_orbitals"], "default": {"n_electrons": 4, "n_orbitals": 4}, "x-ui": {"advanced": true}, "properties": { "n_electrons": {"title": "Electrons", "type": "integer", "minimum": 2, "maximum": 8, "multipleOf": 2}, "n_orbitals": {"title": "Orbitals", "type": "integer", "minimum": 2, "maximum": 6} } } ``` The user sees, under "More options", a dashed "Set active space" button. Pressing it fills the object from its `default` and shows the two boxes in a panel with a "Use default" link that removes the object again (so your code's default applies). `multipleOf` is not hinted; give the rule in the `description`. `custom_solvents` in the same schema is an array of objects: each item is a card with its fields (two code areas and text boxes), a remove cross and an "Add" link. ## Validation: form vs server | Check | In the browser form | On the server | In the SDK / lint | |---|---|---|---| | Valid JSON object | JSON view blocks Run until it parses | `422` | `qsolve validate` | | Types, `required`, `enum`, `minimum`, `maximum`, `minItems`, `maxItems`, `minLength`, `maxLength`, `minProperties`, `maxProperties`, `additionalProperties`, `anyOf`, `uniqueItems`, `multipleOf`, `exclusiveMin/Max` | Ajv (draft-07, all errors, non-strict); Run is blocked and the message shown | `jsonschema`, the schema's draft (`$schema`); first best-matching error returned as `422 invalid_input` | same library before `__init__` (`Invalid input at : `) and on examples (`E023`) | | `format` | not checked | not checked | not checked | | `pattern`, `patternProperties`, `format: "regex"`, non-local `$ref` | n/a | n/a | `E021` at lint | | Schema is valid draft-07 | if it fails to compile the form skips client checks | n/a | `E020` | | Schema size above 256 KB | n/a | n/a | `E022` | | Request body above the limit | n/a | `max_input_bytes`, 1,000,000 bytes by default (verified in the platform settings) | n/a | | `x-ui` | never | never | never | | `max_from`, `symmetric`, `square` | checked by the form (inline message, Run blocked) | not enforced | not enforced | | Dataset editor limits (rows, features, label from `X`/`y`) | entry rules in the editor; the schema's own `minItems`/`maxItems`/`minimum`/`maximum` are checked by Ajv as usual | schema keywords only | schema keywords only | | Solver-setting ranges (`min`/`max` of a parameter) | checked in the run composer before submit | checked by the API | n/a | | Cross-field rules ("node names unique", "num_to_select at most len(assets)", "matrix is symmetric") | no | no | your `__init__` raises `WorkflowError("...")` | Messages in the form (from the form's validator wrapper): | Keyword | Message | |---|---| | `required` | This is required. | | `type` | Must be a whole number. / Must be a number. / Must be a ``. | | `minimum`, `maximum` | Must be at least N. / Must be at most N. | | `minItems`, `minProperties` | Add at least N. | | `maxItems`, `maxProperties` | At most N allowed. | | `minLength` | This can't be empty. (`minLength` 1) / Use at least N characters. (N above 1) | | `x-ui.max_from` | Must be at most N, the number of `` you listed. | | `x-ui.symmetric` | Must be symmetric: `` is a but `` is b. | | `x-ui.square` | Must be square: R rows, but row K has C columns. | | `maxLength` | Keep this under N characters. | | `enum` | Choose one of the options. | | `additionalProperties` | "x" isn't a field this workflow accepts. | | `anyOf` | Fill in the fields for the option you chose. | | anything else | the validator's own message, capitalised | Server and SDK messages use the Python validator's wording (for example `[] is too short`). To make a custom rule readable for users, put it in the `description` or raise `WorkflowError` in `__init__`; both run only after the schema passes. If `$schema` is missing the lint accepts it as draft-07 but the server and SDK pick the validator from `$schema`, falling back to the library's newest draft (unverified in practice). Always write `"$schema": "http://json-schema.org/draft-07/schema#"`. ## Pitfalls Do: - Give every property a `title`, and a `description` with units and the rule the schema cannot express. - Set `additionalProperties: false` on every object you control. - Use a string `enum` (with `option_labels`) for choices. - Put a complete example first in `inputs.examples`; every example must validate (`E023`). - Use `items: {"type": "string", "enum": [...]}` for "pick several". - Match `slider` ranges and `minimum`/`maximum`; match `maxItems` to the manifest `envelope`. - Put label lists before the fields they label, and make them `required` or part of the same alternative; the labelled grid shows "Add names first" until they exist. - Put optional table columns last. - Re-check the form on a local platform preview or the workflow run page after any schema change, because `x-ui` mistakes are silent. - Check cross-field rules in `__init__`. Do not: - Rely on `x-ui` to restrict anything. It is presentation only. - Rely on `max_from`, `symmetric` or `square` for correctness outside the browser form: API and SDK callers skip them. Re-check in `__init__`. - Use `pattern`/`patternProperties`/`format: "regex"`, or a `$ref` that is not `#/...` (lint errors `E021`). - Name a property `pattern` or `patternProperties`: lint scans all keys and reports `E021` even for a property of that name (the check does not distinguish keywords from property names). Rename the field, for example `motif`. (Observed in the lint source; not run.) - Use a property with only `oneOf`/`anyOf`, or a `type` list that mixes other types. The form draws the first non-null type or a raw JSON box. - Use tuple-form `items: [a, b]` (JSON box). - Use `enum` of numbers expecting a drop-down. - Count on `default` for API or SDK callers: only the form fills defaults (see [Defaults](#defaults)). - Put commas in values that users will enter as chips; they split on commas. - Rely on `x-order` written by hand; it is overwritten on publish. ## Checklist before publishing 1. `$schema` is `http://json-schema.org/draft-07/schema#`; top level is `"type": "object"`. 2. `qsolve validate .` and `qsolve lint .` pass: no `E020` to `E024`. 3. No `pattern`, `patternProperties`, `format: "regex"`, or remote `$ref`; no property named `pattern`. 4. Schema is below 256 KB; each example input and any pasted data stays below the request limit (1,000,000 bytes by default). 5. Every property has a `title`; those with units, formats or hidden rules have a `description`. 6. Every object has `additionalProperties: false` (unless it is a deliberate map). 7. Bounds (`minItems`, `maxItems`, `minimum`, `maximum`, `maxLength`) are set and agree with the manifest `envelope`. 8. Every `x-ui` key was checked against this page's table for the right shape and spelled exactly; every `labels_from`, `column_labels_from`, `options_from`, `max_from`, `keys_from` names a real sibling property of the right kind. 9. `matrix`, `vector`, `table`, `dataset` and `code` fields were opened in the form and tried with a spreadsheet paste. 10. Every `default` is repeated in the workflow code; `max_from`, symmetry and other cross-field rules are also checked in `__init__`. 11. If the workflow uses alternatives, each `anyOf` entry has a `title` and `required` fields, and `additionalProperties: false` is set so switching alternatives never leaves stale fields. 12. The first example input is complete and loads cleanly; blank form and each alternative were tried. 13. `advanced` is used only on fields that are optional or already filled. --- # Output schema reference > Everything about schemas/output.json: how the answer is validated, how each JSON shape is drawn for users, every x-ui hint the answer view reads, what happens on misuse, with examples from published workflows. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/output-schema-reference · Updated: 2026-11-13 `schemas/output.json` is a JSON Schema (draft-07) for the dictionary your `answer()` returns. It does two jobs: Qsolve validates the quantum answer against it, and the answer view uses it for labels, help text, units, number formats and precision. This page lists every key the code reads, every shape and how it is drawn, and what goes wrong when you misuse them. For the rules shared with the input schema (draft-07, no `pattern`, local `$ref` only, 256 KB), see [Input and output schemas](/docs/build/input-and-output-schemas). For the input side, see [Input schema reference](/docs/build/input-schema-reference). ## What the schema does, and does not do | Question | Answer | |---|---| | Is the quantum answer validated against it? | Yes, always, by `jsonschema.validate` after `answer()` returns. | | Are the other methods' answers validated? | No. They are shown as returned; if their `answer()` raises, that method simply has no answer. | | Does the schema decide what the headline accuracy is? | No. The headline comes from the manifest's `metric` and is computed by the platform. Nothing in `output.json` can change it. See [Benchmarks](/docs/build/benchmarks). | | Does the schema decide the order of the answer? | Only through `x-order`, if you write one (see [Order](#order)). | | Does a wrong `x-ui` hint reject an answer? | No. Validation ignores `x-ui`. A misspelled or unsupported hint is silently dropped. | | Are the other methods' answers shown? | Yes, on the run page, under the answer, with the same schema. | ## Validation The run does these steps in order: 1. The quantum method and the reference run. If either produced no value, the run fails. 2. `answer()` is called on the quantum outcome. Any exception becomes `answer() failed on the quantum result: : `. 3. The answer is validated against the output schema. A mismatch fails the run with: ```text Workflow produced an invalid answer: ``` The message is the first error the validator finds, for example `'energy_hartree' is a required property` or `'abc' is not of type 'number'`. It does not name the path, so keep your schema specific and test with `qsolve run`. 4. The answer becomes the `answer` field of the result. It must be a dictionary: the result model requires an object, so declare `"type": "object"` at the top. Method answers (for the non-quantum methods) are produced by `answer()` too, inside a try block. If it raises, that method's answer is `None` and no answer is offered for it. They are never checked against the schema, so an answer with the wrong shape for a classical method is still displayed, drawn from its shape with whatever hints match. Allow `null` where a value can be missing. Declaring `"type": ["number", "null"]` lets `answer()` return `None`; a plain `"type": "number"` fails the run on `None`. The answer view draws a null as `–`. ## Where the answer is shown | Place | Schema used? | Notes | |---|---|---| | Run page, "Answer" card | Yes | The run's own version's output schema is delivered with the run. Titles, help text, units, formats and precision apply. | | Run page, ": its answer" under the Answer card | Yes | One per non-quantum method that returned an answer (runs recorded before methods were stored have none). Same schema, but not validated against it (see above). | | Workflow page, benchmark detail, "The quantum answer" | Yes | Entries over 20,000 characters are left out (see [Large values](#large-values)). | | Workflow page, benchmark detail, "Show its answer" on a method card | Yes | One per method that has an answer. | All three use one renderer, so a hint written once behaves the same everywhere. A run is drawn with the schema of the version it ran, not the latest one. The platform also shows the run's `summary` (your optional `summary()` method, or a default sentence), the quantum-versus-classical comparison, and the quality card, none of which come from `output.json`. ## How shapes are drawn The answer view knows nothing about any workflow. It looks at the value's shape. The label of a field is its `title` if the schema has one at that place, otherwise the key turned into words (`num_to_select` becomes "Num to select"; underscores and dashes become spaces, a camel-case boundary gets a space, the first letter is capitalised, keys of two characters or fewer are left as written). | JSON value | What the user sees | |---|---| | number, string, boolean, `null` at an object level | A labelled figure with a copy button (copies the value as returned). All of an object's scalars are gathered in a grid at the top of its section. | | string longer than 28 characters | The same figure in smaller text instead of large numerals. Long text wraps. | | array of scalars | Chips, one per item, in order. Empty array: the word "none". | | array of objects | A table, one row per object. See [Tables](#tables). | | object whose values are all objects | A table, one row per key, first column the key. See [Tables](#tables). | | any other object | A titled box holding the object's own figures, chips and tables. See [Nested objects](#nested-objects). | | array of arrays, or mixed array | Chips whose text is the item as JSON, for example `["a","b"]`. No schema hints are applied. | | any value over 20,000 characters | A preview with "Show all" and "Download". See [Large values](#large-values). | Everything that is not a scalar (arrays and objects) is drawn after the figures, each under a small heading with its label. ### Scalars | Value | Display | |---|---| | `true` / `false` | `Yes` / `No` | | `null` or missing | `–` | | integer (a JSON number with no fractional part) | Thousands separators, English style: `12345` shows `12,345`. | | other number | Six significant digits (`0.1234567` shows `0.123457`). Magnitudes of 100,000 or more, or under 0.001, use exponent form with six significant digits (`1.23457e+5`, `5.00000e-4`). Zero shows `0`. | | string | As written. | Because Python's `3.0` is the JSON number `3.0`, which the browser reads as an integer, a float with no fractional part is drawn as an integer. ### `x-ui.unit` A string appended after a numeric value, after a non-breaking space (so the unit never wraps onto its own line): `-1.137 Ha`, `12 kcal/mol`. It applies to numbers only; on a string or boolean it does nothing. The unit is plain text and is not interpreted, so `Ha` and `mHa` are different strings and no conversion happens. Put the hint on the property that holds the number, on the array or object of numbers, or on `items`. In a table column, the unit goes in the column header (`Quantum energy (Ha)`) and is removed from the cells. ### `x-ui.format` | Value | Display | |---|---| | `"percent"` | The number is a fraction: `0.143` shows `14.3%`. Any size is shown as it is: `1.43` shows `143.0%`, `2.5` shows `250.0%`. `unit` is ignored. | | `"integer"` | Rounded to a whole number with thousands separators: `1234.6` shows `1,235`. `unit` still applies. | | `"scientific"` | Exponent form with 4 significant digits: `0.00001236` shows `1.236e-5`; zero shows `0`. `unit` still applies. | | anything else | Ignored. | Percent details: one decimal by default (`0.5` shows `50.0%`, `-0.05` shows `-5.0%`); from 99% up to just under 100%, two decimals, cut rather than rounded, so `0.99997` shows `99.99%` and a value below 1 never reads `100%` by rounding. Exactly `1` shows `100.0%`. On non-numbers the format is ignored. ### `x-ui.precision` A whole number from 0 to 12, ignored if it is anything else. For `percent` it is the number of decimals (`0.14349` with precision 2 shows `14.35%`). For plain numbers it is the number of significant digits (default 6): `3.14159265` with precision 3 shows `3.14`. For `scientific` it is the significant digits (default 4). It does not apply to `integer`. A whole number with a precision set is drawn with that many significant digits, not with separators. If your number is already a percentage (for example 87.2), do not use `format`; use `"x-ui": {"unit": "%"}` as `interaction-energy` does for `correlation_recovered_pct`. ### Lists of scalars Each item is drawn as a chip. `unit`, `format` and `precision` written on `items`, or on the array property itself, apply to every chip (the hint on `items` wins). `quantum-kernel-classifier` shows both: its `test_accuracy_ci95` array puts `x-ui: {format: percent}` on `items`, so the two chips read `78.3%` and `92.1%`. ### Tables A list of records, or a record of records, becomes a table: - Columns are every field found in the rows, in the order of the row schema's `x-order` if present, then the order first seen. None are dropped; a wide table scrolls sideways (keyboard users can focus it and use the arrow keys). - A field whose value is an array of scalars is shown as comma-separated text, and any other array or object as short JSON (cut at 80 characters, full text on hover). - A row missing a field shows `–` in that cell. - Column header: the `title` of that field in the row schema, else the key in words, plus `(unit)` if it has `x-ui.unit`. Headers are `th scope="col"`; for a record of records the key of each row is a `th scope="row"`. - Cells apply `format`, `precision` and the number rules above. The unit is not repeated in the cells. - For a list, the row schema is `items`. For a record of records, it is `additionalProperties`, used when it is a schema (not `true`/`false`). - For a record of records the first column holds the record keys in words, for example `h2` becomes "H2". Column schema lookup is through the row schema's `properties`. ### Nested objects An object that is not a table is drawn as a shaded box with its own figures, lists and tables, and its fields get titles from the object's `properties`. The answer itself is level 0. Objects at levels 1 and 2 are open boxes. Deeper objects (level 3 and below) sit behind a "Show N fields" control, collapsed until opened. An object nested deeper than 6 levels is shown as JSON behind "Show all". A record of records is a table at any depth. ### Help text, references and nullable values | Keyword | Effect on display | |---|---| | `description` | Help text behind an (i) next to the label of a figure or section. Not shown on table column headers. | | `$ref` to `#/definitions/...` or `#/$defs/...` | Followed. Keywords next to the `$ref` win over the referenced ones; `x-ui` is merged. Remote and missing references are ignored. | | `anyOf` / `oneOf` | A `{"type": "null"}` branch is dropped, so a nullable number keeps its title, unit and format. With several non-null branches their `properties` are merged and the first branch's other keywords are used. | | `allOf` | Merged in. | | `type`, `enum`, `minimum`, `maximum`, `required`, `default`, `if`/`then` | Validation only. The drawing follows the actual value. | | `x-ui` keys other than `unit`, `format`, `precision` | The input form's hints (`widget`, `slider`, `columns` and so on) have no effect on the answer. | A hint on an array or on an object whose values are numbers (`weights`) applies to every number inside it that has no hint of its own. In a table, hints come from the row schema's `properties` only. ### Keys that are data Per-key titles for a record whose keys are data (asset names) are not possible: those labels are the keys in words. For per-asset titles or units, return a list of records, for example `[{"asset": "AAPL", "weight": 0.25}]`, and declare `weight` under `items.properties`. ### Order Fields appear in the order of `x-order` if the schema has one at that level (a list of key names; keys it does not name follow in the order the answer had), then in the order of the stored answer. Scalars are grouped first, then lists and objects. The schema's `properties` order is not used: the database stores objects with sorted keys, and the platform records `x-order` for the input schema only. A hand-written `x-order` in `output.json` is stored as written. Without it, an answer's own key order is not guaranteed. ### Large values On the run page, any value whose JSON text is longer than **20,000 characters** (a string, an array, an object, or a list with more than 10,000 items) is not drawn in full. The user sees its size, the first 600 characters, "Show all" (up to 200,000 characters, scrollable) and "Download" (the whole value as a file). Nothing inside it is formatted. A value under the limit is drawn normally. When a benchmark result is stored for the detail view, the answer is compacted separately: every top-level entry whose JSON text is longer than 20,000 characters (Python's default separators) is dropped from "The quantum answer", and a dropped entry leaves no heading. Nothing nested is trimmed. Put long per-point data (predictions per test point, full histograms) under its own top-level key so only that entry is affected. For `quantum-kernel-classifier`, `predictions` and `baseline_predictions` are such keys. Keep the answer small, and put bulk data in `Outcome.details`, which is kept with the receipt. ## What the platform stores and shows beyond the answer None of these come from `output.json`; they are measured by the SDK harness and the platform. Do not put them in your answer to appear to supply them. | Item | Source | Shown as | |---|---|---| | Headline accuracy and error bar | The manifest's `metric` kind, computed from the method values | Large percentage in benchmark detail, with the metric `note` below it. `matches` produces the verdict "Matches the best classical answer" or "Below the best classical answer". | | Each method's `value`, `stderr`, `vs_reference`, `seconds` | Outcomes of each method | "Every method, on this input" table, with the objective's label, unit and decimals from the manifest's `objective`. | | `verified` per method | `recomputed`: the method returned a `solution` and the SDK re-evaluated it with `evaluate()`. `reported`: no solution to re-evaluate, so it is the workflow's own figure. | "Re-evaluated by Qsolve" or "Reported by the workflow". A re-evaluated value that differs from what the method claimed is recorded (`claimed`) and fails validation. | | Mitigation: `fidelity_raw`, `fidelity_mitigated`, `fidelity_floor`, `postselection_rate`, `methods`, `source`, `coverage` | Measured by the SDK from recorded circuits and counts. The mitigated figure comes from what the workflow reports through `ctx.report_mitigation`, recomputed against the ideal output; with no report it equals the raw figure. | Fidelity tiles and track ("measured" or "reported"), shots kept, gain, step list, source (real hardware, device noise model, ideal simulator). See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). | | Circuit depth, two-qubit gates, qubits, circuits, shots | Recorded by the SDK | Quality card | | `summary` | Your `summary()` method, else `": ; : (accuracy )."`, cut to 500 characters | Sentence above the answer | | `Outcome.details` | Your method | "How the solver got there"; kept with the receipt if under 200,000 bytes, otherwise replaced with a note that it was omitted. | The headline metric `error_rate` needs `n_test` in the quantum method's `Outcome.details`; see [The manifest](/docs/build/manifest#metric). ## Worked examples ### Units on a scalar (`molecular-ground-state`) ```json "energy_hartree": {"type": "number", "title": "Quantum energy", "x-ui": {"unit": "Ha"}}, "error_vs_exact_mha": {"type": "number", "title": "Error vs exact", "x-ui": {"unit": "mHa"}}, "within_chemical_accuracy": {"type": "boolean", "title": "Within chemical accuracy"} ``` In the benchmark detail the user sees three figures in a grid, for example "Quantum energy" `-1.13727 Ha` and "Error vs exact" `0.4 mHa` (illustrative values: six significant digits, then the unit string), and "Within chemical accuracy" `Yes`. ### Nullable value (`molecular-ground-state`) ```json "ccsd_t_full_space_hartree": {"type": ["number", "null"], "title": "CCSD(T), full space", "x-ui": {"unit": "Ha"}} ``` When the classical CCSD(T) check was not computed (above 60 basis functions), `answer()` returns `None` and the user sees "CCSD(T), full space" with `–`. ### A record of records as a table (`molecular-ground-state`, `interaction-energy`) ```json "systems": { "type": "object", "title": "Systems computed", "additionalProperties": { "properties": { "energy_hartree": {"title": "Quantum energy", "x-ui": {"unit": "Ha"}}, "stderr_hartree": {"title": "Error bar", "x-ui": {"unit": "Ha"}}, "qubits": {"title": "Qubits"} } } } ``` The user sees a table headed "Systems computed", one row per system key, columns "Quantum energy (Ha)", "Error bar (Ha)", "Qubits". The row schema sits under `additionalProperties`; the nested `properties` provide the titles and units. ### A list of records as a ranked table (`electrolyte-solvent-screen`) `ranking` is an array of objects with 11 fields. The schema's `items.properties` give each column a title and, for energies, `unit: "kcal/mol"`. All 11 columns are shown and the table scrolls sideways on a narrow screen; put the fields you most want seen first (or list them in `x-order` on `items`). Nullable columns such as `noise_free_kcal_mol` and `mp2_full_space_kcal_mol` show `–` when null. ### Percent figures (`portfolio-optimization`, `quantum-kernel-classifier`) ```json "expected_return": {"type": "number", "title": "Expected return", "x-ui": {"format": "percent"}}, "test_accuracy": {"type": "number", "minimum": 0, "maximum": 1, "title": "Test accuracy", "x-ui": {"format": "percent"}} ``` `0.143` shows `14.3%`; `0.995` shows `99.5%` (99.50% at the default precision); `1.43` shows `143.0%`. The constraints `minimum` and `maximum` guard validation and have no visual effect. ### Lists of words as chips (`max-cut`) ```json "group_a": {"type": "array", "items": {"type": "string"}, "title": "Group A"} ``` The user sees "Group A" followed by one chip per node. `cut_weight` and `total_weight` show as numbers in the grid above, titled "Weight of the cut" and "Total edge weight". ### A small object of figures (`quantum-kernel-classifier`) ```json "confusion_matrix": { "type": "object", "title": "Confusion matrix", "properties": {"tp": {"title": "True positives"}, "tn": {"title": "True negatives"}, "fp": {"title": "False positives"}, "fn": {"title": "False negatives"}} } ``` The user sees a box headed "Confusion matrix" with four labelled integers. The sub-properties have no `type`, so the schema only titles them and does not constrain them; that is valid, and the display does not need `type`. ### A bulk key (`quantum-kernel-classifier`) ### A bulk key (`quantum-kernel-classifier`) `predictions` is an array of integers with no title: the heading reads "Predictions". For a large test set its JSON text can pass 20,000 characters (roughly 6,600 one-digit entries with separators). On the run page it then shows as a preview with "Show all" and "Download"; in the benchmark detail the entry is left out while the other fields stay. ### A mapping with data keys (`generic-qubo`, `portfolio-optimization`) ```json "assignment": {"type": "object", "additionalProperties": {"type": "integer"}, "title": "Assignment (variable to 0 or 1)"}, "weights": {"type": "object", "additionalProperties": {"type": "number"}, "title": "Portfolio weights", "x-ui": {"format": "percent"}} ``` The user sees a box headed with the title and one labelled figure per variable (the variable names in words). Hints on the object, or on `additionalProperties`, apply to every value: the weights `{"AAPL": 0.25, "MSFT": 1.43}` read `25.0%` and `143.0%`. Titles per variable are not possible, because the schema does not list the keys. ## Misuse and what happens | Mistake | What happens | |---|---| | `answer()` returns a key the schema does not list, and the schema allows extras (the default) | Valid. Displayed with its key as a label and no hints. | | Same, with `"additionalProperties": false` at the top | Run fails: `Workflow produced an invalid answer: Additional properties are not allowed ('x' was unexpected)`. | | A required key is missing | Run fails: `Workflow produced an invalid answer: 'x' is a required property`. | | A wrong type (for example `None` for `number`) | Run fails: `Workflow produced an invalid answer: None is not of type 'number'`. | | `answer()` raises | Run fails: `answer() failed on the quantum result: : `. | | `answer()` returns a non-dict | The schema should reject it (`"type": "object"`); without that, building the result fails. | | `pattern`, `patternProperties`, remote `$ref`, `format: regex`, over 256 KB | Lint errors `E021` and `E022`; the workflow cannot be published. | | Hint on a node that cannot use it (`format: percent` on a string, `unit` on a string) | Silently ignored for that value. | | `format` other than `percent`, `integer`, `scientific` | Silently ignored. | | `precision` that is not a whole number from 0 to 12 | Silently ignored. | | `$ref` to something that does not exist, or a remote `$ref` | Ignored: the property is drawn without the referenced hints. | | Several branches in `anyOf` / `oneOf` | Their `properties` are merged and the first branch's other keywords win; no branch is chosen by the value. | | Table with many columns | All columns are shown; the table scrolls sideways. | | Object nested more than 6 levels deep | Shown as JSON behind a "Show all" control. | | Value over 20,000 characters | Collapsed to a preview with "Show all" and "Download". | | NaN or infinity in the answer | Not valid JSON. Avoid: return `None` with a nullable type instead. | ## Pre-publish checklist - [ ] `"type": "object"` at the top, and `required` lists every field that `answer()` always returns. - [ ] Every field that may be missing is declared `["type", "null"]` (or `anyOf` with `{"type": "null"}`), and `answer()` returns `None` there, not a string such as `"n/a"`. - [ ] Every property you want labelled has a `title` written as a person would say it, and a `description` where a user may ask what it means. - [ ] Every physical quantity has `x-ui.unit`, on the property holding the number (or on the array, the object of numbers, or the row schema `properties` for a table). - [ ] `format: percent` is used only on fractions (0.143 for 14.3%). Percentages already multiplied by 100 use `unit: "%"`. - [ ] Bulk data is in `Outcome.details` or in its own top-level key, not inside the answer's main fields. - [ ] If field order matters, `x-order` lists it (see [Order](#order)). - [ ] Key names still read acceptably as words for any key you leave without a `title`. - [ ] `qsolve lint .` passes, and `qsolve run` on an example finishes without `Workflow produced an invalid answer`. ```bash qsolve validate . # loads both schemas, validates the example inputs qsolve lint . # E020 to E023 on the schemas qsolve test . # runs the example and the checks end to end ``` ## See also - [Input and output schemas](/docs/build/input-and-output-schemas) - [Input schema reference](/docs/build/input-schema-reference) - [Writing the workflow](/docs/build/writing-the-workflow) - [The manifest](/docs/build/manifest) - [Benchmarks](/docs/build/benchmarks) --- # Parameters > How to declare parameters in qsolve.yaml: every field, the five types and their exact checks, ranges and choices, how defaults and overrides are validated, how the run form shows them and how they reach your code. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/parameters · Updated: 2026-11-13 Parameters are settings a user can change without changing the problem: shots, circuit depth, an iteration count, a mode switch. Declare each under `parameters` in `qsolve.yaml`; Qsolve checks every value before your code runs. Parameters are separate from the problem input, which is described by the [input schema](/docs/build/input-and-output-schemas). ## Declaring a parameter ```yaml parameters: - {name: layers, label: Circuit depth, type: int, default: 3, min: 1, max: 8, description: "More layers can find better answers but take longer to train"} - {name: shots, label: Final shots, type: int, default: 4000, min: 100, max: 20000, description: Final sampling shots} - {name: train_on, label: Train angles on, type: enum, default: sim, choices: [sim, backend], description: "sim trains noise-free and uses the device only to measure"} - {name: penalty_scale, label: Penalty strength, type: float, default: 1.0, min: 0.1, max: 20, description: Cardinality penalty multiplier} ``` The first three come from `max-cut`, the last from `portfolio-optimization`. Every key is one of these seven, and any other key in an entry fails the manifest load: | Key | Type | Required | Default | Meaning and constraints | |---|---|---|---|---| | `name` | string | Yes | | The key your code reads from `self.params`. Unique within the manifest (`parameter names must be unique`). The model puts no restriction on the characters, so use a Python identifier (`optimizer_iters`) to keep it usable as a key and in `-p name=value`. | | `type` | string | Yes | | Exactly `int`, `float`, `bool`, `str` or `enum`. | | `default` | any | Yes | | Used when the user gives no value. It is checked with the same rules as a user's value, so a default that fails them makes the manifest invalid with that rule's message. There is no way to declare "no default". | | `label` | string | No | `""` | The name shown in the form, at most 60 characters. When empty, the form shows the name with `_` and `-` replaced by spaces and the first letter capitalised (`optimizer_iters` shows as "Optimizer iters"). | | `description` | string | No | `""` | What changing it does. No length limit in the model. Shown as an info tooltip next to the label. | | `min` | number | No | none | Inclusive lower bound. Read only for `int` and `float`; ignored for other types. | | `max` | number | No | none | Inclusive upper bound. Same rules as `min`. | | `choices` | list | For `enum` | none | The allowed values. Read only for `enum`; ignored for other types. | Checked when the manifest loads (a failure makes the manifest invalid with the message shown): `min` greater than `max` (`parameter 'x': min 5.0 is greater than max 1.0`), an `enum` with no or an empty `choices` list (`parameter 'x': an enum needs a non-empty `choices` list`), and a `min` or `max` that is NaN or infinite. A `default` is checked like any value. The choices of an `enum` are not required to be unique. ## The five types | Type | Accepts | Rejects | Error message (`'x'` is the parameter name) | |---|---|---|---| | `int` | whole numbers, including negative | booleans, numbers with a fractional part or written as `3.0`, strings | `parameter 'x' must be an int` | | `float` | any finite number, integers included | booleans, NaN, infinity, strings | `parameter 'x' must be a number` (NaN and infinity: `must be a finite number`) | | `bool` | `true` or `false` | anything else, including `0` and `1` | `parameter 'x' must be a bool` | | `str` | text, any length, including empty | anything else | `parameter 'x' must be a string` | | `enum` | a value equal to one of `choices` (a bool never equals the int `1`: `true` is not the choice `1`) | any other value | `parameter 'x' must be one of ['sim', 'backend']` | For `int` and `float` the bounds are checked after the type, and a value outside them is rejected with `parameter 'shots' must be >= 100.0` or `parameter 'shots' must be <= 20000.0`. Bounds are stored as numbers, so the message shows a decimal point even for whole bounds. Bounds never apply to `bool`, `str` or `enum`. NaN and infinity never pass for `int` or `float`, so a NaN cannot slip through a bound. Values are not converted. A `float` parameter keeps an integer as an integer (`default: 1` reaches your code as `1`, not `1.0`), so convert in your code when it matters: `float(self.params["penalty_scale"])`. A `str` parameter has no length limit or pattern. If you need a closed set of strings, use `enum`. ## How values reach your code Qsolve resolves the parameters before it builds your class: it starts from the defaults, overlays the user's overrides and checks every value. The result is a dictionary holding every declared parameter, which is passed to `__init__` and stored as `self.params`: ```python p, shots = int(self.params["layers"]), int(self.params["shots"]) ``` - **Every declared name is present.** You can index `self.params` without checking for missing keys. - **Every value has passed the checks above.** Do not re-validate type and range. - **Undeclared names never arrive.** An override naming an undeclared parameter is rejected with `unknown parameters: ['name']` (the names are sorted). - **The resolved set is recorded.** It is stored with the run, shown on the run page and kept in the receipt (`provenance.params`), so a run records the values it really used, defaults included. - **Parameters are also passed to `estimate()`** through `self.params`, because `estimate()` is a method of the same instance. The seed is not a parameter. It is a separate field of every run and reaches your code as `ctx.seed`. ## Where overrides come from | Place | How | When a value is wrong | |---|---|---| | Local run | `qsolve run . -i input.json -p layers=1 -p train_on=backend`. The text after `=` is parsed as JSON first, so `500` is a number, `true` is a bool, `0.5` is a float and `fast` (not valid JSON) stays text. | `Error:` plus the message, exit code 1. A `-p` without `=` is a usage error: `--param expects key=value`. | | Benchmark case | `params:` in `benchmarks/cases.yaml`. Missing names take the default. See [Benchmarks](/docs/build/benchmarks). | The case fails with the message. | | Run form | Every declared parameter gets a control (see below). | The API answers `422 invalid_params` with the message. | | API | The `params` object of `POST /v1/runs`; names omitted take the default. | `422 invalid_params`, message as above. | A string value such as `"500"` is a `str`, not an `int`. Over the API and in YAML, write numbers as numbers. ## How the run form shows them The run page collects the parameters in a collapsible section called "Solver settings", closed by default and showing "Tuned defaults" until something differs from the default, then "N changed". The controls follow the type: | Type | Control | Notes | |---|---|---| | `enum` | A row of buttons, one per choice | Shows each choice as text. | | `bool` | A switch | | | `int`, `float` | A number box; for `int` a typed value is rounded to a whole number | When both `min` and `max` are set, the range is printed beside it ("100 to 20,000"); outside the range the box is marked invalid. | | `str` | A text box | | Every control is labelled with `label` (or the generated name) and carries `description` as a tooltip. A "Reset to defaults" button appears once any value is changed. The form always submits all parameters, defaults included, in declared order of the manifest. Two behaviours to design around: - **Give both bounds, or the range is not shown.** With only a `min` or only a `max`, the box is still marked invalid when you leave the bound, but no range text appears. - **The form does not stop an out-of-range number by itself.** The input form's own validity check covers the problem input; the parameter values are checked on the server, which refuses the run with `invalid_params` and the message above. Put bounds in the manifest and rely on that check. - **Use string choices for an `enum`.** The form sends the clicked choice as text, so an enum declared with numeric choices such as `[1, 2]` would be submitted as `"1"` and refused with `must be one of [1, 2]`. Use `[one, two]`-style text, and convert in your code. Parameter controls have no `x-ui` hints: those apply to the problem input only. They are described in the [input schema reference](/docs/build/input-schema-reference). ## Choosing parameters well Expose the settings that change cost, accuracy or run time, and give each a bound that keeps a run inside your `resources.timeout_s` and at a sensible cost. A `shots` parameter with no `max` lets a user ask for a run you did not plan for, and the platform runs at most 100,000 shots per circuit (`qsolve.MAX_SHOTS_PER_CIRCUIT`): lint `E025` is an error when a `shots` parameter has no `max`, a `max` above that or a `default` above it. - **Name the shots parameter `shots`.** When a workflow gives no `estimate()`, the default estimate uses `params["shots"]` (and `0` if there is none) as its total shots, with one circuit, and is marked as a default; see [Writing the workflow](/docs/build/writing-the-workflow). - **Use an `enum` for modes, not a string.** `constraint_mode: [mixer, penalty]` is checked, a free string is not. - **Make the default the setting you would recommend.** Researchers run defaults. In `max-cut` the defaults are 3 layers, 4000 final shots and noise-free training (`train_on: sim`). - **If changing a setting changes which problem is solved, it is an input, not a parameter.** - **Reflect cost-driving parameters in your `estimate()`.** A run that sends many more circuits than estimated fails the `estimate_honest` gate. In the examples, `train_on: backend` sends one circuit per optimiser step to the target, and the estimate counts them, which is why `train_shots` and `optimizer_iters` are bounded (`max: 8000` and `max: 400` in `max-cut`). - **Bound by the limits you advertise.** `quantum-kernel-classifier` uses `min: 0` with the documented meaning "0 = auto-tune" for `reps` and `bandwidth`; a sentinel like this is a normal int or float, so say what it means in `description`. - **Defaults must be cheap enough to validate.** The platform runs your benchmark cases with their `params`, falling back to the defaults, inside the validation time limit. See [Writing the workflow](/docs/build/writing-the-workflow) for `estimate()` and [The manifest](/docs/build/manifest) for the rest of `qsolve.yaml`. Every lint code is in [Lint codes](/docs/build/lint-codes). --- # SDK reference > Overview of the qsolve-sdk Python package: install, import map of every module, how the pieces fit together, and which names are public API and which are internal. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-reference · Updated: 2026-11-13 `qsolve-sdk` is the bridge between a plain Qiskit workflow and the Qsolve platform. It contains no quantum algorithm. You bring circuits, error mitigation and classical methods; the SDK runs them on Qsolve's backends, records every circuit that leaves your code, re-evaluates every answer and measures the numbers. The platform runs this same package inside its sandbox, so a workflow that passes `qsolve test` on your machine behaves the same on Qsolve. This page is the map. Each area has its own reference page with every public name, its signature, parameters, return value, exceptions and an example. ## Install ```bash pip install qsolve-sdk # the package; Python 3.12 or newer, Qiskit 2.x pip install "qsolve-sdk[ibm]" # also the IBM runtime: device noise models and IBM hardware ``` The package installs the `qsolve` command (see [The qsolve command line](/docs/build/cli)) and the Python package `qsolve`. The `ibm` extra is needed for `fake:` noise-model targets as well as for real IBM hardware, so install it if you use either (the scaffold's third benchmark case uses a noise model). ```python import qsolve print(qsolve.__version__) # for example "1.0.0"; "0.0.0+local" when run from an uninstalled source tree ``` ## What you import Everything a workflow needs is exported from the top-level package. These 24 names are the whole of `qsolve.__all__`: | Name | Kind | Reference page | |---|---|---| | `Workflow` | base class you subclass | [The Workflow class](/docs/build/sdk-workflow-class) | | `Outcome` | dataclass every method returns | [The Workflow class](/docs/build/sdk-workflow-class) | | `Context` | dataclass passed to every method | [The Workflow class](/docs/build/sdk-workflow-class) | | `WorkflowError` | exception | [The Workflow class](/docs/build/sdk-workflow-class) | | `InvalidSolution` | exception | [The Workflow class](/docs/build/sdk-workflow-class) | | `MethodUnavailable` | exception | [The Workflow class](/docs/build/sdk-workflow-class) | | `QuantumNotAllowed` | exception | [The Workflow class](/docs/build/sdk-workflow-class) | | `Estimate` | pydantic model (pre-run estimate) | [Results and events](/docs/build/sdk-results-and-events) | | `Result` | pydantic model | [Results and events](/docs/build/sdk-results-and-events) | | `Provenance` | pydantic model | [Results and events](/docs/build/sdk-results-and-events) | | `Quality` | pydantic model | [Results and events](/docs/build/sdk-results-and-events) | | `Mitigation` | pydantic model | [Results and events](/docs/build/sdk-results-and-events) | | `Uncertainty` | pydantic model | [Results and events](/docs/build/sdk-results-and-events) | | `MethodResult` | pydantic model (one method's measured result) | [Results and events](/docs/build/sdk-results-and-events) | | `Metrics` | pydantic model (headline accuracy) | [Results and events](/docs/build/sdk-results-and-events) | | `CircuitStats` | pydantic model (circuit size and shots) | [Results and events](/docs/build/sdk-results-and-events) | | `RunOutput` | pydantic model returned by `run_workflow` | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `MAX_SHOTS_PER_CIRCUIT` | constant, `100000`: the most shots the platform runs per circuit | [The manifest](/docs/build/manifest) | | `Manifest` | pydantic model of `qsolve.yaml` | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `load_workflow` | function | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `run_workflow` | function | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `estimate_run` | function | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `__version__` | string | this page | A workflow's own code normally needs only the first seven names plus `Estimate`: ```python from qsolve import Context, Estimate, InvalidSolution, MethodUnavailable, Outcome, Workflow, WorkflowError ``` ## Module map Names outside `qsolve.__all__` are importable from their module (`from qsolve.metering import fidelity`) and are documented in the pages below, but they are the harness's own machinery, not the contract with your workflow. See [What is stable](#what-is-stable). | Module | What it holds | Page | |---|---|---| | `qsolve.workflow` | `Workflow`, `Outcome`, `Context` and the four exceptions: the contract | [The Workflow class](/docs/build/sdk-workflow-class) | | `qsolve.runner` | `run_workflow`, `estimate_run`, `validate_input`, `RunOutput`: the harness the platform runs | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `qsolve.manifest` | `Manifest` and its parts, `load_workflow`, `LoadedWorkflow`: the parsed `qsolve.yaml` | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `qsolve.comparison` | `run_methods`, `headline`, `implementation`: runs every declared method and computes accuracy | [Runner and manifest](/docs/build/sdk-runner-and-manifest) | | `qsolve.results` | `Result`, `Provenance`, `Estimate`, `Quality`, `Mitigation`, `CircuitStats`, `MethodResult`, `Metrics`, `Uncertainty` | [Results and events](/docs/build/sdk-results-and-events) | | `qsolve.events` | `emit`, `capture`, `Sink`: progress events | [Results and events](/docs/build/sdk-results-and-events) | | `qsolve.circuit_record` | `circuit_ops`, `circuit_summary`, `active_qubits`, `lowered`, `DRAWABLE`: circuits as drawable records | [Results and events](/docs/build/sdk-results-and-events) | | `qsolve.backends` | `Backend`, `AsyncBackend`, `SampleResult`, `ProviderJobFailed`, `AerBackend`, `get_backend`, plus the `fake`, `ibm`, `remote` and `hardware` submodules | [Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge) | | `qsolve.bridge` | `QsolveBackend`, `QsolveSampler`, `QsolveEstimator`: the Qiskit-native objects in `ctx` | [Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge) | | `qsolve.sim` | simulator threading and run-time prediction | [Metering and cost](/docs/build/sdk-metering-and-cost) | | `qsolve.metering` | `Recorder` and everything measured at the backend boundary | [Metering and cost](/docs/build/sdk-metering-and-cost) | | `qsolve.benchmark` | `run_benchmarks` and the gate and case models | [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks) | | `qsolve.lint` | `lint_workflow`, `Finding`, `has_errors` | [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks) | | `qsolve.sandbox_entry` | the process the platform runs in the sandbox (`python -m qsolve.sandbox_entry`) | [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks) | | `qsolve.cli` | the `qsolve` command (a Typer app named `app`) | [The qsolve command line](/docs/build/cli) | | `qsolve._version` | `__version__`, read from the installed package metadata | this page | ## How the pieces fit ```text qsolve.yaml -> load_workflow() -> Manifest + two JSON Schemas | input.json -> validate_input() (jsonschema; WorkflowError on a mismatch) | _instantiate(): import entrypoint, YourWorkflow(input, params), size() vs envelope | run_methods(): every declared method, reference and classical first, quantum last | | | classical ctx | quantum ctx | backend refuses | QsolveBackend -> Backend (Aer | fake | IBM | executor) | | every circuit, shot and count -> metering.Recorder v v evaluate(solution) recomputes each objective -> MethodResult (recomputed | reported) | headline(): accuracy, +/-, matches -> Metrics metering.quality(): fidelity, depth, 2q gates -> Quality answer(outcome) + output schema check -> Result.answer | RunOutput(result=Result, provenance=Provenance) ``` In words: 1. **You write** a `Workflow` subclass ([The Workflow class](/docs/build/sdk-workflow-class)) and a manifest ([The manifest](/docs/build/manifest)). 2. **The runner** (`run_workflow`) loads and validates everything, builds your class, and calls `run_methods` ([Runner and manifest](/docs/build/sdk-runner-and-manifest)). 3. **Each method gets a `Context`.** The quantum method's `ctx.sampler`, `ctx.estimator` and `ctx.backend` are the bridge objects, which send circuits to a backend; the classical methods get a context whose backend refuses ([Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge)). 4. **Everything that crosses the backend is recorded** by `metering.Recorder`, from which Qsolve computes circuit size and fidelity without trusting your code ([Metering and cost](/docs/build/sdk-metering-and-cost)). 5. **The comparison** recomputes each objective with your `evaluate`, flags contradictions and computes accuracy against the reference. 6. **The output** is a `Result` and a `Provenance` ([Results and events](/docs/build/sdk-results-and-events)). 7. **`qsolve test`** (`run_benchmarks`) runs this pipeline for every benchmark case and adds the validation gates; `qsolve lint` is the static check ([Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks)). ## When the platform calls what, and when you do | Call | Who calls it | When | |---|---|---| | `Workflow.__init__`, `size`, `quantum`, `reference`, `classical_`, `evaluate`, `answer`, `estimate`, `summary` | the SDK, never you | during `run_workflow` and `estimate_run` | | `ctx.sampler`, `ctx.estimator`, `ctx.backend`, `ctx.emit`, `ctx.report_mitigation` | you, from your methods | whenever your method needs them | | `run_workflow`, `estimate_run`, `load_workflow`, `run_benchmarks`, `lint_workflow` | the CLI and the platform; you may call them from your own tests | `qsolve run`, `estimate`, `test`, `lint`, or a pytest file ([Your first workflow](/docs/build/your-first-workflow) shows the scaffold's test) | | `Backend`, `QsolveBackend`, `Recorder`, `serve_executor` | the SDK and the platform executor | not part of a workflow; you subclass `Backend` only to test the harness | ## Environment variables | Variable | Read by | Meaning | |---|---|---| | `QSOLVE_IBM_TOKEN` | `IBMBackend` | Your IBM Quantum token. Required for `--target ibm:`; never put it in workflow code. | | `QSOLVE_IBM_INSTANCE` | `IBMBackend` | Optional IBM instance. | | `QSOLVE_SIM_THREADS` | `qsolve.sim.sim_threads` | Threads a simulation may use. Positive integer; otherwise every core available to the process. | | `QSOLVE_EXECUTOR_ADDR` | `get_backend`, `RemoteBackend` | Set by the platform inside the sandbox (`unix:` or `tcp::`). When set, `get_backend` returns the credential-free remote backend whatever the target string says. You never set it. | | `QSOLVE_EXECUTOR_TOKEN` | `RemoteBackend` | Shared secret for the executor. Set by the platform. | ## What is stable Compatibility policy, in one place: | Level | Names | Promise | |---|---|---| | Public, stable | everything in `qsolve.__all__`; the Qiskit-facing behaviour of `ctx.sampler`, `ctx.estimator`, `ctx.backend`; the `qsolve` commands and their exit codes; the lint codes (a code keeps its meaning) | Changes are backward-compatible within a major version. Something is removed or changed incompatibly only in a new major version, after at least one minor release that deprecates it, and the change is listed in `CHANGELOG.md`. | | Documented, importable, not in `__all__` | `qsolve.results` models other than the exported ones, `qsolve.sim`, `qsolve.circuit_record`, `qsolve.events`, `qsolve.benchmark`, `qsolve.lint`, `qsolve.metering`, `qsolve.comparison`, `qsolve.backends` | Safe to read and to use in tests. May change in a minor version; read `CHANGELOG.md` when you upgrade. | | Internal | `qsolve.backends.remote` (the executor protocol), `qsolve.sandbox_entry` (the sandbox process), anything whose name starts with an underscore | May change in any release without notice. Do not import them in a workflow. | Versioning: the SDK follows semantic versioning (`MAJOR.MINOR.PATCH`): a patch fixes bugs only, a minor adds features without breaking public names, a major may break them. The workflow manifest has its own `schema_version` (currently `1`); see [Versioning](/docs/build/versioning). ## Reference pages - [The Workflow class](/docs/build/sdk-workflow-class): `Workflow`, `Outcome`, `Context`, the exceptions. - [Runner and manifest](/docs/build/sdk-runner-and-manifest): `run_workflow`, `estimate_run`, `validate_input`, `load_workflow`, `Manifest`, `run_methods`, `headline`. - [Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge): targets, `get_backend`, every backend class, `QsolveBackend`, `QsolveSampler`, `QsolveEstimator`. - [Metering and cost](/docs/build/sdk-metering-and-cost): `Recorder`, fidelity, `Estimate`, run-time prediction in `qsolve.sim`. - [Results and events](/docs/build/sdk-results-and-events): every field of `Result`, `Provenance` and friends; `emit`; circuit records. - [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks): `run_benchmarks`, case and gate models, `lint_workflow`, `sandbox_entry`. - [The qsolve command line](/docs/build/cli): every command and flag. Schema and manifest details live in their own references: [The manifest](/docs/build/manifest), [Parameters](/docs/build/parameters), [Input schema reference](/docs/build/input-schema-reference), [Output schema reference](/docs/build/output-schema-reference), [Benchmarks](/docs/build/benchmarks) and [Lint codes](/docs/build/lint-codes). --- # SDK: the Workflow class > Reference for qsolve.workflow: Workflow and every method you override, Outcome, Context (including emit and report_mitigation), and the exceptions WorkflowError, InvalidSolution, MethodUnavailable and QuantumNotAllowed. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-workflow-class · Updated: 2026-11-13 Module `qsolve.workflow`. Everything on this page is importable from the top-level package: `from qsolve import Workflow, Outcome, Context, WorkflowError, InvalidSolution, MethodUnavailable, QuantumNotAllowed`. For a guided walk-through read [Writing the workflow](/docs/build/writing-the-workflow); this page is the exact reference. Back to the [SDK reference](/docs/build/sdk-reference). ## Workflow ```python class Workflow: def __init__(self, input: dict[str, Any], params: dict[str, Any]) -> None def size(self) -> int def quantum(self, ctx: Context) -> Outcome def reference(self, ctx: Context) -> Outcome def evaluate(self, solution: Any) -> float def answer(self, outcome: Outcome) -> dict[str, Any] def estimate(self, target: str) -> Estimate | None def summary(self, quantum: Outcome, reference: Outcome) -> str | None ``` You subclass `Workflow` and name the class in the manifest's `entrypoint` (`package.module:ClassName`). **The SDK calls every method; you never call them.** The base class implements only `__init__`, `estimate` and `summary`. The other five raise `NotImplementedError` until you override them. In addition to these, every method of role `classical` that you declare in the manifest is a method named `classical_(self, ctx) -> Outcome`, where `` is the manifest method id with each dash turned into an underscore (`simulated-annealing` is `classical_simulated_annealing`). The base class does not define these; the runner looks them up by name. | Manifest method | Class method | Required | |---|---|---| | role `quantum` | `quantum` | yes (exactly one) | | role `reference` | `reference` | yes (exactly one) | | role `classical`, id `x-y` | `classical_x_y` | one per declared method | A declared method whose class method is missing fails the run with `method 'x-y' is declared in the manifest but the workflow has no `classical_x_y` method`. ### `__init__(self, input, params)` | Parameter | Type | Meaning | |---|---|---| | `input` | `dict[str, Any]` | The user's input, already validated against `schemas/input.json`. | | `params` | `dict[str, Any]` | Every manifest parameter by name: defaults plus the user's overrides, each checked against its type and bounds. Parameters you did not declare are never present. | Returns nothing. The base implementation stores them as `self.input` and `self.params`; call `super().__init__(input, params)` first. **Side effects:** none in the base class. Called once per run, by the runner, before any method runs. Raise `WorkflowError("message")` for anything the user got wrong that a schema cannot express. The platform shows the message to the user as written. A `ValueError`, `KeyError`, `TypeError` or `IndexError` raised here becomes `WorkflowError("Could not set up the problem from this input: ")`; any other exception is a crash. ```python def __init__(self, input, params): super().__init__(input, params) self.nodes = input["nodes"] if len(set(self.nodes)) != len(self.nodes): raise WorkflowError("node names must be unique") ``` **Gotcha:** do the expensive work in the method that needs it, not here. `estimate_run` also builds your class (to call `size` and `estimate`), so a slow constructor slows every price quote. ### `size(self) -> int` The number of qubits or variables. The runner compares it with the manifest `envelope` straight after construction: if it is above `envelope.max_variables` or `envelope.max_qubits` the run is refused with `Problem has N variables; this workflow supports at most M ()`. No credits are spent. Raises `NotImplementedError` if you do not override it. ### `quantum(self, ctx) -> Outcome` The method with role `quantum`. It is the only method whose `ctx` can send circuits. Build circuits in Qiskit, train however you like, and run what you want measured with `ctx.sampler.run(...)`, `ctx.estimator.run(...)` or `ctx.backend.run(...)`. Local statevector simulation that does not go through `ctx` is not metered. Exceptions: a `WorkflowError` raised here is the deliberate "this input cannot be solved" and reaches the user as written. `MethodUnavailable` makes the run fail with `the quantum method or the reference produced no result: `. Any other exception is a crash: the platform reports it and bills what was used. The method must send at least one circuit through `ctx`, or validation fails (`quantum method sent no circuit to the backend`). ### `reference(self, ctx) -> Outcome` The method with role `reference`: the classical answer the quantum result is judged against. Its `ctx` refuses circuits. Same exception rules as `quantum`; the reference must produce a value or the run fails. ### `classical_(self, ctx) -> Outcome` One per `classical` method. Its `ctx` refuses circuits. Raise `MethodUnavailable("why")` when the method does not apply to an input: it is shown as unavailable with that reason and the run continues. **Any other exception, `WorkflowError` included, is caught and the method is shown as unavailable with `failed: : `, but a method in that state fails the validation case** (see `check_quality` in [Testing, linting and the sandbox](/docs/build/sdk-testing-and-benchmarks)). Only `MethodUnavailable` is a clean "not applicable". ### `evaluate(self, solution) -> float` | Parameter | Meaning | |---|---| | `solution` | A solution in whatever JSON-compatible form your methods put in `Outcome.solution` or in `samples[i]["solution"]`. | Returns the objective of that solution; **lower is better**. The SDK calls it on every method's `solution` and on every entry of `samples`, to recompute the number each method claimed. Keep it exact and fast. Raise `InvalidSolution("reason")` for a solution that breaks a rule of the problem. Any other exception is turned into `WorkflowError("evaluate() failed on a method's solution: : ")` and fails the run. A non-finite return value marks that method unavailable. See [How Qsolve checks your answer](/docs/build/writing-the-workflow#how-qsolve-checks-your-answer). ```python def evaluate(self, solution): x = np.asarray(solution, dtype=float) if x.shape != (self.n,) or not np.isin(x, (0, 1)).all(): raise InvalidSolution("a solution is one 0 or 1 per variable") return float(x @ self.Q @ x) ``` ### `answer(self, outcome) -> dict[str, Any]` Turns an `Outcome` into the domain answer people read, shaped by `schemas/output.json` ([Output schema reference](/docs/build/output-schema-reference)). Called on the quantum method's outcome (it must succeed; the result is validated against the output schema, and a failure or mismatch fails the run with `answer() failed on the quantum result: ...` or `Workflow produced an invalid answer: ...`) and on every other method's outcome so answers can be shown side by side (a failure there is ignored and that method simply has no answer). ### `estimate(self, target) -> Estimate | None` | Parameter | Meaning | |---|---| | `target` | The target string, for example `sim-cpu` or `ibm:ibm_fez`. | Optional. The base class returns `None`. Return an [`Estimate`](/docs/build/sdk-results-and-events#estimate) of what a run will send to the target; it becomes the price quote. With `None`, `estimate_run` substitutes one circuit and `params["shots"]` shots (or 0 if there is no `shots` parameter) and marks it `by_default`. The runner sets `by_default` itself, and adds the fixed run overhead to a `runtime_s` you give. Called by `estimate_run` and `qsolve estimate`; the `estimate_honest` gate compares it with what the first benchmark case really sent. Details in [Metering and cost](/docs/build/sdk-metering-and-cost). ### `summary(self, quantum, reference) -> str | None` Optional. Return one sentence for the result page, or `None` for the default (`: ; : (accuracy NN.NN%).`). The runner truncates it to 500 characters. Called after all methods have run, with the quantum and reference `Outcome`s. ## Outcome ```python @dataclass class Outcome: solution: Any objective: float stderr: float | None = None samples: list[dict[str, Any]] | None = None details: dict[str, Any] = field(default_factory=dict) ``` Every method returns one. | Field | Type | Default | Meaning | |---|---|---|---| | `solution` | any JSON-compatible value | required | A solution in the form `evaluate` accepts. `None` for a result that cannot be re-evaluated (a measured energy, a number from a library): the objective is then accepted and labelled `reported` instead of `recomputed`. | | `objective` | `float` | required | The value this method reports for it. Lower is better. It must be finite: otherwise the method is unavailable (`it reported a value that is not a finite number`). If `solution` is given, the SDK recomputes it with `evaluate`; a difference of more than `1e-6 * max(1, abs(value))` is recorded as `claimed` and fails validation. | | `stderr` | `float \| None` | `None` | One standard deviation, when the value is a measurement. Must be finite and not negative, or the method is unavailable. | | `samples` | `list[dict] \| None` | `None` | For a sampling method, every outcome as `{"solution": ..., "p": probability}`, optionally with a `"label"` string. The SDK evaluates each, reports `valid_fraction` and `p_reference`, and shows the five most probable. Entries without a `solution` key or with a negative, non-finite or non-numeric `p` are dropped; the rest are renormalised to sum to 1. | | `details` | `dict[str, Any]` | `{}` | Free-form diagnostics kept with the run (quantum method only, under `provenance.solver.details`). NumPy arrays and scalars are converted to plain JSON; details over 200,000 bytes serialised are replaced by a note. For the `error_rate` metric, `details["n_test"]` must hold the number of test cases. | Maximisation: return the negative value as `objective` and turn it back in `answer()`. ```python return Outcome( solution=best["solution"], objective=self.evaluate(best["solution"]), samples=sorted(samples, key=lambda s: -s["p"])[:10], details={"theta": theta.tolist()}, ) ``` **Gotcha:** `samples` should hold all your outcomes, not a hand-picked top few, or `valid_fraction` and `p_reference` describe only that subset. ## Context ```python @dataclass class Context: seed: int target: str backend: BackendV2 sampler: BaseSamplerV2 estimator: BaseEstimatorV2 emit: Callable[..., None] report_mitigation: Callable[..., None] ``` Built by the runner and passed to every method; you never construct one except in tests. There are two flavours, made from the same inner backend: the quantum method's is live, and the one given to `reference` and every `classical_` has a backend that refuses to run anything. | Field | Type | Meaning | |---|---|---| | `seed` | `int` | The run's seed (`qsolve run --seed`, default 0). Use it for every random number generator. | | `target` | `str` | The target string as requested, for example `sim-cpu` or `fake:fez@1.6`. | | `backend` | `BackendV2` | A [`QsolveBackend`](/docs/build/sdk-backends-and-bridge#qsolvebackend). `backend.run(circuits, shots=N)` returns a finished job. | | `sampler` | `BaseSamplerV2` | A [`QsolveSampler`](/docs/build/sdk-backends-and-bridge#qsolvesampler). | | `estimator` | `BaseEstimatorV2` | A [`QsolveEstimator`](/docs/build/sdk-backends-and-bridge#qsolveestimator). | | `emit` | `Callable` | `emit(type, **data)`, see below. | | `report_mitigation` | `Callable` | see below. | Using `backend`, `sampler` or `estimator` from a classical method raises `QuantumNotAllowed`. It is raised at the point of the run call. ### `ctx.emit(type, **data)` This is `qsolve.events.emit`. Sends a progress event to whoever is listening (the run page on the platform, the `on_event` callback of `run_workflow`). With no listener it does nothing and costs nothing. It never raises and never affects the result. | Parameter | Meaning | |---|---| | `type` | Event kind, a string. For example `"train_step"`. The runner itself uses `"stage"`. | | `**data` | JSON-compatible keyword fields, for example `step=3, value=-1.2`. The event is `{"type": type, "t": , **data}`. | **Gotchas:** pass `type` positionally; a keyword `type=` clashes with the first parameter. A data field named `t` replaces the timestamp. The platform caps how many events it keeps. ```python ctx.emit("train_step", step=i, value=float(cost), best=float(best)) ``` ### `ctx.report_mitigation(circuits, distribution, methods=(), kept=None, steps=())` Declares the mitigated output of earlier circuits. Mitigation is your own post-processing, so Qsolve cannot see it; you report it, Qsolve recomputes its fidelity against the circuit's ideal output and labels it `reported` next to the raw figure it measured itself. The whole story, including what you may claim, is in [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). | Parameter | Type | Default | Meaning | |---|---|---|---| | `circuits` | `list[int]` | required | Dispatch indices of the circuits this distribution mitigates, from `pub_result.metadata["qsolve"][k]["dispatch"]`. Must be non-empty and each must refer to a circuit already sent. | | `distribution` | `dict[str, float]` | required | The mitigated distribution, bit string to weight (Qiskit order, classical bit 0 on the right). Weights must be finite and not negative and sum to more than zero. Normalised by Qsolve. | | `methods` | `list[str]` or tuple | `()` | Names of the mitigation methods, plain text. | | `kept` | `float \| None` | `None` | Share of shots that survived your checks, between 0 and 1 inclusive. `None` counts as 1. | | `steps` | `list[dict]` or tuple | `()` | Your own fidelity ladder, for example `{"name": "readout correction", "fidelity": 0.9}`. Stored and shown as given. | Returns `None`. Raises `WorkflowError` when `circuits` is empty or cites an index that was not sent (`report_mitigation cites circuits [3], but 2 were sent so far ...`), when the distribution is empty or has an invalid weight, or when `kept` is outside 0 to 1. Called by you, from the quantum method, after the circuits it covers were run. A report is only counted for circuits sent by the quantum method that share one ideal output. ## Exceptions | Exception | Base | Raised by | Meaning and effect | |---|---|---|---| | `WorkflowError` | `Exception` | you, from `__init__` or from `quantum`/`reference`; the SDK, for any problem with input, parameters, target or answer | A problem with the user's input or the workflow, worded for the person who submitted it. The platform shows the message as is. `qsolve run` prints `Error: ` and exits 1. | | `InvalidSolution` | `Exception` | you, from `evaluate` | A solution breaks a rule of the problem. The method that produced it is shown as unavailable with your message. | | `MethodUnavailable` | `Exception` | you, from any method | The method does not apply to this input; say why in the message (empty message shows `not available for this input`). | | `QuantumNotAllowed` | `WorkflowError` | the SDK | A classical method (role `reference` or `classical`) tried to use the quantum backend. The message is `a classical method tried to run a circuit: only the quantum method may use the backend`. | `InvalidSolution` and `MethodUnavailable` are plain `Exception` subclasses, not `WorkflowError`s. Catch `WorkflowError` and you also catch `QuantumNotAllowed`; do not swallow it inside a classical method, because that hides a violation the platform wants to see. `WorkflowError` is also re-exported from `qsolve.runner` (it is in that module's `__all__`); it is the same class. ## A complete minimal workflow The test helper in the SDK's own tests uses this shape: a tiny QUBO solved by exhaustive search and by one sampling circuit. ```python import numpy as np from qiskit import QuantumCircuit from qsolve import Estimate, InvalidSolution, Outcome, Workflow, WorkflowError class Tiny(Workflow): def __init__(self, input, params): super().__init__(input, params) self.Q = np.array(input["Q"], dtype=float) if self.Q.ndim != 2 or self.Q.shape[0] != self.Q.shape[1]: raise WorkflowError("Q must be square") self.n = len(self.Q) def size(self): return self.n def evaluate(self, solution): x = np.asarray(solution, dtype=float) if x.shape != (self.n,) or not np.isin(x, (0, 1)).all(): raise InvalidSolution("one 0 or 1 per variable") return float(x @ self.Q @ x) def answer(self, outcome): return {"bits": [int(b) for b in outcome.solution], "objective": self.evaluate(outcome.solution)} def estimate(self, target): return Estimate(qubits=self.n, circuits_on_target=1, shots_on_target=int(self.params["shots"])) def reference(self, ctx): states = (np.arange(2**self.n)[:, None] >> np.arange(self.n)) & 1 e = np.einsum("si,ij,sj->s", states, self.Q, states) return Outcome(solution=states[int(np.argmin(e))].tolist(), objective=float(e.min())) def classical_greedy(self, ctx): # manifest method id: greedy, role: classical return self.reference(ctx) def quantum(self, ctx): qc = QuantumCircuit(self.n) qc.h(range(self.n)); qc.measure_all() shots = int(self.params["shots"]) counts = ctx.sampler.run([(qc, None, shots)]).result()[0].data.meas.get_counts() best = min(([int(c) for c in reversed(k)] for k in counts), key=self.evaluate) return Outcome(solution=best, objective=self.evaluate(best)) ``` The matching manifest, schemas and benchmark cases are in [Your first workflow](/docs/build/your-first-workflow) and [The manifest](/docs/build/manifest). --- # SDK: runner and manifest > Reference for qsolve.runner, qsolve.manifest and qsolve.comparison: run_workflow, estimate_run, validate_input, RunOutput, load_workflow, Manifest and its parts, run_methods, headline and implementation. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-runner-and-manifest · Updated: 2026-11-13 The harness that runs a workflow, the objects that describe one on disk, and the comparison that turns method outputs into numbers. The platform calls the same functions inside its sandbox. Back to the [SDK reference](/docs/build/sdk-reference). ## qsolve.runner ```python from qsolve.runner import RunOutput, estimate_run, run_workflow, validate_input ``` `__all__` of the module: `RunOutput`, `WorkflowError`, `estimate_run`, `run_workflow`, `validate_input`. `estimate_run` and `run_workflow` are also exported from `qsolve`. ### run_workflow ```python run_workflow( workflow_dir: str, input: dict[str, Any], *, target: str = "sim-cpu", params: dict[str, Any] | None = None, seed: int = 0, backend: Backend | None = None, commit_sha: str | None = None, image_digest: str | None = None, on_event: Sink | None = None, ) -> RunOutput ``` Runs every method the manifest declares on one input, measures everything and builds the result. This is what `qsolve run` and the platform call. **Who calls it:** the CLI, the sandbox entry point, `run_benchmarks`, and your own tests. | Parameter | Type | Default | Meaning | |---|---|---|---| | `workflow_dir` | `str` | required | Directory holding `qsolve.yaml` (a `Path` also works at runtime; the annotation is `str`). | | `input` | `dict` | required | The input, validated against the input schema. | | `target` | `str` | `"sim-cpu"` | `sim-cpu`, `sim-gpu`, `fake:[@scale]` or `ibm:`. Ignored when `backend` is given. See [Backends and the Qiskit bridge](/docs/build/sdk-backends-and-bridge#get-backend). | | `params` | `dict \| None` | `None` | Parameter overrides by name. Unknown names or out-of-range values raise. Declared parameters you do not override use their defaults. | | `seed` | `int` | `0` | The run's seed, available as `ctx.seed`. The same input, parameters and seed give the same result. | | `backend` | `Backend \| None` | `None` | Use this backend object instead of resolving `target`. For tests and for the platform. | | `commit_sha` | `str \| None` | `None` | Recorded in `provenance.commit_sha`. | | `image_digest` | `str \| None` | `None` | Recorded in `provenance.image_digest`. | | `on_event` | `Sink \| None` | `None` | A function receiving each progress event dict. | Returns a [`RunOutput`](#runoutput). Raises `WorkflowError` for: invalid parameters (`unknown parameters: [...]`, `parameter 'x' must be <= 6.0`), input that does not match the schema (`Invalid input at : `), an unusable target or backend (unknown target, no `QSOLVE_IBM_TOKEN`, missing `ibm` extra, unknown fake device, noise scale outside 1 to 10), a class that cannot be imported or is not a `Workflow` subclass, a constructor failure, an input above the envelope, a quantum or reference method that produced no value (a reference whose `answer()` fails or breaks the output schema counts as no value), a failing `answer()`, or an answer that breaks the output schema (`Workflow produced an invalid answer: ...`). Also raises `FileNotFoundError`, `ValueError` or `pydantic.ValidationError` from `load_workflow` when the manifest or schemas are invalid (the CLI turns those into a message for `validate`, `test`, `run` and `estimate`). Exceptions that are not `WorkflowError` raised inside your quantum or reference method propagate, with a note `raised in the quantum method ''`. **Side effects.** Imports your workflow package (and, to keep two workflows with the same package name `src` apart, removes stale modules of that name from `sys.modules` and puts the workflow root first on `sys.path`). Limits BLAS to one thread for the duration of the run so summation order is fixed and the same seed gives bit-identical results on your laptop and in the sandbox. Sends circuits to the backend (on `ibm:` targets that spends your own quota). Emits events: `{"type": "stage", "stage": "build"}`, then `{"type": "stage", "stage": "run", "device": ..., "target": ...}`, then `{"type": "stage", "stage": "compare"}`, plus whatever your methods `emit`. **What it computes.** | Output | Rule | |---|---| | `result.objective` | the quantum method's recomputed value | | `result.baseline_objective`, `baseline_solver` | the reference's value and its manifest `title` | | `result.gap` | `(quantum - reference) / max(abs(reference), 1e-12)`; negative when the quantum result is better | | `result.uncertainty` | `kind="sampling"` with `stderr` and the note `one standard deviation on the quantum value` when the quantum method gave a `stderr`, otherwise `kind="exact"` | | `result.summary` | `workflow.summary(...)` or the default sentence, cut to 500 characters | | `provenance.input_hash`, `result_hash` | SHA-256 of the canonical JSON, floats rounded to a 1e-6 grid | | `provenance.target` | the backend's target class (`sim-cpu`, `sim-gpu` or `qpu`), not the string you passed | ```python from qsolve import run_workflow out = run_workflow(".", {"Q": [[-1, 2], [2, -1]]}, params={"shots": 500}, seed=1, on_event=lambda e: print(e["type"], e.get("stage"))) print(out.result.metrics.matches, out.provenance.result_hash) ``` **Gotchas.** `target` is not checked until after parameters and input validate, so a bad target with a bad input reports the input error first. The first failure wins; nothing is partial. Do not call it from inside a workflow. ### estimate_run ```python estimate_run( workflow_dir: str, input: dict[str, Any], *, target: str = "sim-cpu", params: dict[str, Any] | None = None, ) -> Estimate ``` Validates and sizes a run without executing any method. Used for price quotes and credit holds, and by `qsolve estimate`. It loads the manifest, resolves parameters, validates the input, builds your workflow class (so `__init__` and `size` run, and the envelope is enforced) and calls `estimate(target)`. | Behaviour | Detail | |---|---| | Workflow returns `None` | An `Estimate(qubits=size(), circuits_on_target=1, shots_on_target=params["shots"] or 0, notes=["the workflow gives no estimate; using a conservative default"], by_default=True)`. | | Workflow returns an `Estimate` | `by_default` is forced to `False`, and if `runtime_s` is set the fixed run overhead `RUN_OVERHEAD_S` (20 s) is added and the sum rounded to 0.1 s. | | Errors | `WorkflowError` as for `run_workflow`. No circuit is run and no backend is created, so the target string is passed to your `estimate` unchecked. | There is no `seed` or `on_event` parameter. The CLI exposes no `--param`, so `qsolve estimate` uses the manifest defaults; call the function directly to estimate with overrides. ### validate_input ```python validate_input(wf: LoadedWorkflow, input: dict[str, Any]) -> None ``` Checks `input` against `wf.input_schema` with `jsonschema.validate`. Returns `None` when valid. Raises `WorkflowError("Invalid input at : ")`, where the path is `` for a top-level problem. Called by `run_workflow`, `estimate_run`, `qsolve validate` (for the examples) and `run_benchmarks`. If the schema itself is invalid, `jsonschema.SchemaError` propagates; `qsolve lint` reports that as `E020`. ### RunOutput ```python class RunOutput(BaseModel): result: Result provenance: Provenance ``` What `run_workflow` returns. `out.model_dump(mode="json")` is exactly what `qsolve run --out` writes and what the sandbox sends as its result frame. Fields are described in [Results and events](/docs/build/sdk-results-and-events). ### Other module-level names | Name | Value or role | |---|---| | `HASH_DECIMALS` | `6`: floats are rounded to an absolute 1e-6 grid before hashing. Finer would make the reproducibility gate flaky. | | `SOLVER_DETAILS_MAX_BYTES` | `200_000`: larger `Outcome.details` are replaced by `{"omitted": " bytes, over the 200000 kept"}`. | | `WorkflowError` | Re-export of `qsolve.workflow.WorkflowError`. | Private helpers (underscore names such as `_instantiate`, `_context`, `_hash`, `_stable`, `_plain`, `_source`, `_make_importable`) are not part of the API. ## qsolve.manifest ```python from qsolve.manifest import Manifest, load_workflow, LoadedWorkflow ``` The parsed form of `qsolve.yaml`. Every model rejects unknown keys (`extra="forbid"`). The meaning of each manifest field, with examples, is in [The manifest](/docs/build/manifest) and [Parameters](/docs/build/parameters); this section is the Python surface. ### load_workflow ```python load_workflow(root: str | Path) -> LoadedWorkflow ``` Reads `qsolve.yaml` in `root`, validates it and reads both JSON Schema files. **Returns** a `LoadedWorkflow`. **Raises:** | Exception | When | |---|---| | `FileNotFoundError("No qsolve.yaml in ")` | there is no `qsolve.yaml` | | `ValueError("qsolve.yaml is not valid YAML: ...")` | YAML error | | `pydantic.ValidationError` (a `ValueError`) | the manifest breaks a rule | | `ValueError("schema file not found: ")`, `" must contain a JSON object"`, `"schema path escapes workflow directory: "` | a schema path is missing, not an object, or points outside the workflow directory | | `json.JSONDecodeError` (a `ValueError`) | a schema file is not valid JSON | Side effect: reads files only. It does not import your code or validate the schemas as JSON Schema. ### LoadedWorkflow | Field | Type | Meaning | |---|---|---| | `root` | `Path` | The resolved workflow directory. | | `manifest` | `Manifest` | The parsed manifest. | | `input_schema` | `dict` | The input JSON Schema. | | `output_schema` | `dict` | The output JSON Schema. | ### Manifest | Field | Type | Default | Notes | |---|---|---|---| | `schema_version` | `Literal[1]` | required | Must be `1`. The module constant `SCHEMA_VERSION` is `1`. | | `name` | `str` | required | Matches `^[a-z0-9][a-z0-9-]{1,62}$`. | | `version` | `str` | required | Semantic version `MAJOR.MINOR.PATCH`. | | `family`, `summary`, `license` | `str` | required | | | `tags` | `list[str]` | `[]` | | | `inputs` | `SchemaRef` | required | `schema:` path (Python name `schema_path`) and `examples:` list. | | `parameters` | `list[Parameter]` | `[]` | Names must be unique, each default must pass its own `check`. | | `outputs` | `SchemaRef` | required | | | `entrypoint` | `str` | required | `package.module:ClassName`, matching `^[A-Za-z_][\w.]*:[A-Za-z_]\w*$`. | | `category` | `str` | `""` | At most 60 characters. | | `objective` | `ObjectiveDecl` | `label="Objective"`, `unit=""`, `decimals=4` | `decimals` is 0 to 10. | | `metric` | `MetricDecl` | `kind="relative_gap"` | `kind` is `relative_gap`, `relative_error` or `error_rate`; `note` up to 600 characters; `tolerance` 0 to 1 (default 0); `tolerance_abs` optional, at least 0. | | `methods` | `list[MethodDecl]` | required, at least 2 | Ids unique; exactly one `quantum` and exactly one `reference`. | | `envelope` | `Envelope` | required | `max_qubits` 1 to 1000, `max_variables` 1 to 100000, `notes` string. | | `targets` | `Targets` | `simulators=["cpu"]`, `qpu=[]`, `min_qubits_qpu=None` | `simulators` items `cpu` or `gpu`; `qpu` items `ibm` (Braket is not available yet). `min_qubits_qpu` is accepted but ignored by the platform (lint `W052`). | | `resources` | `Resources` | `timeout_s=1800`, `memory_mb=4096`, `network="none"` | `timeout_s` 1 to 86400; `memory_mb` 256 to 65536; `network` can only be `none`. | | `runtime` | `Runtime` | `python="3.12"`, `dependencies="requirements.lock"` | | Sub-models: `SchemaRef(schema_path, examples)`, `Parameter(name, type, default, label, description, min, max, choices)`, `ObjectiveDecl`, `MetricDecl`, `MethodDecl(id, role, title<=80, what<=500, strength<=300, limit<=300)`, `Envelope`, `Targets`, `Resources`, `Runtime`. `MethodDecl.id` follows the name rule (lowercase letters, digits, dashes, 2 to 63 characters). #### Manifest.resolve_params ```python resolve_params(self, overrides: dict[str, Any] | None = None) -> dict[str, Any] ``` Returns every declared parameter: the override if given, otherwise the default, each passed through `Parameter.check`. **Raises** `ValueError` for `unknown parameters: [...]` and for any value of the wrong type or outside its bounds. Called by `run_workflow` and `estimate_run`, which convert the `ValueError` to `WorkflowError`. #### Parameter.check ```python check(self, value: Any) -> Any ``` Returns `value` unchanged or raises `ValueError`. Rules: `int` rejects bools and non-ints; `float` accepts finite ints and floats but not bools, NaN or infinity; `bool` needs a real bool; `str` needs a string; `enum` needs a value in `choices` (a bool never matches the int `1`); `min` and `max` apply to `int` and `float`. A `Parameter` itself is rejected at load when `min > max`, when an `enum` has no `choices`, or when `min` or `max` is NaN or infinite. Messages look like `parameter 'layers' must be <= 6.0` (bounds are stored as floats, so an integer bound prints with `.0`). ## qsolve.comparison ```python from qsolve.comparison import headline, implementation, run_methods ``` Runs the declared methods and computes the numbers. `run_workflow` calls it; you do not need to. ### implementation ```python implementation(wf: Workflow, decl: MethodDecl) -> Any ``` The bound method of `wf` that implements `decl`: `quantum` for role `quantum`, `reference` for role `reference`, `classical_` for role `classical`. Raises `WorkflowError("method 'x' is declared in the manifest but the workflow has no `name` method")` when it is missing or not callable. ### run_methods ```python run_methods( wf: Workflow, decls: list[MethodDecl], *, quantum_ctx: Context, classical_ctx: Context, recorder: Recorder, ) -> tuple[list[MethodResult], dict[str, Outcome]] ``` Runs every declared method, reference and classical ones first and the quantum method last, each with its own context, and returns a `MethodResult` per declaration (in manifest order) and the `Outcome` of each method that produced one (by id). For every method it sets `recorder.method`, times it, and: - a `MethodUnavailable` makes it unavailable with your message; - a classical method that raises anything else is unavailable with `failed: : `; a quantum or reference method that raises `WorkflowError` re-raises it, and any other exception re-raises with a note; - a non-quantum method that sent circuits is unavailable (`it sent circuits to the quantum backend, which only the quantum method may do`); - a non-finite objective or error bar makes it unavailable; - otherwise `evaluate` recomputes the value (`verified="recomputed"`) or, with no solution, the reported value is kept (`verified="reported"`); a difference between claimed and recomputed values larger than `1e-6 * max(1, |value|)` is stored in `claimed`; - `answer(outcome)` is attempted. For a reference or classical method, a failing `answer()` or an answer that breaks the output schema (passed as `output_schema`, which `run_workflow` supplies) makes that method unavailable with the reason (`answer() failed: ...` or `its answer breaks the output schema: ...`); the run goes on. For the quantum method a failure leaves `answer=None` here and the runner reports it (`WorkflowError`). After the loop it fills `vs_reference = (value - ref) / max(|ref|, 1e-12)` for each method, and for sampling methods `p_reference`, `valid_fraction` and `top_outcomes` (the five most probable, each labelled by its `label` or its JSON, cut at 60 characters). `p_reference` counts samples whose recomputed value is at most the reference value (with a 1e-9 relative slack). ### headline ```python headline(decl: MetricDecl, methods: list[MethodResult], quantum: Outcome) -> Metrics ``` The accuracy against the reference with one of three generic formulas, `scale = max(abs(reference), 1e-12)`: | `kind` | `accuracy` | `accuracy_pm` | `matches` when | |---|---|---|---| | `relative_gap` | `1 - max((q - ref) / scale, 0)` | none | accuracy is within `tolerance` of 1; or `abs(q - ref) <= tolerance_abs`; or the quantum `stderr` is at most a quarter of `scale` and `abs(q - ref) <= 2 * stderr`; or `q <= ref` | | `relative_error` | `1 - abs(q - ref) / scale` | `stderr / scale` | the same three conditions as above, without the `q <= ref` rule | | `error_rate` | `1 - q` | half-width of the 95% Wilson interval for a rate `q` measured on `quantum.details["n_test"]` cases (none if `n_test` is missing or below 1) | `q <= ref + tolerance + tolerance_abs`, or the 95% interval's lower end is at most `ref + tolerance + tolerance_abs` | `accuracy` is clipped to 0 to 1. Raises `ValueError` if the quantum or reference method has no value. See [Benchmarks](/docs/build/benchmarks) for how a workflow author chooses a kind. --- # SDK: backends and the Qiskit bridge > Reference for qsolve.backends and qsolve.bridge: targets and get_backend, Backend and AsyncBackend, AerBackend, FakeHardwareBackend, IBMBackend, RemoteBackend, QsolveBackend, QsolveSampler and QsolveEstimator. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-backends-and-bridge · Updated: 2026-11-13 How circuits get from your Qiskit code to a simulator, a noise model or a quantum computer. In your workflow you only touch the bridge objects in `ctx` (`ctx.backend`, `ctx.sampler`, `ctx.estimator`); the backends behind them are the platform's. Targets and what they cost are explained in [Backends and targets](/docs/build/backends-and-targets). Back to the [SDK reference](/docs/build/sdk-reference). ## Targets | Target string | Backend class | `target` / `provider` / `device` attributes | Notes | |---|---|---|---| | `sim-cpu` | `AerBackend(gpu=False)` | `sim-cpu` / `aer` / `aer-cpu` | Noise-free Qiskit Aer, up to 26 qubits. | | `sim-gpu` | `AerBackend(gpu=True)` | `sim-gpu` / `aer` / `aer-gpu` | Needs `qiskit-aer-gpu`; up to 30 qubits. | | `fake:[@scale]` | `FakeHardwareBackend` | `qpu` / `fake` / `fake_[@scale]` | An IBM device's published noise on Aer; needs the `ibm` extra; free. | | `ibm:` | `IBMBackend` | `qpu` / `ibm` / `` | Real IBM hardware; needs the `ibm` extra and `QSOLVE_IBM_TOKEN`. | | (platform only) | `RemoteBackend` | whatever the executor reports | Used inside the sandbox; see below. | ## qsolve.backends ```python from qsolve.backends import (AerBackend, AsyncBackend, Backend, ProviderJobFailed, SampleResult, get_backend) ``` The package `__all__` holds exactly those six names. `FakeHardwareBackend`, `IBMBackend` and `RemoteBackend` live in the submodules `qsolve.backends.fake`, `.ibm` and `.remote` and are imported lazily so the optional dependencies load only when needed. ### get_backend ```python get_backend(target: str) -> Backend ``` Resolves a target string to a backend object. Called by `run_workflow` when you pass no `backend`. It takes no other arguments: an unknown keyword argument raises `TypeError`, and an unknown or malformed target raises `ValueError` (for example a non-numeric noise scale in `fake:fez@fast`). | Rule (checked in this order) | Result | |---|---| | Environment variable `QSOLVE_EXECUTOR_ADDR` is set | `RemoteBackend()`, whatever `target` says. On the platform the workflow only ever gets the credential-free remote backend; the executor decides the real device. | | `sim-cpu` | `AerBackend(gpu=False)` | | `sim-gpu` | `AerBackend(gpu=True)` | | starts with `ibm:` | `IBMBackend(device=)` | | starts with `fake:` | `FakeHardwareBackend(device, scale)`, parsing `device[@scale]` with `scale = float(...)`, default 1.0 | | anything else | `ValueError("Unknown target '...'. Use sim-cpu, sim-gpu, ibm: or fake:[@scale].")` | Other exceptions: `RuntimeError` when the `ibm` extra is missing or `QSOLVE_IBM_TOKEN` is not set; `ValueError` for an unknown fake device, a scale outside 1 to 10, or a scale that is not a number. `run_workflow` turns `ValueError`, `RuntimeError` and `ImportError` from this call into `WorkflowError`. ### SampleResult ```python @dataclass class SampleResult: counts: dict[str, int] shots: int exec_time_s: float = 0.0 metadata: dict[str, Any] = field(default_factory=dict) ``` What a backend returns for one circuit. `counts` maps bit strings to counts in Qiskit order (qubit 0 is the rightmost character; spaces from multiple registers are tolerated by consumers). `metadata` varies by backend: | Key | From | Meaning | |---|---|---| | `dispatch` | every run through the bridge | Index of the recorded circuit; cite it in `ctx.report_mitigation`. Added by `QsolveBackend.dispatch`. | | `readout` | `fake`, `ibm` | Per measured bit, `[P(read 1 given 0), P(read 0 given 1)]` of the physical qubit it came from, or `None` when the device does not report it. | | `two_qubit_gates` | `fake`, `ibm` | Two-qubit gates in the transpiled circuit. | | `physical_qubits` | `fake`, `ibm` | Physical qubits the transpiled circuit used. | | `device_circuit` | `fake`, `ibm` | The transpiled circuit as drawable ops (`circuit_ops`), for the first four distinct circuits of a batch, each of at most 5,000 operations. | | `job_id`, `batch_index`, `batch_size`, `quantum_seconds` | `ibm` | The provider job; `quantum_seconds` (the job's billed quantum time) appears on the first circuit of a batch, 0 on the others. | ### MAX_BATCH and sample_all `MAX_BATCH = 300` (in `qsolve.backends.base`): circuits per batch, bounded by provider job limits and the executor's message size. ```python sample_all(backend: Backend, circuits: Sequence[QuantumCircuit], shots: int, seeds: Sequence[int]) -> list[SampleResult] ``` `backend.sample_batch` in chunks of `MAX_BATCH`. For code that talks to a `Backend` directly, with circuits that do not depend on each other's results. ### Backend ```python class Backend(ABC): target: str # "sim-cpu" | "sim-gpu" | "qpu" provider: str device: str @abstractmethod def sample(self, circuit: QuantumCircuit, shots: int, seed: int) -> SampleResult def sample_batch(self, circuits: Sequence[QuantumCircuit], shots: int, seeds: Sequence[int]) -> list[SampleResult] def calibration_snapshot(self) -> dict[str, Any] @property def max_qubits(self) -> int ``` Executes measured circuits and returns counts. Workflows never touch a `Backend`; the harness does. Subclass it only to test the harness with your own fake. | Member | Meaning | |---|---| | `target`, `provider`, `device` | Class or instance attributes a subclass must set. `target` decides `source` in the fidelity measurement: anything other than `qpu` is an ideal simulator. | | `sample(circuit, shots, seed)` | Abstract. Run one circuit. `seed` seeds simulators; hardware ignores it. | | `sample_batch(circuits, shots, seeds)` | Run several independent circuits, each with its own seed. Default: one `sample` call per circuit (a simulator then gives exactly the counts of separate calls). Raises `ValueError("one seed per circuit")` when the lengths differ. Overridden by backends that can send a batch as one job. | | `calibration_snapshot()` | Device facts stored in `provenance.calibration_snapshot`. Default `{}`. | | `max_qubits` | Largest circuit accepted. Default 30. The bridge sizes its Qiskit `Target` with it. | ### AsyncBackend ```python class AsyncBackend(Backend): batches: bool = False def submit(self, circuit, shots, seed) -> str # abstract def collect(self, job_id: str) -> SampleResult # abstract def cancel(self, job_id: str) -> None # default: does nothing def submit_batch(self, circuits, shots, seeds) -> str # default: NotImplementedError def collect_batch(self, job_id: str) -> list[SampleResult] # default: NotImplementedError ``` A backend whose jobs live at the provider and can be collected later by id (QPUs). The platform records the job id between `submit` and `collect`, so a run interrupted while waiting in a hardware queue re-attaches to the same job instead of paying twice. `sample` is `collect(submit(...))`. `sample_batch` uses `submit_batch` and `collect_batch` when `batches` is true (one provider job, one queue wait) and otherwise falls back to one job per circuit. `submit` and `collect` are abstract; the batch pair must be implemented when `batches` is true. `cancel` should cancel a job still waiting at the provider. ### ProviderJobFailed `class ProviderJobFailed(RuntimeError)`. The provider finished a job without a result (failed or cancelled on their side). Resubmitting is the right response; any other exception may be transient. Raised by `IBMBackend.collect_batch` when IBM reports `ERROR` or `CANCELLED`. ### AerBackend ```python AerBackend(gpu: bool = False) ``` Local Qiskit Aer simulator. `gpu=True` needs `qiskit-aer-gpu` and sets `target="sim-gpu"`, `device="aer-gpu"`; otherwise `sim-cpu` and `aer-cpu`. `provider` is `aer`. `max_qubits` is 30 on GPU, 26 on CPU. | Method | Behaviour | |---|---| | `sample(circuit, shots, seed)` | Simulates one circuit with `seed_simulator=seed` inside `qsolve.sim.simulator_threads()`, so every core the run was given is used. Returns counts and wall-clock seconds. The same seed gives the same counts at any thread count. | | `runnable(circuit, seed=0)` | Returns `circuit` itself when every gate in it is native to Aer (or is `barrier` or `measure`), else the circuit transpiled for Aer with `seed_transpiler=seed`. Transpiling a small circuit costs far more than simulating it, so it is skipped when possible. | ### FakeHardwareBackend ```python from qsolve.backends.fake import FakeHardwareBackend FakeHardwareBackend(device: str = "fez", scale: float = 1.0) ``` A real device's published noise, simulated: the free rehearsal for hardware runs. `target="qpu"`, `provider="fake"`, `device="fake_"` with `"@"` appended when `scale != 1`. Needs the `ibm` extra. | Parameter | Meaning | |---|---| | `device` | Name of a fake IBM device: `fez`, `marrakesh`, `torino`, ... The class `Fake` is looked up in `qiskit_ibm_runtime.fake_provider`. `ValueError("no fake device 'x' (e.g. fez, marrakesh, torino)")` if absent. | | `scale` | Multiplier on every error rate (gate, readout, preparation errors, capped at 0.5 for readout and 0.75 for gates); T1 and T2 are divided by it. Must be from 1 to 10, else `ValueError("noise scale must be between 1 and 10")`. `fez@1.6` is a pessimistic rehearsal: a real device has tended to be worse than its own published model (1.6 is an empirical rule of thumb, not a fitted constant). | `sample_batch` transpiles the circuits exactly as for the device (`transpile_best`) and runs them as one Aer job with the noise model. Aer takes one seed per job, so for a batch every circuit's seed is mixed into it (a hash of the whole list): changing any one seed changes the job, and the same seeds always give the same counts. A single circuit keeps its own seed. Each result carries `readout`, `two_qubit_gates`, `physical_qubits` and (for a few circuits) `device_circuit` metadata, the same as `IBMBackend`, so workflows exercise their hardware path end to end. `sample` is a batch of one. `calibration_snapshot()` returns `{"backend": device, "noise_scale": scale}`. `max_qubits` is the simulated device's qubit count. Building the noise model takes several seconds for a 156-qubit device. ### IBMBackend ```python from qsolve.backends.ibm import IBMBackend IBMBackend(device: str, token: str | None = None, instance: str | None = None) ``` IBM Quantum hardware through `qiskit-ibm-runtime`. An `AsyncBackend` with `batches = True`. Credentials come from the environment, never from workflow code. | Parameter | Meaning | |---|---| | `device` | IBM backend name, for example `ibm_fez`. | | `token` | Defaults to `QSOLVE_IBM_TOKEN`. `RuntimeError("QSOLVE_IBM_TOKEN is not set")` if neither exists. | | `instance` | Defaults to `QSOLVE_IBM_INSTANCE`. | `RuntimeError` if the `ibm` extra is missing. The constructor connects (channel `ibm_quantum_platform`) and looks up the device, so it needs network access. | Method | Behaviour | |---|---| | `submit_batch(circuits, shots, seeds)` | Transpiles with `transpile_best` (keeping the cheapest of four tries), sends one `SamplerV2` job with dynamical decoupling (`XpXm`) and gate twirling on (measurement twirling off, so readout errors stay as calibrated), and caps the job's quantum time with `max_execution_seconds`. Returns the IBM job id. Costs your IBM quota. | | `collect_batch(job_id)` | Waits for the job; one `SampleResult` per circuit in submission order, metadata as in the table above. `exec_time_s` is the wall wait divided by the batch size. Raises `ProviderJobFailed` when IBM ended the job as `ERROR` or `CANCELLED`, other exceptions as they are. | | `submit`, `collect` | One-circuit versions of the above. | | `cancel(job_id)` | Cancels a job that is not yet `DONE`, `ERROR` or `CANCELLED`. | | `calibration_snapshot()` | `{"backend": device, "n_qubits": n}`. | | `max_qubits` | The device's qubit count. | Module constants in `qsolve.backends.ibm`: `S_PER_JOB = 2.4` and `S_PER_SHOT = 0.00023`, an empirical fit to billed quantum time on `ibm_fez` (IBM can change its billing; re-fit if estimates drift from the bill), and ```python max_execution_seconds(n_circuits: int, shots: int) -> int ``` which returns `ceil(10 + 3 * (S_PER_JOB + S_PER_SHOT * n_circuits * shots))`: three times the expected bill plus 10 s, so a job that somehow needs more is stopped by IBM instead of draining the account. ### Hardware helpers (qsolve.backends.hardware) Shared by the fake and IBM backends; not needed by workflows. | Name | Meaning | |---|---| | `TRANSPILE_TRIES = 4` | Seeds tried per circuit. | | `transpile_best(circuits, backend, seed, tries=4) -> list[QuantumCircuit]` | Transpile at optimisation level 3 (noise-aware layout) with `tries` seeds and keep, per circuit, the result with the fewest two-qubit gates, then the shallowest. Deterministic. Circuits that differ only in rotation angles are compiled once as a template and then given their angles, so copies sent to split shots share one result. | | `two_qubit_count(circuit)`, `two_qubit_counts(circuits)` | Two-qubit operations, barriers excluded. | | `measured_qubits(circuit) -> dict[int, int]` | Classical bit to physical qubit, from the measurements of a transpiled circuit. | | `readout_errors(circuit, properties) -> list[list[float]] \| None` | `[[P(1\|0), P(0\|1)], ...]` per classical bit, or `None` when unavailable. | | `device_metadata(compiled) -> list[dict]` | Per circuit `two_qubit_gates`, `physical_qubits`, and for the first `DEVICE_CIRCUITS = 4` distinct circuits (of at most `DEVICE_CIRCUIT_MAX_OPS = 5000` operations) the transpiled circuit as `device_circuit`. | ### RemoteBackend and the executor (internal) `qsolve.backends.remote` is the protocol by which a sandboxed workflow reaches a backend without holding any credentials. The project's contributing notes list it as internal and subject to change, so this is a summary, not a contract. Inside the sandbox there is no network and no provider secret. `RemoteBackend(addr=None, token=None)` talks to the platform's executor over a local socket: `addr` defaults to `QSOLVE_EXECUTOR_ADDR` (`unix:` or `tcp::`), `token` to `QSOLVE_EXECUTOR_TOKEN`. It asks the executor for `info` and copies its `target`, `provider`, `device`, `max_qubits` and calibration. `sample` and `sample_batch` send circuits as base64 QPY, one JSON object per line, in chunks of `MAX_BATCH`; replies larger than `MAX_LINE_BYTES` (8 MiB) are refused. Executor errors become `RuntimeError("executor error: ...")`. The executor owns credentials and enforces credit holds, shot and qubit caps and writes the provenance. `serve_executor(backend, host="127.0.0.1", port=0, *, address=None, max_shots=100_000, max_qubits=32, token=None) -> (server, address)` is the reference executor the tests use: it serves `backend` on `unix:` or `tcp::` and returns the server and the resolved address. The platform wraps it with metering. A request with `shots` outside `1..max_shots`, a circuit above the qubit cap or a wrong token is refused. ## qsolve.bridge ```python from qsolve.bridge import DEFAULT_SHOTS, QsolveBackend, QsolveEstimator, QsolveSampler ``` Qiskit-native execution. A workflow written in plain Qiskit runs through these unchanged. Behind them is whatever target the run was given. Circuits are sent as the workflow built them (the platform transpiles for the device); every dispatch is recorded by the run's `Recorder` ([Metering and cost](/docs/build/sdk-metering-and-cost)). `DEFAULT_SHOTS = 1024`. ### QsolveBackend ```python QsolveBackend(inner: Backend, recorder: Recorder, *, seed: int = 0, allow_quantum: bool = True) ``` A Qiskit `BackendV2` over a Qsolve target. It is `ctx.backend`. The harness builds it; you use it. | Parameter | Meaning | |---|---| | `inner` | The Qsolve `Backend` to run on. | | `recorder` | Where every dispatch is recorded. | | `seed` | Base seed. The n-th circuit sent through this object gets seed `seed + n`. | | `allow_quantum` | When false, `dispatch` (and so `run`, the sampler and the estimator) raises `QuantumNotAllowed`. The classical methods' context is built this way. | Attributes: `name` is `qsolve-`, `backend_version` is `"1"`, `inner`, `recorder`, `allow_quantum`. `target` is a Qiskit `Target` with a generic gate set (`id x y z h s sdg t tdg sx sxdg rx ry rz p u cx cy cz ch swap crx cry crz cp rxx ryy rzz ecr ccx`) sized to `inner.max_qubits`. `max_circuits` is `None` (batches are split internally). Default options: `shots=1024` and `seed_simulator=None`. With `seed_simulator` set (as an option of `run`, or with `set_options`), circuit `k` of that call is seeded `seed_simulator + k`, so a call is reproducible whatever ran before it; left `None`, seeds continue from `seed` plus a running count. #### dispatch ```python dispatch(self, circuits: Sequence[QuantumCircuit], shots: int) -> list[SampleResult] ``` The one place circuits leave the workflow. Splits into batches of `MAX_BATCH`, copies each circuit (so what is recorded is what is sent, even if your code edits its circuit afterwards), assigns seeds, calls `inner.sample_batch`, records circuit, shots, counts, metadata and time in the `Recorder`, and adds `metadata["dispatch"]` (the recorded index) to each result. Thread-safe. Raises `QuantumNotAllowed` when `allow_quantum` is false. Called by `run`, by the sampler and by the estimator; you rarely call it directly. #### run ```python run(self, run_input: QuantumCircuit | Sequence[QuantumCircuit], **options) -> JobV1 ``` Qiskit's `BackendV2.run`. `run_input` is one circuit or a sequence. The only option read is `shots` (default 1024). Returns an already-finished job (runs are synchronous from the workflow's point of view) whose `result()` is a Qiskit `Result` with counts per circuit, so `backend.run(qc, shots=2000).result().get_counts()` works as usual. ```python qc = QuantumCircuit(2); qc.h(0); qc.cx(0, 1); qc.measure_all() counts = ctx.backend.run(qc, shots=2000).result().get_counts() ``` ### QsolveSampler ```python QsolveSampler(backend: QsolveBackend, *, default_shots: int = 1024) ``` A `SamplerV2` over a `QsolveBackend`. It is `ctx.sampler`. ```python run(self, pubs: Iterable[SamplerPubLike], *, shots: int | None = None) -> BasePrimitiveJob ``` | Parameter | Meaning | |---|---| | `pubs` | Sampler pubs: a circuit, or `(circuit, parameter_values, shots)` tuples. A shot count inside a pub wins over `shots`. | | `shots` | Default shots for pubs that give none; if also `None`, `default_shots` is used. | Returns a finished job; `.result()` is a `PrimitiveResult` with one pub result per pub. Each pub result has a data field per classical register of the circuit (so `measure_all()` gives `data.meas`), each a `BitArray`, and `metadata` with `shots`, `circuit_metadata` and `qsolve`, a list with one entry per bound circuit (in flattened parameter order) holding the backend's metadata, including `dispatch` and, on `qpu` targets, `readout` and `two_qubit_gates`. **All circuits of all pubs with the same shot count go to the backend as one batch** (on a QPU: one job, one queue wait). A result always holds exactly the requested number of shots: when a device rounds the count up (5000 asked, 5024 returned) a random subset of the requested size is kept; fewer than requested raises `ValueError`. ```python counts = ctx.sampler.run([(measured, theta, shots)]).result()[0].data.meas.get_counts() ``` **Gotchas.** A circuit must contain measurements. Bit strings are in Qiskit order (clbit 0 on the right), so variable 0 is the last character; reverse before decoding. The result is always synchronous. ### QsolveEstimator ```python QsolveEstimator(backend: QsolveBackend, *, default_precision: float = 0.015625) ``` An `EstimatorV2` over a `QsolveBackend`. It is `ctx.estimator`. It measures each group of qubit-wise commuting Pauli terms of an observable in its own basis and combines the results. **No error mitigation is applied.** ```python run(self, pubs: Iterable[EstimatorPubLike], *, precision: float | None = None) -> BasePrimitiveJob ``` | Parameter | Meaning | |---|---| | `pubs` | Estimator pubs: `(circuit, observables, parameter_values)`. | | `precision` | Target standard error of each value. Defaults to `default_precision` (0.015625, which is 4096 shots per measurement group). A pub's own precision wins. | For each value the shots per group are `ceil(1 / precision**2)`. Returns a finished job; each pub result holds `data.evs` and `data.stds` (arrays of the broadcast shape) and `metadata["target_precision"]`. **Cost note:** every parameter set and every observable is a separate dispatch with one circuit per basis group, so many observables or tight precision quickly mean many circuits and shots; the run is billed for what is sent, and `estimate()` should account for it. --- # SDK: metering and cost > Reference for qsolve.metering and qsolve.sim: how the SDK records what crosses the backend, how fidelity and circuit size are measured, and how Estimate and run-time prediction feed the price quote. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-metering-and-cost · Updated: 2026-11-13 Qsolve does not take your word for how big or how faithful your quantum run was. Everything your quantum method sends to a backend passes through a recorder, and the numbers are computed from that record. This page documents the recorder, the measurement functions, the pre-run estimate and the run-time model. Back to the [SDK reference](/docs/build/sdk-reference). Most of this is harness machinery: as a workflow author you call `ctx.report_mitigation` and return an `Estimate`; the rest you read to understand what the platform measures. For the concepts see [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity) and [Benchmarks](/docs/build/benchmarks). ## qsolve.metering ```python from qsolve import metering ``` ### Constants | Name | Value | Meaning | |---|---|---| | `IDEAL_MAX_QUBITS` | `20` | Largest circuit whose ideal output is simulated (exactly, noise-free). Bigger circuits cannot be scored. | | `MAX_IDEAL_CIRCUITS` | `200` | Distinct unreported circuits simulated for fidelity, spread evenly over the run. | | `MAX_STATS_CIRCUITS` | `40` | Distinct circuits transpiled for depth and gate counts: the 20 largest, then an even spread of the rest, so a workflow cannot hide its deepest circuit among many small ones. | | `BASIS` | `["cx", "rz", "sx", "x"]` | The gate set circuits are transpiled to for depth and two-qubit counts. | ### Dispatch ```python @dataclass class Dispatch: index: int method: str circuit: QuantumCircuit shots: int counts: dict[str, int] metadata: dict[str, Any] seconds: float ``` One circuit that crossed the backend boundary. `index` is its position in the run's record (the number you cite in `report_mitigation`), `method` the id of the manifest method that sent it, `circuit` the copy that was sent, `counts` and `metadata` what came back, `seconds` the backend time. ### MitigationReport ```python @dataclass class MitigationReport: circuits: list[int] distribution: dict[str, float] methods: list[str] kept: float | None steps: list[dict[str, Any]] ``` What a workflow declared through `ctx.report_mitigation`, after validation. ### Recorder ```python @dataclass class Recorder: dispatches: list[Dispatch] reports: list[MitigationReport] method: str = "quantum" lock: threading.Lock ``` Everything that crossed the backend boundary during one run. One is created per `run_workflow`. | Member | Meaning | |---|---| | `add(circuit, shots, counts, metadata, seconds) -> int` | Appends a `Dispatch` tagged with the current `method` and returns its index. Thread-safe. Called by `QsolveBackend.dispatch`. | | `of(method) -> list[Dispatch]` | The dispatches sent by one method id. | | `report(circuits, distribution, methods, kept, steps) -> None` | Stores a `MitigationReport`. Called by `ctx.report_mitigation` after it has validated the arguments. | | `method` | The method currently running; the comparison sets it before each method, which is how a dispatch is attributed. | ```python rec = metering.Recorder() backend = QsolveBackend(AerBackend(), rec) # see the bridge page backend.run(qc, shots=100) print(len(rec.dispatches), rec.of("quantum")[0].counts) ``` ### fidelity and shot_noise_floor ```python fidelity(p: dict[str, float], q: dict[str, float]) -> float shot_noise_floor(ideal: dict[str, float], shots: int, seed: int) -> float ``` `fidelity` is the classical (Bhattacharyya) fidelity `F = (sum_i sqrt(p_i q_i))^2` of two distributions over bit strings. Both are normalised inside the function; spaces in bit-string keys are ignored; non-positive weights are dropped; it returns 0.0 if either side has no weight. 1 means identical, 0 disjoint. `shot_noise_floor` draws `shots` noise-free samples from `ideal` (multinomial, seeded) and returns their fidelity with `ideal`. With finite shots even a perfect device scores below 1; this is that score, reported as `fidelity_floor` next to every figure. ### circuit_key ```python circuit_key(qc: QuantumCircuit) -> tuple[Any, ...] ``` A hashable identity for a bound circuit: width, and per operation its name, qubit and clbit positions, parameter values (rounded to 12 places; arrays by shape and bytes) and, for non-standard operations such as Pauli-evolution gates, the identity of their decomposition. Two dispatches with equal keys are the same circuit; their counts are pooled before scoring. ### ideal_distribution ```python ideal_distribution(qc: QuantumCircuit) -> dict[str, float] | None ``` The noise-free output distribution over the circuit's classical bits (bit string with classical bit 0 on the right; probabilities above 1e-14 only), or `None` when it cannot be simulated exactly: more than 20 qubits, no qubits, mid-circuit measurement (a gate after a measurement), `reset`, `delay`, `if_else`, `while_loop`, `for_loop`, `switch_case`, a classical condition, or classical bits that are never measured. Barriers are ignored. Uses a statevector simulation. ### active_qubits ```python active_qubits(qc: QuantumCircuit) -> int ``` The number of distinct qubits the circuit's operations touch, barriers excluded. (Different from `qsolve.circuit_record.active_qubits`, which returns the sorted list of qubit indices.) ### circuit_stats ```python circuit_stats(dispatches: list[Dispatch]) -> CircuitStats | None ``` The measured size of what was sent: `qubits` (the widest circuit by active qubits), `depth` and `two_qubit_gates` (the maxima over a sample of at most 40 distinct circuits transpiled to `BASIS` at optimisation level 1, the `cx` count being the two-qubit count), `circuits` (the number of dispatches) and `shots` (the sum of shots over all of them). `None` when there are no dispatches. The platform's `max_two_qubit_gates` bound in a benchmark case is checked against this number. ### mitigation ```python mitigation(rec: Recorder, method: str, source: str) -> Mitigation | None ``` Raw fidelity from the recorded circuits, mitigated fidelity from the workflow's reports. Returns `None` when the method sent nothing or none of its circuits can be scored. Which circuits count: 1. Circuits a valid report covers, and every other circuit whose ideal output is not a single bit string. A circuit whose output is a single bit string (a mirror or calibration circuit, an idle register) computes nothing the answer depends on and is left out; if every circuit is like that, they are scored anyway. A circuit left unreported that does compute something counts at its raw fidelity, so reporting a flattering subset changes nothing. 2. Of those, only circuits with at least half the two-qubit operations (after opening composite gates) of the largest. A trivial circuit sent alongside carries no weight. 3. A report is used only when all the circuits it cites share one ideal output and were sent by this method. 4. Circuits that cannot be scored are left out; `coverage` is the share of the method's shots that could be scored. The figures are shot-weighted averages. With no usable report, `fidelity_mitigated` equals `fidelity_raw`, `postselection_rate` is 1.0, `methods` is empty and `reported` is false. With reports, `fidelity_mitigated` is the fidelity of your reported distribution against the ideal output (circuits without a report contribute their raw fidelity), `postselection_rate` is the mean of your `kept` values (1 where omitted), `methods` the union of your names, `reported` is true and `details["steps"]` your ladder. `source` is stored as `Mitigation.source`: `hardware`, `noise_model` or `ideal_simulator`. `run_workflow` derives it from the backend: any non-`qpu` target is `ideal_simulator`; a `qpu` whose device name starts with `fake` is `noise_model`; otherwise `hardware`. ### quality ```python quality(rec: Recorder, method: str, source: str) -> Quality ``` `Quality(mitigation=mitigation(...), circuit=circuit_stats(rec.of(method)))`. Each part is `None` when nothing was sent or scorable. This is what `run_workflow` stores in `result.quality`, for the quantum method. ## Estimate `Estimate` is defined in `qsolve.results` and exported from `qsolve`; it is the one metering type you construct. Your `Workflow.estimate(target)` returns it. ```python class Estimate(BaseModel): qubits: int circuits_on_target: int shots_on_target: int classical_cost_class: str = "low" runtime_s: float | None = None # at least 0 notes: list[str] = [] by_default: bool = False ``` | Field | Meaning | |---|---| | `qubits` | Qubits the run uses. | | `circuits_on_target` | Circuits submitted to the chosen backend over the whole run. | | `shots_on_target` | Total shots across those circuits. | | `classical_cost_class` | `"low"`, `"medium"` or `"high"`: local CPU work in the sandbox. Not validated by the model; use those three values. | | `runtime_s` | Predicted seconds for the whole run on one reference core per simulator thread, excluding queue time at a hardware provider. `None` means not predicted. Must be at least 0. The runner adds `RUN_OVERHEAD_S` (20 s). The platform refuses a run whose prediction exceeds `resources.timeout_s` (a platform rule; the SDK itself does not check it). | | `notes` | Free text shown with the quote. | | `by_default` | Set by the SDK, never by you: true when the workflow gave no estimate and the SDK used one circuit and `params["shots"]` shots. | What the estimate is used for: - **The price quote and credit hold.** The platform turns the numbers into credits ([Credits and pricing](/docs/use/credits-and-pricing)). - **The `estimate_honest` gate.** `run_benchmarks` compares your estimate with what the first benchmark case really sent. Circuits and shots must each be within a factor of two (`0.5 <= estimated / actual <= 2`). With no estimate it passes with a note; if no circuits were sent it passes. ```python def estimate(self, target): return Estimate(qubits=self.n, circuits_on_target=1, shots_on_target=int(self.params["shots"]), classical_cost_class="low") ``` Count every circuit your quantum method sends, including one per measurement basis and per parameter set; the Estimator primitive sends one circuit per basis group per value. ## qsolve.sim ```python from qsolve.sim import (RUN_OVERHEAD_S, S_PER_CIRCUIT, S_PER_GATE_AMPLITUDE, S_PER_SHOT, S_PER_TRANSPILE, sim_threads, simulator_threads, statevector_seconds) ``` Simulator threading and run-time prediction. Aer updates each amplitude of a statevector independently, so a simulation gives bit-identical results at any thread count; simulations may therefore use every core the run was given, while BLAS and other OpenMP code stay on one thread, where summation order could change the result. ### Constants (seconds, for one core of a current server CPU) | Name | Value | Meaning | |---|---|---| | `S_PER_GATE_AMPLITUDE` | `1.8e-9` | One gate applied to one amplitude. | | `S_PER_CIRCUIT` | `0.012` | Aer call, result decoding and Python around one circuit. | | `S_PER_SHOT` | `2e-6` | Drawing one shot and decoding it into counts. | | `S_PER_TRANSPILE` | `0.15` | Transpiling a circuit the simulator cannot run as written. | | `RUN_OVERHEAD_S` | `20.0` | Sandbox start, imports, classical baseline and interpretation. | The platform scales the prediction to its own machines. The private constants `_PARALLEL_FROM_QUBITS = 15` (Aer parallelises statevector updates from 15 qubits up) and `_THREAD_EFFICIENCY = 0.7` (measured: 2 threads run 20 to 24 qubit circuits 1.6 to 1.7 times faster) are used by `statevector_seconds`. ### sim_threads ```python sim_threads() -> int ``` Threads a simulation may use: the positive integer in `QSOLVE_SIM_THREADS` (set by the platform to the sandbox's CPUs) if there is one, otherwise the number of cores this process may run on (`os.sched_getaffinity`, else `os.cpu_count()`), at least 1. ### simulator_threads ```python @contextmanager simulator_threads() -> Iterator[None] ``` A context manager that lets OpenMP (Aer) use `sim_threads()` threads for the duration of the block, via `threadpoolctl`. Used by `AerBackend.sample`. If you run your own `AerSimulator` calls inside a method, wrap them in it to use the cores the run was given: ```python from qsolve.sim import simulator_threads with simulator_threads(): counts = sim.run(qc, shots=1000).result().get_counts() ``` ### statevector_seconds ```python statevector_seconds(qubits: int, gates: int, circuits: int = 1, shots: int = 0, threads: int | None = None) -> float ``` Predicted seconds to simulate `circuits` circuits of `gates` gates on `qubits` qubits, drawing `shots` shots from each. | Parameter | Meaning | |---|---| | `qubits` | Circuit width. | | `gates` | Gates per circuit. | | `circuits` | Number of circuits. Default 1. | | `shots` | Shots drawn from each circuit. Default 0. | | `threads` | Threads to assume; default `sim_threads()`. | The model: `work = S_PER_GATE_AMPLITUDE * gates * 2**qubits`; for 15 or more qubits and more than one thread, `work /= 1 + 0.7 * (threads - 1)`; the result is `circuits * (work + S_PER_CIRCUIT + S_PER_SHOT * shots)`. It is an estimate for use in `Estimate.runtime_s`, not a measurement. Add `S_PER_TRANSPILE` per circuit the simulator cannot run as written, and the time of any classical baseline you run. ```python Estimate(qubits=n, circuits_on_target=1, shots_on_target=shots, runtime_s=statevector_seconds(n, gates=400, circuits=1, shots=shots)) ``` --- # SDK: results, events and circuit records > Reference for qsolve.results, qsolve.events and qsolve.circuit_record: every field of Result, MethodResult, Metrics, Quality, Mitigation, CircuitStats, Provenance, Uncertainty and Estimate, plus emit, capture and the drawable circuit records. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-results-and-events · Updated: 2026-11-13 What a run produces. You never build these objects in a workflow (except `Estimate`); the SDK builds them from what it measured and you read them in `qsolve run --out`, in tests and on the platform's run page. All are pydantic models: `model_dump(mode="json")` gives plain JSON and `Model.model_validate(data)` reads it back. Back to the [SDK reference](/docs/build/sdk-reference). ## qsolve.results ```python from qsolve import Estimate, Mitigation, Provenance, Quality, Result, Uncertainty # top level from qsolve.results import CircuitStats, MethodResult, Metrics # module only ``` Who measures what: the numbers are computed by the SDK; the words (`title`, `what`, `strength`, `limit`, `note`) are declared by your manifest. See [Reading your results](/docs/use/reading-your-results) for how a user sees them. ### Result The final, user-facing result of one run (`RunOutput.result`). | Field | Type | Default | Meaning | |---|---|---|---| | `answer` | `dict[str, Any]` | required | The quantum method's `answer()`, validated against the output schema. | | `objective` | `float` | required | The quantum method's objective, recomputed by `evaluate` when it gave a solution. | | `baseline_objective` | `float` | required | The reference method's objective. | | `baseline_solver` | `str` | required | The reference method's manifest `title`. | | `gap` | `float` | required | `(objective - baseline) / max(abs(baseline), eps)`; 0 means it matched the baseline; negative means better. | | `uncertainty` | `Uncertainty` | required | See below. | | `summary` | `str` | `""` | One sentence, at most 500 characters. | | `artifacts` | `dict[str, Any]` | `{}` | Reserved for extra output; the SDK leaves it empty. | | `quality` | `Quality \| None` | `None` | Fidelity and circuit size of the quantum run. | | `methods` | `list[MethodResult]` | `[]` | Every declared method, in manifest order. | | `metrics` | `Metrics \| None` | `None` | The headline accuracy. | ### Uncertainty | Field | Type | Default | Meaning | |---|---|---|---| | `kind` | `str` | required | `"sampling"`, `"exact"` or `"heuristic"`. `run_workflow` writes `sampling` when the quantum method gave a `stderr`, otherwise `exact`. | | `stderr` | `float \| None` | `None` | One standard deviation on the quantum value. | | `note` | `str` | `""` | Text, for example `one standard deviation on the quantum value`. | ### MethodResult One method, quantum or classical, run on this input. | Field | Type | Default | Meaning | |---|---|---|---| | `id` | `str` | required | The manifest method id. | | `role` | `str` | required | `quantum`, `reference` or `classical`. | | `title`, `what`, `strength`, `limit` | `str` | `""` except `title` | Declared in the manifest. | | `value` | `float \| None` | `None` | The objective this method reached (lower is better); `None` when unavailable. | | `stderr` | `float \| None` | `None` | The method's own error bar. | | `vs_reference` | `float \| None` | `None` | `(value - reference) / max(abs(reference), eps)`; 0 means the same. | | `seconds` | `float \| None` | `None` | Wall-clock seconds for the method. The only field that differs run to run; it is excluded from the reproducibility hash. | | `verified` | `str` | `"reported"` | `recomputed` when the SDK evaluated the method's solution with your `evaluate`; `reported` when it is the workflow's claim with no solution to re-evaluate. | | `dispatches` | `int` | `0` | Circuits this method sent to the backend (the quantum method must send at least one). | | `claimed` | `float \| None` | `None` | What the method itself reported, set only when it disagreed with the recomputed value. A set `claimed` fails validation. | | `unavailable` | `str` | `""` | Why the method could not run on this input. | | `p_reference` | `float \| None` | `None` | For sampling methods: the share of samples on the reference answer. | | `valid_fraction` | `float \| None` | `None` | For sampling methods: the share of samples that are valid answers. | | `top_outcomes` | `list[dict]` | `[]` | The five most probable outcomes: `{"outcome": str, "p": float}`. | | `answer` | `dict \| None` | `None` | This method's `answer()`, when it succeeded. | ### Metrics The headline numbers, computed by the SDK ([`headline`](/docs/build/sdk-runner-and-manifest#headline)). | Field | Type | Default | Meaning | |---|---|---|---| | `kind` | `str` | required | `relative_gap`, `relative_error` or `error_rate` (from the manifest). | | `accuracy` | `float` | required | 0 to 1: how close the quantum result is to the reference. | | `accuracy_pm` | `float \| None` | `None` | Its plus or minus (one sigma, or a 95% interval half-width for an error rate). | | `note` | `str` | `""` | How it was computed, in your words (manifest `metric.note`). | | `tolerance` | `float` | `0.0` | The manifest's `metric.tolerance`. | | `matches` | `bool` | `False` | The platform's verdict: equal to the reference, within a declared tolerance, or statistically indistinguishable from it. | ### Quality | Field | Type | Default | Meaning | |---|---|---|---| | `mitigation` | `Mitigation \| None` | `None` | Fidelity of the quantum run. | | `circuit` | `CircuitStats \| None` | `None` | Size of what was sent. | ### Mitigation Fidelity of the quantum run. The raw figure is measured by the SDK; the mitigated figure is what your own post-processing reports, recomputed against the ideal output. See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity) and [Metering and cost](/docs/build/sdk-metering-and-cost#mitigation). | Field | Type | Default | Meaning | |---|---|---|---| | `fidelity_raw` | `float` | required | Measured against the ideal distribution, before any mitigation. | | `fidelity_mitigated` | `float` | required | After the listed methods; equals the raw figure when you reported nothing. | | `reported` | `bool` | `False` | True when the mitigated figure comes from your report. | | `fidelity_floor` | `float` | required | The best a noise-free device scores at this shot count. | | `postselection_rate` | `float` | required | Share of shots that pass your checks (1.0 means nothing discarded). | | `methods` | `list[str]` | `[]` | Mitigation methods you named. | | `source` | `str` | required | `hardware`, `noise_model` or `ideal_simulator`. | | `coverage` | `float` | `1.0` | Share of the quantum method's shots that could be scored (not circuits over 20 qubits or with mid-circuit measurement). | | `details` | `dict[str, Any]` | `{}` | Holds `steps`, your fidelity ladder, when reported. | ### CircuitStats Size of what the quantum method actually sent, measured by the SDK. | Field | Type | Default | Meaning | |---|---|---|---| | `qubits` | `int` | required | Width of the widest circuit (active qubits). | | `depth` | `int` | required | Greatest depth after transpiling to `cx, rz, sx, x`. | | `two_qubit_gates` | `int` | required | Greatest `cx` count after transpiling. | | `circuits` | `int` | `0` | Circuits sent in total. | | `shots` | `int` | `0` | Shots over all of them. | ### Provenance Immutable record of how a result was produced (`RunOutput.provenance`). | Field | Type | Default | Meaning | |---|---|---|---| | `run_id` | `str` | required | A new UUID for each run. | | `workflow`, `version` | `str` | required | Manifest `name` and `version`. | | `commit_sha`, `image_digest` | `str \| None` | `None` | As passed to `run_workflow`. | | `sdk_version` | `str` | required | `qsolve.__version__` at run time. | | `input_hash` | `str` | required | SHA-256 of the input's canonical JSON, floats rounded to 1e-6. | | `params` | `dict` | required | The resolved parameters. | | `seed` | `int` | required | The seed. | | `target` | `str` | required | The backend's target class: `sim-cpu`, `sim-gpu` or `qpu`. | | `provider`, `device` | `str` | required | For example `aer` and `aer-cpu`; `fake` and `fake_fez@1.6`; `ibm` and `ibm_fez`. | | `calibration_snapshot` | `dict` | `{}` | The backend's `calibration_snapshot()`. | | `shots` | `int` | `0` | Total shots the quantum method sent. | | `exec_time_s` | `float` | `0.0` | Wall-clock seconds of the whole run. | | `started_at`, `finished_at` | `str` | required | ISO 8601 UTC timestamps. | | `result_hash` | `str` | required | SHA-256 of the result with each method's `seconds` set to null. The reproducibility gate compares it. | | `solver` | `dict` | `{}` | `{"name": , "details": }`. Not hashed. | ### Estimate The pre-run resource estimate, and the one result type you construct yourself. Its fields, and how the platform uses it, are in [Metering and cost](/docs/build/sdk-metering-and-cost#estimate). ## qsolve.events ```python from qsolve.events import Sink, capture, emit ``` Structured run events. Solvers and workflows call `emit`; the runner (or the platform) installs a sink that streams them to the run page and log store. With no sink installed `emit` costs nothing. | Name | Signature | Meaning | |---|---|---| | `Sink` | `Callable[[dict[str, Any]], None]` | The type of an event receiver. | | `emit` | `emit(type: str, **data: Any) -> None` | Sends `{"type": type, "t": time.time(), **data}` to the installed sink, if any. Never raises: an exception in the sink is swallowed because observability must never break a run. In a workflow use `ctx.emit`, which is this function. | | `capture` | `capture(sink: Sink \| None) -> ContextManager[None]` | Installs `sink` for the duration of a `with` block (a `ContextVar`, so it is per thread and per async task) and restores the previous one afterwards. `None` turns events off. `run_workflow(..., on_event=f)` is `capture(f)` around the run. | ```python from qsolve.events import capture, emit seen = [] with capture(seen.append): emit("train_step", step=1, value=-1.2) print(seen[0]["type"], seen[0]["step"]) # train_step 1 ``` Events the runner emits itself: `stage` with `stage` set to `build`, `run` (also carrying `device` and `target`) and `compare`. Event payloads must be JSON-serialisable; the sandbox converts non-finite floats to `null`. ## qsolve.circuit_record ```python from qsolve.circuit_record import DRAWABLE, active_qubits, circuit_ops, circuit_summary, lowered ``` Circuits as compact, drawable records: what the platform shows on a run page. A viewer can draw or export them without Qiskit. | Name | Signature | Meaning | |---|---|---| | `DRAWABLE` | `set[str]` | Gate names a viewer can draw and OpenQASM 2 can express (`h`, `cx`, `rz`, `measure`, `barrier`, ... 38 names), plus non-unitary operations. | | `lowered` | `lowered(circuit) -> QuantumCircuit` | Expands every operation outside `DRAWABLE` by decomposing, for at most 8 rounds. Opaque operations without a definition stay as they are and are drawn as labelled boxes. | | `circuit_ops` | `circuit_ops(circuit, max_ops=20_000) -> dict` | `{"qubits": int, "clbits": int, "ops": [[name, [qubit indices], [params], [clbit indices]], ...], "truncated": bool}` of the lowered circuit. Parameters are rounded to 6 places; an unbound parameter is its name; `delay` ops have no params. Qubit indices are positions in the circuit (physical qubits for a transpiled circuit). Stops at `max_ops` and sets `truncated`. | | `circuit_summary` | `circuit_summary(circuit) -> dict` | `{"qubits", "clbits", "size", "depth", "two_qubit_gates", "ops": {name: count}}` without lowering: cheap enough for every circuit a run sends. Two-qubit gates exclude barriers. | | `active_qubits` | `active_qubits(circuit) -> list[int]` | Sorted qubit indices that some operation touches (barriers included). Not to be confused with `qsolve.metering.active_qubits`, which returns a count. | ```python from qsolve.circuit_record import circuit_ops rec = circuit_ops(qc) print(rec["ops"][:3]) # [["h", [0], [], []], ["cx", [0, 1], [], []], ...] ``` --- # SDK: testing, linting and the sandbox > Reference for qsolve.benchmark, qsolve.lint and qsolve.sandbox_entry: run_benchmarks, the gate and case models, check_quality, check_estimate, lint_workflow, Finding, and the framed protocol of the sandbox entry point. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/sdk-testing-and-benchmarks · Updated: 2026-11-13 The code behind `qsolve test` and `qsolve lint`, which the platform's validation pipeline also runs, and the entry point the platform runs inside its sandbox. Back to the [SDK reference](/docs/build/sdk-reference). ## qsolve.benchmark ```python from qsolve.benchmark import (BenchmarkReport, Case, CaseResult, Expect, Gate, check_estimate, check_quality, load_hardware_cases, run_benchmarks) ``` Benchmark and validation gates for a workflow. Used by `qsolve test` locally and by the platform's validation, so a workflow passes locally if and only if it passes on the platform. What each gate and `expect` key means for a workflow author is in [Benchmarks](/docs/build/benchmarks). ### run_benchmarks ```python run_benchmarks(workflow_dir: str | Path, target: str = "sim-cpu") -> BenchmarkReport ``` Runs the validation gates and every case in `benchmarks/cases.yaml`. | Parameter | Meaning | |---|---| | `workflow_dir` | The workflow directory. | | `target` | The target for every case that sets no `target` of its own. | Returns a [`BenchmarkReport`](#benchmarkreport); `passed` is true when every gate passed. A case that raises is recorded as a failed case with `error=": "`; it does not stop the others. Raises `FileNotFoundError`, `ValueError` or `pydantic.ValidationError` when the manifest or its schemas are invalid, or a case in `cases.yaml` is malformed (`qsolve test` prints `Invalid workflow: ...` and exits 1). It does not run `qsolve lint`. Side effects: runs your workflow once per case, plus a second run of the first case and an estimate (so cases on `ibm:` targets spend real quota; use `fake:` for rehearsal). The gates, in the order they are added: | Gate | Passes when | |---|---| | `manifest` | the manifest and schemas load (otherwise the function raises instead) | | `example_inputs` | at least one example is declared and each validates against the input schema | | `benchmark_cases` | `cases.yaml` defines at least one case | | `methods_declared` | the manifest declares at least two methods (always true for a manifest that loads, since a smaller list is rejected earlier) | | `fidelity_checked` | some case sets `expect.min_fidelity` and runs on a target starting with `fake:` or `ibm:` (a case's own `target`, else the run's target) | | `cases_pass` | at least one case ran and every case passed | | `reproducible` | re-running the first case that completed with the same seed gives the same `result_hash`, or differs only by floating-point noise (relative 1e-8, `FLOAT_NOISE`); on a difference the detail names the path of the first differing value. Present only when at least one case ran. | | `estimate_honest` | `check_estimate` passes for the first completed case. Present only when at least one case ran. | ### Case An entry under `cases:` (run by `qsolve test`) or `hardware_cases:` (never run by it) in `benchmarks/cases.yaml`. Unknown keys are rejected. | Field | Type | Default | Meaning | |---|---|---|---| | `name` | `str` | required | Label in reports. | | `input_file` | `str \| None` | `None` | Path of the input JSON relative to the workflow root; may not leave the directory. | | `input` | `dict \| None` | `None` | The input inline. If both are given, `input` wins; if neither, the case raises `case '' needs input or input_file`. | | `params` | `dict` | `{}` | Parameter overrides. | | `seed` | `int` | `0` | The seed. | | `target` | `str \| None` | `None` | Run this case on another target, for example `fake:fez@1.6`. | | `expect` | `Expect` | all defaults | The requirements. | ### Expect | Field | Type | Default | The case fails when | |---|---|---|---| | `max_gap` | `float` | `0.0` | the relative gap to the reference exceeds it (plus 1e-9). 0 means the quantum result must match or beat the reference. | | `max_abs_error` | `float \| None` | `None` | if set, replaces `max_gap`: `abs(objective - baseline)` must be at most this. | | `min_fidelity` | `float \| None` | `None` | there are no mitigation numbers, fewer than 90% of the shots could be scored (`MIN_COVERAGE`), or the mitigated fidelity is below it. | | `min_postselection` | `float \| None` | `None` | there are no mitigation numbers or the share of shots kept is below it. | | `max_two_qubit_gates` | `int \| None` | `None` | the largest sent circuit has more two-qubit gates than this. | ### check_quality ```python check_quality(expect: Expect, r: Any) -> str ``` Why a result misses what the platform requires of every result, or the case's `expect`; `""` when it misses nothing. `r` is a `Result`. Always required: a headline metric exists; no method is unavailable with `failed: ...`; no method has a `claimed` value (it contradicted its own solution); the quantum method sent at least one circuit. Then each `expect` bound that is set. ### check_estimate ```python check_estimate(est: Estimate, actual: CircuitStats | None) -> tuple[bool, str] ``` Whether the workflow's estimate matches what the SDK metered. Returns `(True, "no estimate() given: ...")` for a default estimate, `(True, "no circuits were sent")` when `actual` is `None`, `(False, "estimated N circuits, the run sent M")` (or shots) when either ratio is outside 0.5 to 2, otherwise `(True, "estimate within 2x of what was sent")`. ### load_hardware_cases ```python load_hardware_cases(root: str | Path) -> list[Case] ``` The cases under `hardware_cases:` in `benchmarks/cases.yaml`, or `[]` when the file has none. They are never run by `qsolve test` or by validation: the platform runs them on a real device when the workflow's developer asks, at the developer's own cost. `MAX_HARDWARE_CASES = 3` and `HARDWARE_TARGET` (`^ibm:[A-Za-z0-9_.-]{1,60}$`) are the limits lint enforces on them (codes `E052` to `E054`, see [Lint codes](/docs/build/lint-codes)). ### CaseResult | Field | Type | Default | Meaning | |---|---|---|---| | `name` | `str` | required | The case name. | | `passed` | `bool` | required | Whether it met its requirements. | | `target` | `str \| None` | `None` | The target it ran on. | | `gap` | `float \| None` | `None` | The relative gap. | | `abs_error` | `float \| None` | `None` | `abs(objective - baseline)`. | | `objective`, `baseline_objective` | `float \| None` | `None` | The two objectives. | | `fidelity` | `float \| None` | `None` | Mitigated fidelity. | | `postselection` | `float \| None` | `None` | Share of shots kept. | | `exec_time_s` | `float \| None` | `None` | Run time. | | `shots` | `int \| None` | `None` | Shots sent. | | `error` | `str \| None` | `None` | Why it failed (exception text or the `check_quality` reason). | ### Gate `Gate(name: str, passed: bool, detail: str = "")`. ### BenchmarkReport | Field | Type | Meaning | |---|---|---| | `workflow`, `version` | `str` | From the manifest. | | `target` | `str` | The default target of the run. | | `passed` | `bool` | Every gate passed. | | `gates` | `list[Gate]` | | | `cases` | `list[CaseResult]` | | | `total_time_s` | `float` | Wall-clock seconds. | `report.model_dump(mode="json")` is what `qsolve test --out` writes and what the sandbox sends for a `validate` job. ## qsolve.lint ```python from qsolve.lint import Finding, has_errors, lint_workflow ``` Static checks on a workflow repository. No code is executed. The same checks run locally (`qsolve lint`) and on the platform before anything is built or run. They are defence in depth: the sandbox, not this linter, is the security boundary. Every code, its meaning and its fix are in [Lint codes](/docs/build/lint-codes). ### lint_workflow ```python lint_workflow(root: str | Path) -> list[Finding] ``` Returns every finding (an empty list means clean). It does not raise for a broken repository: a missing `qsolve.yaml` returns `E001`, an unparseable or invalid manifest or schema file returns `E002`. Reads files only. Besides the manifest and schemas it checks `benchmarks/cases.yaml`: each case has an `input` or an existing `input_file` inside the repository, case names are unique, no `ibm:` target sits under `cases`, and each case input matches the input schema (`E055` to `E059`); and a `shots` parameter may not exceed `qsolve.MAX_SHOTS_PER_CIRCUIT` (`E025`). See [Lint codes](/docs/build/lint-codes). ### Finding ```python @dataclass(frozen=True) class Finding: severity: Literal["error", "warning"] code: str # E### or W### message: str path: str = "" # relative to the workflow root; may be empty ``` `as_dict()` returns `{"severity", "code", "message", "path"}` as plain strings. `Severity` is the type alias `Literal["error", "warning"]`. ### has_errors `has_errors(findings: list[Finding]) -> bool`: true when at least one finding has severity `error`. This is what decides the exit code of `qsolve lint`. ### Constants | Name | Value | Meaning | |---|---|---| | `MAX_FILES` | `500` | More files is error `E011`. | | `MAX_TOTAL_BYTES` | `20 * 1024 * 1024` | More total bytes is error `E011`. | | `MAX_SCHEMA_BYTES` | `256 * 1024` | A larger schema is error `E022`. | | `MAX_TEXT_SCAN_BYTES` | `1024 * 1024` | How much of a text file is scanned for secrets. | | `SKIP_DIRS` | `{"__pycache__", ".git", ".venv", ".pytest_cache", ".ruff_cache", ".mypy_cache", "node_modules"}` | Directories ignored by the checks. | | `ALLOWED_MODULES` | `qsolve numpy scipy sklearn qiskit qiskit_aer pyscf pydantic jsonschema yaml typer rich` | Third-party modules available in the platform runtime image; workflows may import these and the standard library. The sandbox has no network, so nothing else can be installed at run time. | | `RISKY_IMPORTS` | `subprocess socket ctypes pickle marshal shelve multiprocessing` | Imports that are flagged. | | `RISKY_CALLS` | `eval exec compile __import__` | Calls that are flagged. | `locked_modules(root: Path) -> frozenset[str]` returns the import names of the packages a workflow pins in its own `requirements.lock` (distribution names are mapped to import names, for example `scikit_learn` to `sklearn`, `pyyaml` to `yaml`); those are accepted as importable in addition to `ALLOWED_MODULES`. ## qsolve.sandbox_entry Internal: the platform runs it, you do not. Documented so the protocol is not a mystery. ```bash python -m qsolve.sandbox_entry < job.json ``` `main() -> int` reads one JSON job from stdin: | Key | Meaning | |---|---| | `mode` | `"validate"` runs `run_benchmarks(workflow_dir, "sim-cpu")`; `"estimate"` runs `estimate_run`; any other value runs `run_workflow`. | | `workflow_dir` | The workflow directory. | | `input`, `params` | The input and parameter overrides (not used by `validate`). | | `target`, `seed` | The target (required for `estimate` and `run`) and seed (default 0). | | `commit_sha`, `image_digest` | Recorded in the provenance of a run. | It writes framed lines on a private duplicate of the original standard output, one per line: ```text @@QS:event@@ {...} a progress event (capped by the platform) @@QS:result@@ {...} the final result: a BenchmarkReport, an Estimate or a RunOutput @@QS:error@@ {"kind": "workflow"|"crash", "message": "..."} ``` Everything the workflow itself prints, from Python or written straight to file descriptor 1 (a subprocess, a native library), goes to standard error so it cannot be mistaken for a frame. Non-finite floats become `null` so strict parsers accept every frame. This stops accidents and casual forging; code in the same process can still find the protocol descriptor, which is why the platform also checks the result against what its host-side executor metered. | Exit code | Meaning | |---|---| | `0` | Success. | | `2` | A `WorkflowError`: the user's input or the workflow. | | `1` | A crash (any other exception; a bad job spec), with the traceback on standard error. | `FRAME` is the frame format string, `@@QS:{kind}@@ {payload}`. --- # Backends and targets > How workflow code runs circuits through QsolveBackend, QsolveSampler and QsolveEstimator on simulators, noise models and quantum computers, and what Qsolve meters. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/backends-and-targets · Updated: 2026-11-13 Your quantum method runs circuits through `ctx`: `ctx.backend` is a standard Qiskit `BackendV2`, `ctx.sampler` a `SamplerV2` and `ctx.estimator` an `EstimatorV2`, so ordinary Qiskit code runs unchanged. Only the quantum method may use them; in `reference` and `classical_*` they raise `QuantumNotAllowed`. Every circuit that passes through is recorded and measured. ## Targets A target is a string. These are all the forms the platform and the SDK know: | Target | Runs on | Notes | |---|---|---| | `sim-cpu` | Qiskit Aer on a CPU | The default. At most 26 qubits per circuit. Needs `cpu` in the manifest's `targets.simulators`. | | `sim-gpu` | Qiskit Aer on a GPU | At most 30 qubits per circuit. Needs `gpu` in `targets.simulators`, and `qiskit-aer-gpu` when run locally. The platform offers it only while it has GPU capacity. | | `fake:[@scale]` | Aer with a real IBM device's published noise model | For example `fake:fez` or `fake:fez@1.6`. Needs the `ibm` extra. Free locally. **Not selectable on the platform**: the run API refuses it with `target_not_supported`. It is for local runs and for `target:` in benchmark cases. | | `ibm:` | An IBM quantum computer | `` is the IBM backend name, matching `^[A-Za-z0-9_.-]{1,64}$`. Needs `qpu: [ibm]` in the manifest. Locally it uses your token in `QSOLVE_IBM_TOKEN` and optionally `QSOLVE_IBM_INSTANCE`, and needs the `ibm` extra. | Anything else is an error: locally `Unknown target 'x'. Use sim-cpu, sim-gpu, ibm: or fake:[@scale].`, and on the platform `target_not_supported`. Only IBM QPUs are supported today. There is no Amazon Braket target: a manifest cannot list `braket` under `qpu`, the run API answers `target_not_supported` for a `braket:` target, and the platform has no Braket price. `targets.min_qubits_qpu` is advisory. It is accepted and stored, but the platform does not use it to accept, refuse or price a run; use it to tell researchers the smallest problem worth sending to a QPU. ### Noise-model targets `fake:` looks up the device by name among IBM's published fake backends, so `` is the lowercase device name, for example `fez`, `marrakesh` or `torino`; an unknown name fails with `no fake device 'x' (e.g. fez, marrakesh, torino)`. The circuit is compiled for that device and sampled with its calibrated gate errors, readout errors and T1/T2 times. The qubit limit is the device's own qubit count. `@scale` multiplies every error rate and divides T1 and T2 by it. It must be between 1 and 10 inclusive, or the target fails with `noise scale must be between 1 and 10`. Scaled gate errors are capped at 0.5 and readout errors at 0.75. `fake:fez@1.6` is a pessimistic rehearsal: real chips tend to be noisier than their published models, and 1.6 is a rule of thumb. A benchmark case that sets `target: fake:fez@1.6` is how you test your workflow against noise during validation; see [Benchmarks](/docs/build/benchmarks). ### What the target controls on the platform | Stage | What decides it | |---|---| | Whether a run is accepted | `targets` in your [manifest](/docs/build/manifest), checked first; then whether the platform has a machine for the target. | | Which circuits are allowed | At most `envelope.max_qubits` qubits per circuit (default 32 if a stored manifest lacks it), and at most 100,000 shots per circuit in one send. Beyond either, the executor refuses the send with `circuit exceeds the allowed qubit count` or `shots must be in 1..100000`. The shot cap is checked earlier too: a run whose `shots` parameter is above 100,000 is refused when it is created with `422 shots_too_high`, and a workflow whose `shots` parameter has a `default`, a `max` or a benchmark-case value above 100,000 is rejected at submission (before the sandbox gates) with a message naming the cap. | | Price | The target's entry in the platform rate card: `sim-cpu`, `sim-gpu` and (for noise models) `sim-cpu`; `qpu:ibm` for `ibm:`. | | Time limit | Waiting for hardware does not count against `resources.timeout_s`; see [The manifest](/docs/build/manifest#resources). | Your workflow code never sees credentials or provider names. On the platform it gets a backend that forwards circuits to a trusted executor, which decides the real device. Which targets a researcher may request is set by `targets` in your manifest; see [Machines and targets](/docs/use/machines-and-targets). ## Running ordinary Qiskit code With the sampler: ```python measured = qc.copy() measured.measure_all() counts = ctx.sampler.run([(measured, theta, shots)]).result()[0].data.meas.get_counts() ``` A pub is `(circuit, parameter_values, shots)`. The data field is named after the circuit's classical register: `meas` after `measure_all()`, or the name you chose, as `max-cut` does with `result.data.c.get_counts()`. With no shots in the pub and no `shots=` argument, the sampler uses 1024. With the backend: ```python job = ctx.backend.run([circuit_a, circuit_b], shots=2000) counts = job.result().get_counts(0) ``` The job is finished when `run` returns. `backend.run` also defaults to 1024 shots. With the estimator: ```python result = ctx.estimator.run([(circuit, observable, theta)], precision=0.01).result() ``` The estimator groups the observable's Pauli terms into qubit-wise commuting groups, measures each group with one circuit and combines the results. It uses `ceil(1 / precision**2)` shots per group; the default precision of 0.015625 gives 4096. It applies no error mitigation and returns `evs` and `stds`. Send circuits as you built them; Qsolve compiles them for the target. The gate set includes `h`, `x`, `y`, `z`, `s`, `t`, `sx`, `rx`, `ry`, `rz`, `p`, `u`, `cx`, `cy`, `cz`, `ch`, `swap`, `crx`, `cry`, `crz`, `cp`, `rxx`, `ryy`, `rzz`, `ecr` and `ccx`, among others. A sampler result always holds exactly the number of shots you asked for. ## Batching circuits All circuits of one `sampler.run` call with the same shot count go to the target as one batch. On a quantum computer that is one job: one queue wait, one per-job overhead. A pub with an array of parameter values is one circuit per row. ```python ctx.sampler.run([(circuit, theta_grid, 2000)]) # many circuits, one batch ``` Pubs with different shot counts are separate batches, and batches over 300 circuits are split automatically. `quantum-kernel-classifier` sends a whole Gram matrix through one `sampler.run`. ## Shots and seeds Shots are most of the cost on hardware, so ask for what the answer needs. More shots reduce statistical noise, not systematic device error. Every circuit gets a seed built from `ctx.seed`, so on a simulator the same seed gives the same counts. Hardware counts are not reproducible, which is why the `reproducible` gate runs on a simulator. Seed all your own randomness from `ctx.seed`. ## What each result carries Each pub result has per-circuit metadata under `metadata["qsolve"]`, one dictionary per circuit: ```python result = ctx.sampler.run([(circuit, None, shots)]).result()[0] meta = (result.metadata.get("qsolve") or [{}])[0] dispatch = meta.get("dispatch") # the index to cite in report_mitigation readout = meta.get("readout") # per measured bit: [P(1|0), P(0|1)] ``` | Key | Present on | Meaning | |---|---|---| | `dispatch` | every target | The index of this circuit among everything the run sent. You cite it in `report_mitigation`. | | `readout` | noise-model and hardware targets | Per measured classical bit, the readout error `[P(1 given 0), P(0 given 1)]` of its qubit. Can be `None`. | | `two_qubit_gates` | noise-model and hardware targets | Two-qubit gates in the compiled circuit. | | `physical_qubits` | noise-model and hardware targets | Qubits used in the compiled circuit. | Use `.get` and handle a missing key: a simulator has no readout error to correct. ## What is metered Qsolve records every circuit, its shot count and the counts that came back, at the backend. | Measure | How | |---|---| | Circuits and shots | Counted at the backend. | | Qubits | The most qubits any sent circuit uses. | | Depth and two-qubit gates | Each distinct circuit is transpiled to the basis `cx, rz, sx, x`; the maximum over circuits is reported. Up to 40 distinct circuits are measured, largest first, then an even spread. | | Fidelity | The counts compared with the ideal output of the same circuit. See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). | Training on a local statevector simulator, with NumPy or `qiskit.quantum_info`, never touches the backend and is not metered. The `train_on: backend` parameter in the examples trains through `ctx.sampler` instead, which sends and meters one circuit per optimiser step, so the example's `estimate()` counts them. The cost quote comes from your `estimate()`; a run that needs more than was quoted stops with `budget_exhausted`. See [Credits and pricing](/docs/use/credits-and-pricing). ## Designing for hardware - Keep circuits shallow. Fidelity falls with depth and two-qubit gate count. The examples cap QAOA at two layers on noise-model and hardware targets, because a third only added noise. - Rehearse on `fake:@1.6` before spending hardware time. - Optimise on a local simulator and use the target only for the final measurement, as the examples do. - Circuits above 20 qubits cannot be scored for fidelity, because the ideal output is simulated exactly only up to 20 qubits. --- # Error mitigation and fidelity > How Qsolve measures fidelity, how to declare error mitigation with report_mitigation, how mitigated figures are scored and labelled, and what you may claim. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/error-mitigation-and-fidelity · Updated: 2026-11-12 Qsolve measures the fidelity of your quantum run from what crossed the backend, and you declare any error mitigation with `ctx.report_mitigation`. The raw fidelity is always shown. Your mitigated figures appear beside it, labelled reported, and Qsolve recomputes the fidelity against the same ideal output rather than taking your number. ## What fidelity means here Fidelity compares two probability distributions over bit strings: `p`, what the device returned, and `q`, the ideal output of the same circuit. Qsolve uses the classical (Bhattacharyya) fidelity: ```text F = ( sum over bit strings i of sqrt(p_i * q_i) ) ^ 2 ``` `F` is 1 for identical distributions and 0 for disjoint ones. With finite shots even a perfect device scores below 1, so every figure comes with a shot-noise floor: the fidelity a noise-free device would get with the same shots. The ideal output is simulated noise-free and exactly, up to 20 qubits. A circuit with more qubits, or with mid-circuit measurement, resets or classical control, cannot be scored. The share of your quantum method's shots that could be scored is reported as `coverage`. ## What Qsolve measures without you For the quantum method, Qsolve computes the raw fidelity of the circuits that carry the run's noise: - A circuit whose ideal output is a single bit string, such as a mirror or calibration circuit, computes nothing the answer depends on and is left out. - Every other circuit counts, whether or not you reported it. Reporting a flattering subset changes nothing. - Only circuits with at least half the two-qubit operations of the largest circuit are averaged, so a trivial circuit sent alongside carries no weight. - The average is weighted by shots. All dispatches of one circuit are pooled before scoring. With no report, mitigated fidelity equals raw fidelity and the share kept is 1.0. The result's `source` is `ideal_simulator`, `noise_model` or `hardware`. On an ideal simulator fidelity is only the shot-noise floor, so mitigation is validated on noise-model targets; see [Benchmarks](/docs/build/benchmarks). ## Declaring mitigation If you post-process counts to reduce the effect of noise, say so with `ctx.report_mitigation`. Readout correction, post-selection, zero-noise extrapolation and anything else all count. ```python ctx.report_mitigation( circuits=[dispatch], # indices from pub_result.metadata["qsolve"][k]["dispatch"] distribution={"0101": 0.62, "1010": 0.31, ...}, # the mitigated output, bit string to weight methods=["readout correction", "post-selection (exactly k ones)"], kept=0.87, # share of shots kept, between 0 and 1 steps=[{"name": "raw counts", "fidelity": 0.81}, {"name": "readout correction", "fidelity": 0.90}, {"name": "post-selection", "fidelity": 0.96}], ) ``` | Argument | Required | Meaning | |---|---|---| | `circuits` | Yes | Dispatch indices of the circuits whose output this distribution mitigates, from `metadata["qsolve"][k]["dispatch"]` of the pub result. | | `distribution` | Yes | The mitigated distribution, bit string to weight. Weights must be finite, not negative, and sum to more than zero. Qsolve normalises them. | | `methods` | No | Names of the mitigation methods, as plain text. | | `kept` | No | The share of shots that survived your checks, 0 to 1. Omit it when nothing was discarded. | | `steps` | No | Your own ladder: a list of `{"name": ..., "fidelity": ...}`. Shown as you give it. | Bit strings use Qiskit order (classical bit 0 on the right) and must match the measured register's width. This is the real call from the examples: ```python if run.report and run.dispatch is not None: ctx.report_mitigation(circuits=[run.dispatch], **run.report) ``` `run.report` is a dictionary with `distribution`, `methods`, `kept` and `steps`, built after readout correction and post-selection in `max-cut`. `molecular-ground-state` makes one call per measured circuit, with the dispatch indices of its copies. A call raises `WorkflowError` when: - `circuits` is empty or cites a circuit that was not sent: "report_mitigation cites circuits [3], but 2 were sent so far (a dispatch id is in pub_result.metadata['qsolve'][k]['dispatch'])"; - the distribution is empty, has a negative or non-finite weight, or sums to zero; - `kept` is outside 0 to 1. ## How a report is scored Qsolve computes the fidelity of your distribution against the ideal output of the circuits it covers. A report is used only when every circuit it cites shares one ideal output, and only for circuits sent by the quantum method. Otherwise it is ignored. | Number | Source | |---|---| | Raw fidelity and its floor | Measured by Qsolve | | Mitigated fidelity | Recomputed by Qsolve from your distribution; labelled reported | | Share of shots kept | The mean of your reports' `kept` values; reported | | Methods | The union of the names you gave; reported | | Steps | Your list, as written; reported | | Coverage | Measured by Qsolve | ## Fidelity requirements in benchmark cases You set the bar yourself in `benchmarks/cases.yaml`, on a noisy target: ```yaml - name: noise-model-ring-6 input_file: examples/ring_6.json target: fake:fez@1.6 seed: 1 expect: {max_gap: 0.0, min_fidelity: 0.97, max_two_qubit_gates: 40} ``` | `expect` key | The case fails when | |---|---| | `min_fidelity` | no mitigation numbers exist; fewer than 90% of the shots could be scored; or the mitigated fidelity is below the bound | | `min_postselection` | no mitigation numbers exist, or the share of shots kept is below the bound | | `max_two_qubit_gates` | the largest sent circuit, transpiled to `cx, rz, sx, x`, has more two-qubit gates than the bound | The gate `fidelity_checked` fails unless at least one case sets `min_fidelity` on a `fake:` or `ibm:` target. Set bounds to what your circuit measurably reaches, with a margin. ## What you may and may not claim You may report a distribution you computed from the counts that came back through `ctx`, with post-processing described in `methods` and `steps`, and `kept` as the share of those shots your checks retained. You may not report a distribution that was not derived from the measured counts: no ideal output from a simulation, no answer known in advance, no classical solution dressed up as a measurement. Qsolve scores a report against the ideal output, so a distribution equal to the ideal output scores a fidelity of 1. Qsolve can check that the numbers are consistent, not that your mitigation is legitimate. That is why raw figures are shown next to yours and yours are labelled reported. Describe each step in `methods` and `steps` so a reader can judge it. ## Practical advice - Readout correction works on the full distribution, so it needs `2^n` entries. The examples apply it up to 16 qubits and skip it above. - Post-selection suits problems with a conserved quantity, such as exactly k ones. Report the discarded share honestly in `kept`. - Include a `steps` ladder when you can; it shows what each stage bought. - Do not rely on mitigation to rescue a deep circuit. Keep circuits shallow first; see [Backends and targets](/docs/build/backends-and-targets). For how a researcher reads these figures, see [Accuracy, fidelity and noise](/docs/use/accuracy-fidelity-and-noise). --- # Lint codes > Every error and warning code that qsolve lint and the submission's static checks can report, what triggers it, the message and how to fix it. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/lint-codes · Updated: 2026-11-13 `qsolve lint .` runs static checks on a workflow repository. No code of yours is executed. The platform runs the same checks in the `lint` stage of a submission, before anything is built or run. Lint is defence in depth: the sandbox, not the linter, is the security boundary. - Errors (`E`) fail lint and reject a submission. - Warnings (`W`) are reported and never reject a submission. Each finding has a severity, a code, a message and a path. If the manifest cannot be loaded, lint stops after reporting `E001` or `E002` (plus any `E010` and `E011` found while listing files), because the later checks need the manifest. Fix those first and run lint again. For how lint fits in the pipeline, see [Validation and publishing](/docs/build/validation-and-publishing). For gate and run messages, see [Troubleshooting](/docs/build/troubleshooting). ## Files scanned Lint walks every file under the workflow folder, except any path that has one of these folders in it: `__pycache__`, `.git`, `.venv`, `.pytest_cache`, `.ruff_cache`, `.mypy_cache`, `node_modules`. Python files under a folder named `tests` are not checked for imports, because tests are never shipped or run. The secret scan (`E012`, `E013`) covers every file, including tests. ## Manifest and structure | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E001` | Error | There is no `qsolve.yaml` file at the workflow root. | `qsolve.yaml not found at the repository root` | Put `qsolve.yaml` at the repository root, or enter its folder in the **Folder** field when you submit. | | `E002` | Error | Loading the workflow raises a validation error, a value error or a JSON error: `qsolve.yaml` is not valid YAML, a field is missing, unknown or wrong, a schema path escapes the folder, a schema file is missing, a schema file is not a JSON object or not valid JSON. | `invalid workflow definition: ` | Run `qsolve validate .` for the field and reason. See [Manifest](/docs/build/manifest). | | `E030` | Error | The module in `entrypoint` (`package.module:Class`) is not found at `.py` or `/__init__.py` under the root or under `src/`; or the module has no top-level class, function or assignment named `Class`. An `entrypoint` module that is `qsolve` or starts with `qsolve.` is skipped. | `entrypoint: module 'x' not found in the repository` or `entrypoint: 'X' is not defined in x.py` | Match `entrypoint` to a file and a top-level class, for example `src.workflow:MaxCut` needs `src/workflow.py` defining `class MaxCut`. | | `E031` | Error | The entrypoint module has a syntax error, is not valid UTF-8 or cannot be read, or any `.py` file outside `tests` cannot be parsed (syntax error, not UTF-8, null bytes). | `x.py: syntax error line N: `, `x.py: cannot parse Python: ` or `cannot parse Python: ` | Fix the syntax at the line shown. | ## Schemas and example inputs | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E020` | Error | The input or output schema is not a valid JSON Schema draft-07. | `invalid JSON Schema: ` | Fix the schema. See [Input schema reference](/docs/build/input-schema-reference) and [Output schema reference](/docs/build/output-schema-reference). | | `E021` | Error | Anywhere in a schema, at any depth, a keyword is `pattern` or `patternProperties`; or a `$ref` string does not start with `#`; or `format` is `regex`. One finding per occurrence, with its location in the path. | `'pattern' is not allowed in input/output schemas (regular expressions can be abused to hang validation); use enum, minLength or maxLength instead`, `only local '#/...' $ref values are allowed`, or `format 'regex' is not allowed` | Use `enum`, `minLength` or `maxLength`; point `$ref` at `#/definitions/...`. Only schema keywords are checked: a property that is merely *named* `pattern` (a key under `properties`, `definitions` or `$defs`), and the data in `required`, `enum`, `const`, `default` and `examples`, are not flagged. | | `E022` | Error | The schema, serialised to JSON, is longer than 262,144 characters (256 KiB). | `schema is too large` | Shrink the schema. | | `E023` | Error | An example listed in `inputs.examples` cannot be read or is not JSON, or does not validate against the input schema. If the schema itself is invalid, this is skipped and `E020` reports. | `example input unreadable: ` or `example input does not match the input schema: ` | Fix the file path, the example, or the schema. `qsolve validate .` shows the failing path. | | `E024` | Error | `inputs.examples` is empty. | `at least one example input is required` | List at least one file under `inputs.examples`. | | `E025` | Error | A parameter named `shots` of type `int` has no `max`, a `max` above 100,000 or a `default` above 100,000. The platform runs at most 100,000 shots per circuit (`qsolve.MAX_SHOTS_PER_CIRCUIT`) and refuses a larger request, so the workflow would offer values that always fail. | `parameter 'shots' allows up to N (default M) but the platform runs at most 100,000 shots per circuit: set max (and default) to 100000 or less` | Set `max` (and `default`) to 100000 or less. | | `W052` | Warning | `targets.min_qubits_qpu` is set. The manifest accepts it, but the platform currently ignores it. | `targets.min_qubits_qpu is currently ignored by the platform` | Remove it, or keep it for when it is enforced. It does not affect any run today. | ## Repository files | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E010` | Error | A path in the repository is a symbolic link. One finding per link. | `symbolic links are not allowed` | Replace the link with a real file. | | `E011` | Error | The repository has more than 500 files, or the files add up to more than 20 MiB (20,971,520 bytes). Either condition gives its own finding. | `too many files (N > 500)` or `repository too large (N > 20971520 bytes)` | Remove files and data you do not need. | | `E012` | Error | A file path matches a credential pattern: `.env` or `.env.*` (`.env.example`, `.env.sample` and `.env.template` are allowed, and still get the secret scan `E013`), `id_rsa*`, `id_ed25519*`, `*.pem`, `*.p12`, `*.pfx`, `*.key`, `credentials*.json`, `.netrc`. | `file looks like a credential; remove it and rotate the secret` | Delete the file, and revoke the credential if it was real. A template named `.env.example`, `.env.sample` or `.env.template` is fine. | | `E013` | Error | The text of a file contains one of: a private key header (`-----BEGIN [RSA \|EC \|OPENSSH \|DSA ]PRIVATE KEY-----`); an AWS access key (`AKIA` plus 16 capitals or digits); a GitHub token (`ghp_`, `gho_`, `ghu_`, `ghs_` or `ghr_` plus 30 or more characters); a Supabase or Stripe style secret (`sb_secret_`, `sk_live_` or `sk_test_` plus 16 or more characters); an IBM Quantum token assignment (a name starting `ibm` or `qiskit` and containing `token`, set to a quoted string of 30 or more characters). Files over 1 MiB, files ending in `.png .jpg .gif .pdf .zip .npy .npz`, and files that are not UTF-8 are not scanned. | `possible committed; remove it and rotate the secret` | Remove it and rotate the secret. A workflow never needs credentials. | | `W051` | Warning | None of `LICENSE`, `LICENSE.md`, `LICENSE.txt`, `COPYING` exists at the root. | `no LICENSE file found` | Add a `LICENSE` file. | | `W060` | Warning | There is no `README.md` at the root. | `no README.md; describe what the workflow solves and its limits` | Add a README. | ## Code and imports Checked on every `.py` file outside a `tests` folder. | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E040` | Error | An absolute import (`import x` or `from x import y`) names a top-level module that is not the standard library, not a module or package of your own repository (at the root or under `src/`, or `src`), not `__future__`, not on the allowed list, and not pinned in `requirements.lock`. Relative imports are not checked. | `imports 'x', which is not available in the runtime. Allowed: the Python standard library and ` | Import only the standard library and `qsolve`, `numpy`, `scipy`, `sklearn`, `qiskit`, `qiskit_aer`, `pyscf`, `pydantic`, `jsonschema`, `yaml`, `typer`, `rich`. There is no network in the sandbox, so nothing else can be installed. | | `W041` | Warning | An import of `subprocess`, `socket`, `ctypes`, `pickle`, `marshal`, `shelve` or `multiprocessing`; or a direct call to `eval()`, `exec()`, `compile()` or `__import__()`. Each import and each call gives a finding. | `imports 'x' (blocked or useless in the sandbox; flagged for review)` or `calls eval() (flagged for review)` | Remove them. They are blocked or useless in the sandbox and flagged for review. | | `W042` | Warning | An import that would be `E040` is pinned in your `requirements.lock` instead. It is reported as this warning and not as `E040`. | `imports 'x', pinned in requirements.lock but not in the runtime image, so the sandbox will reject it` | The sandbox rejects the import at run time. Remove the dependency or replace it. See [Project layout](/docs/build/project-layout). | | `W043` | Warning | A call to something named `import_module` or `__import__` as an attribute, such as `importlib.import_module(...)`. | `imports a module by name at run time (not checked: the runtime image decides what is importable)` | Prefer ordinary imports so lint can check them. | Names in `requirements.lock` are read as the text before `==`, `>=`, `~=`, `<=`, an extras bracket or the end of the line, with `-` and `.` turned into `_`. Four distribution names are mapped to their import names: `scikit_learn` to `sklearn`, `pyyaml` to `yaml`, `pillow` to `PIL`, `opencv_python` to `cv2`. Any other package whose import name differs from its distribution name would be seen under the distribution name. ## Benchmarks and hardware cases | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E050` | Error | `benchmarks/cases.yaml` does not exist. | `benchmarks/cases.yaml is required: the platform benchmarks every workflow` | Add the file. See [Benchmarks](/docs/build/benchmarks). | | `E052` | Error | The `hardware_cases` list cannot be loaded (invalid YAML, an unknown key, a wrong type, a missing `name`, a case that does not match the case format); or a hardware case has no `target` or one that does not match `ibm:` (letters, digits and `_ . -`, 1 to 60 characters). One finding for the whole list when it cannot be loaded, one per bad case otherwise. | `hardware_cases are invalid: ` (cut at 300 characters) or `hardware case 'x' needs target: ibm:, for example ibm:ibm_fez` | Fix the entry. | | `E053` | Error | There are more than 3 `hardware_cases`. | `at most 3 hardware_cases are allowed` | Keep at most three. | | `E054` | Error | There is at least one hardware case and `targets.qpu` in the manifest does not contain `ibm`. | `hardware_cases need `qpu: [ibm]` under targets` | Add `ibm` to `targets.qpu`. See [Backends and targets](/docs/build/backends-and-targets). | | `E055` | Error | A case under `cases:` is malformed (an unknown key, a wrong type, no `name`), `cases.yaml` is not a mapping, or a case has neither `input` nor `input_file`. Also applies to `hardware_cases`: a hardware case with no input. (A file that is not valid YAML is `E052`.) | `cases are invalid: ` or `case 'x' needs `input` or `input_file`` | Give every case an `input` or an `input_file`. | | `E056` | Error | A case's `input_file` is an absolute path, contains `..`, resolves outside the repository (including through a symbolic link), does not exist, is not valid JSON, or is not a JSON object. Checked for `cases` and `hardware_cases`. | `case 'x': input_file 'p' must be a relative path inside the repository (no absolute path, no '..')`, `... not found` or `... unreadable: ` | Put the input file inside the repository and use a relative path. | | `E057` | Error | Two cases share a name, counting `cases` and `hardware_cases` together. | `duplicate case name 'x'` | Give every case a unique name. | | `E058` | Error | A case under `cases:` has a `target` starting `ibm:`. Cases run on every validation; a real device belongs under `hardware_cases`. | `case 'x' targets ibm:...: a real device belongs under `hardware_cases`, not `cases` (which run on every validation)` | Move the case to `hardware_cases`, or use `fake:` to rehearse. | | `E059` | Error | A case's input (inline or from its file) does not match the input schema. Skipped when the schema itself is invalid (`E020`). | `case 'x': input does not match the input schema: ` | Fix the case input or the schema. | ## License | Code | Severity | Triggers when | Message | Fix | |---|---|---|---|---| | `E051` | Error | The manifest's `license` is empty or only spaces. | `a license identifier is required` | Set `license` in `qsolve.yaml`, for example `Apache-2.0`. | ## Codes that do not exist This page lists every code the SDK's lint emits: 25 errors (`E001`, `E002`, `E010` to `E013`, `E020` to `E025`, `E030`, `E031`, `E040`, `E050` to `E059`) and 6 warnings (`W041`, `W042`, `W043`, `W051`, `W052`, `W060`). A code that is not here comes from a newer SDK or another tool: update with `pip install -U qsolve-sdk`. --- # Benchmarks > Every key of benchmarks/cases.yaml, the exact pass and fail rules, the eight validation gates, hardware cases, how results enter the track record and what the workflow page shows, with the lint code for each mistake. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/benchmarks · Updated: 2026-11-13 A benchmark case is a known-answer input with the result you expect, stored in `benchmarks/cases.yaml`. Qsolve runs every case on `qsolve test`, on submission, and again after publication as ordinary platform runs that build the track record. Each case compares the quantum method with the baseline, your `reference` method's result. The file is required: without it lint reports `E050`. Lint codes are listed in [Lint codes](/docs/build/lint-codes). ## The file ```yaml cases: - name: ring-6-with-chord input_file: examples/ring_6.json seed: 1 expect: {max_gap: 0.0} - name: noise-model-ring-6 input_file: examples/ring_6.json target: fake:fez@1.6 seed: 1 expect: {max_gap: 0.0, min_fidelity: 0.97, max_two_qubit_gates: 40} hardware_cases: - name: hardware-ring-6 input_file: examples/ring_6.json seed: 1 target: ibm:ibm_fez expect: {max_gap: 0.0, min_fidelity: 0.97, max_two_qubit_gates: 40} ``` This is from the `max-cut` workflow. The file has two top-level keys, `cases` and `hardware_cases`. Any other top-level key is ignored. A missing `cases` key means zero cases, which fails the `benchmark_cases` and `cases_pass` gates. A misspelled key inside a case or inside `expect` is rejected: the unknown key makes `qsolve test` stop with a validation error, and the platform's gates stage fails. Lint does not check the content of `cases`, only of `hardware_cases`, so run `qsolve test` to find such mistakes. ## Case keys | Key | Required | Default | Meaning | |---|---|---|---| | `name` | Yes | | Text shown in the track record. Uniqueness is not checked, but the track record keeps one current row per name and target, so two cases with the same name and target hide each other: keep names unique. | | `input_file` | One of the two | | Path, relative to the workflow folder, to a JSON file holding one object. The resolved path must stay inside the workflow folder. | | `input` | One of the two | | The input object written inline. If both are given, `input` wins and `input_file` is ignored. | | `params` | No | `{}` | Parameter overrides, for example `{shots: 20000}`. Checked like any run's parameters against the manifest. | | `seed` | No | `0` | Integer seed of the run. | | `target` | No | the run's target | For example `fake:fez@1.6`. Without it the case runs on the target `qsolve test` was given, `sim-cpu` by default, and on the platform `sim-cpu`. | | `expect` | No | all defaults below | What the result must satisfy. | At submission, before the sandbox gates, every case (`cases` and `hardware_cases`) is checked and the submission is rejected, naming the case, if: it has neither `input` nor `input_file`; `input_file` is absolute, contains `..`, leaves the workflow folder (including through a link) or does not exist or is not a JSON object; the input does not match your input schema; or `params.shots` is above 100,000. One bad case therefore rejects the submission; it never silently skips the others. Put large inputs in `benchmarks/inputs/` and refer to them with `input_file`. A case that raises before it produces a result is listed as `FAIL : `. ## Expectations Every `expect` key is optional and the object may be empty. | Key | Type | Default | The case passes when | |---|---|---|---| | `max_gap` | number | `0.0` | `gap <= max_gap + 1e-9`. The gap is `(quantum - baseline) / max(abs(baseline), 1e-12)`. A quantum result that beats the baseline has a negative gap and passes. A gap of 0.05 allows a quantum objective 5% worse. | | `max_abs_error` | number | none | `abs(quantum - baseline) <= max_abs_error`. When set, `max_gap` is ignored entirely. Use it when the objective is large and the interesting difference small (total energies), and for classifiers, where it bounds the difference of two error rates. It is two-sided: a result better than the baseline by more than this also fails. | | `min_fidelity` | number | none | The mitigated fidelity against the ideal output is at least this, and at least 90% of the quantum method's shots could be scored. Values outside 0 to 1 are not rejected. | | `min_postselection` | number | none | The share of shots kept by the error checks is at least this. Values outside 0 to 1 are not rejected. | | `max_two_qubit_gates` | integer | none | No circuit the quantum method sent has more two-qubit gates than this. | The objective is lower-is-better, so a maximisation problem returns a negative number from `evaluate`; see [Writing the workflow](/docs/build/writing-the-workflow). ### What fails a case whatever `expect` says The checks run in this order and the first one that applies gives the message. A case fails when: | Message | Condition | |---|---| | `no headline metric: quantum and reference could not be compared` | The quantum method or the reference produced no usable value. | | `method x failed: ...` | A method's unavailable reason starts with `failed:` (a classical method crashed). A method that is merely unavailable (`MethodUnavailable`) does not fail the case by itself. | | `method x claimed A, its solution gives B` | A method's reported objective differs from `evaluate(solution)` by more than `1e-6 * max(1, abs(value))`. | | `quantum method x sent no circuit to the backend` | The quantum method made no dispatch through `ctx`. | | `no mitigation numbers reported` | `min_fidelity` or `min_postselection` is set and the run reported none. | | `only N% of the quantum method's shots could be scored against an ideal output ...` | `min_fidelity` is set and coverage is under 90%. Circuits over 20 qubits or with mid-circuit measurement cannot be scored. | | `fidelity F < B` | The mitigated fidelity is below `min_fidelity`. | | `post-selection P < B` | The share of shots kept is below `min_postselection`. | | `no circuit size reported` | `max_two_qubit_gates` is set and no circuit was recorded. | | `N two-qubit gates > M` | A circuit exceeds `max_two_qubit_gates`. | Missing `max_gap` or `max_abs_error` fails without a message; the table shows the gap. The same rules decide a case in `qsolve test`, in the platform's gates stage, in the history runs and in hardware runs. ## Comparison with the baseline Every result carries a gap, the absolute error, and a headline accuracy with a verdict, "Matches the best classical answer" or "Below the best classical answer". The baseline is the `reference` method. Qsolve recomputes every method's objective with your `evaluate` where a method returns a solution; the platform, not your code, produces each number. Classical methods run with a backend that refuses circuits. You choose one of three metric kinds in the manifest under `metric`. The default is `relative_gap`. | `metric.kind` | Accuracy | Use for | |---|---|---| | `relative_gap` | `1 - max((q - r) / abs(r), 0)`. Only a worse result loses accuracy. | Optimisation with a lower-is-better objective. | | `relative_error` | `1 - abs(q - r) / abs(r)`, with `stderr / abs(r)` as its plus-or-minus. | Measured quantities such as energies. | | `error_rate` | `1 - q`, where `q` is a share of mistakes. The plus-or-minus is a 95% Wilson interval for `n_test` cases (the quantum method's `details["n_test"]`). | Classifiers. | `q` is the quantum objective, `r` the baseline's. A result matches when any of these holds: - `relative_gap` or `relative_error`: accuracy is at least `1 - tolerance`. - `relative_gap` or `relative_error`: `abs(q - r)` is at most `tolerance_abs`, in the objective's units. - `relative_gap` or `relative_error`: the difference is at most twice the quantum method's `stderr`. This counts only while `stderr` is at most a quarter of `abs(r)`. - `relative_gap`: the quantum objective is no worse than the baseline's. - `error_rate`: `q <= r + tolerance + tolerance_abs`, or the lower end of the 95% interval of `q` is at most `r + tolerance + tolerance_abs`. `tolerance` is between 0 and 1 (default 0), `tolerance_abs` is at least 0 (default none), `note` is at most 600 characters and is shown beside the number. Set a tolerance only when exact equality is the wrong standard, for example chemical accuracy of 1.6 mHa, written `tolerance_abs: 0.0016`. The manifest fields are documented in [Manifest](/docs/build/manifest). This verdict is separate from `expect`: a case can pass its `expect` and still show "Below the best classical answer", and the other way round. ## The validation gates `qsolve test` and the platform's gates stage run these gates on `sim-cpu` with no network. A case with its own `target` keeps it. | Gate | Passes when | |---|---| | `manifest` | The manifest and both schemas load and validate. | | `example_inputs` | At least one example exists and every example validates against the input schema. | | `benchmark_cases` | At least one case is defined. | | `methods_declared` | The manifest declares at least two methods. | | `fidelity_checked` | At least one case sets a non-zero `min_fidelity` and runs on a `fake:` or `ibm:` target. | | `cases_pass` | At least one case ran and every case meets its `expect`. | | `reproducible` | Re-running the first case that ran, same input, params, target and seed, gives the same result hash, or a result equal up to relative 1e-8 (absolute 1e-12) on every number. Wall-clock method times are ignored. | | `estimate_honest` | Your `estimate()` is within a factor of two of the circuits and the shots that case sent (ratio from 0.5 to 2.0, each checked only when the run sent some). Passes with a note when you give no `estimate()` or the run sent no circuits. | `reproducible` and `estimate_honest` are added only when at least one case ran to a result, so a workflow whose every case raises shows six gates. All gates must pass to publish. Gate messages and fixes are in [Troubleshooting](/docs/build/troubleshooting). ## Noise-model cases Include one case on a noisy target with `min_fidelity` set; the `fidelity_checked` gate needs it. Use `fake:fez@1.6`, a device noise model with errors scaled up by 1.6 (the scale is between 1 and 10). It runs on a CPU and costs nothing. This is from `generic-qubo`: ```yaml - name: noise-model-qubo-5 input_file: examples/qubo_5.json target: fake:fez@1.6 seed: 1 expect: {max_gap: 0.0, min_fidelity: 0.97, max_two_qubit_gates: 30} ``` `qsolve test --target fake:fez` also satisfies the gate for a `min_fidelity` case without its own target, but the platform always validates on `sim-cpu`, so declare the target in the case. Do not put `ibm:` targets under `cases`. Validation has no network and no device access, so such a case cannot run and fails. Lint does not flag it. Real-device cases go under `hardware_cases`. ## Hardware cases A device run is slow, scarce and costs money, so Qsolve never starts one for you and validation ignores them. List the cases you want measured on a real IBM device under `hardware_cases`. This is from `electrolyte-solvent-screen`: ```yaml hardware_cases: - name: hardware-water-only input: basis: sto-3g solvents: [water] params: {shots: 1000} seed: 1 target: ibm:ibm_fez expect: {max_abs_error: 0.02, min_fidelity: 0.97, min_postselection: 0.9} ``` A hardware case has the same keys as a case, and `target` is required. | Rule | Value | Lint code | |---|---|---| | `hardware_cases` must load: valid YAML, keys known, types right | | `E052` | | `target` matches `ibm:`: letters, digits, `_ . -`, 1 to 60 characters | for example `ibm:ibm_fez` | `E052` | | At most this many entries | 3 | `E053` | | The manifest lists `ibm` under `targets.qpu` | | `E054` | | All hardware cases together, inputs written out, are at most | 600 KB | none: publication stops with "The hardware cases are too large" | | Each case has `input` or an `input_file` that exists inside the workflow folder | | none: the submission is rejected with a message naming the case | | Each case's input matches your input schema | | none: the submission is rejected with the schema path that failed | Lint itself does not check these last two; the platform does, right after lint and before the gates. Keep them few and small: a case a device can handle at a price you accept. ### How they run 1. Publish the workflow. At publication Qsolve copies each hardware case, with its input file written inline, into the version. Hardware cases of a version are fixed at that moment. 2. Open the workflow page as its developer (admins can too) and choose **Get a quote**. The platform creates one run per case in your account and prices each. Nothing runs until you confirm each quote. Only the latest published version's cases are started. If a previous batch is still being priced or run, the request is refused as in progress. 3. The run needs the target to be in the manifest's `targets` and the device to be available, or it is refused with `target_not_supported` or an availability error. 4. The runs are ordinary runs paid from your own credits, each capped by the platform's per-run limit (50 credits unless the operator changed it). 5. A maintenance pass adds each finished run to the track record, 20 runs per pass, so it can take a minute. A run that failed for platform reasons (`platform_error`, `backend_unavailable`, `run_too_large`, `insufficient_credits`, `unsupported_target`, `cancelled`) says nothing about the workflow and is not recorded. A run that failed because of the workflow (a crash, a timeout) is recorded as failed. 6. The pass or fail uses the same rules as any case. A newer result of the same case name on the same target replaces the earlier one, which stays as history. ## How results enter the track record | Source | When | Target | Billed to | |---|---|---|---| | Gates stage | On every submission | `sim-cpu` and each case's own target | Nothing recorded; a check only | | History stage | After publication of a new version | Each `cases` entry, on its own target or `sim-cpu` | The platform's system account, at most 20 credits and 900 seconds per case, three cases at a time | | Hardware cases | When the developer confirms a quote | Each `hardware_cases` entry | The developer | The history stage runs each case through the real platform pipeline (estimate, credit hold, sandbox, settlement), so the recorded cost is what a researcher would pay. For each run Qsolve stores the case name, target, device, pass or fail, gap, absolute error, both objectives, shots, run time, credits, the seed, the params and the quality numbers. A case the platform could not run is not recorded and is counted in the submission report. If the history stage fails as a whole, the stage shows as skipped and the version stays published. Resubmitting identical content (the same version and digest) does not run the history stage again when the version already has recorded results, so the track record is never duplicated. Resubmitting identical content of an existing version changes nothing. A new version starts with its own empty track record. ## What the workflow page shows | Part | What it contains | |---|---| | Track record | Latest result of each case on a `sim-` target for the current version, with accuracy, verdict, cost and circuit numbers. The headline accuracy is the median over these rows. The workflow cards count passed and total cases on `sim-` targets only. | | Noise-model estimate | Latest results on `fake:` targets, with fidelity and share of shots kept. Shown apart, not in the headline. | | On quantum hardware | Latest results on every other target (`ibm:`). Shown apart, not in the headline, because the hardware measures the device as much as the workflow. | | Earlier results | Older results of the same case and target, kept as history. | Versions and track record: only the current version's `sim-` and `fake:` results are shown. Hardware results of earlier versions stay on the page, labelled with their version, until a newer run of the same case replaces them, because they are scarce. See [Versioning](/docs/build/versioning). ## Lint codes for benchmark mistakes | Mistake | Code | |---|---| | `benchmarks/cases.yaml` missing | `E050` | | `hardware_cases` unreadable or invalid, or a hardware case without an `ibm:` target | `E052` | | More than three `hardware_cases` | `E053` | | `hardware_cases` without `qpu: [ibm]` in the manifest | `E054` | Everything else in the file is checked by `qsolve test`, not by lint. ## Writing good cases - Use inputs where you know the answer, or where the reference is exact. - Cover the range: a tiny case, one near your envelope, a degenerate one. `generic-qubo` has cases with and without its cardinality constraint, up to 18 variables. - Include a case where the quantum method is expected to do worse, with an honest tolerance. `portfolio-optimization` allows `max_gap: 0.05` and `0.10` on its larger synthetic cases; `max-cut` includes a complete graph, where low-depth QAOA struggles. - Keep cases quick. The whole gates run, including the second run of the first case, is one sandbox job. Its time limit is your manifest's `resources.timeout_s` raised to at least 300 seconds and capped at 7200; its memory is `resources.memory_mb` capped at 8192. - For classifiers, use test sets of at least 200 points. On fewer, one point moves accuracy by several percent and the interval is wide. `quantum-kernel-classifier` bounds the error-rate difference with `max_abs_error: 0.06`. - Set noise-model bounds from measured values and leave a margin; the `electrolyte-solvent-screen` workflow sets its tolerances at about three times the shot noise. --- # The qsolve command line > Reference for every qsolve command, argument and option: init, validate, lint, run, estimate and test, with defaults, exit codes, output formats, environment variables, error messages and examples. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/cli · Updated: 2026-11-13 `qsolve` is installed with `qsolve-sdk` (`pip install "qsolve-sdk[ibm]"`). It is a [Typer](https://typer.tiangolo.com) application with six commands, all working on a workflow directory on your machine. The platform runs the same SDK code, so these are the checks it applies. The Python behind each command is documented in the [SDK reference](/docs/build/sdk-reference). | Command | Purpose | |---|---| | [`init`](#qsolve-init) | Scaffold a working Qiskit workflow (a QUBO solved with QAOA) to start from. | | [`validate`](#qsolve-validate) | Check the manifest, schemas and example inputs. | | [`lint`](#qsolve-lint) | Static checks the platform applies before running anything. No code is executed. | | [`run`](#qsolve-run) | Run a workflow locally on one input. | | [`estimate`](#qsolve-estimate) | Size a run (qubits, circuits, shots) without executing it. | | [`test`](#qsolve-test) | Run the validation gates and benchmark cases, the same ones the platform runs. | ## Global options ```text qsolve [OPTIONS] COMMAND [ARGS]... ``` | Option | Meaning | |---|---| | `--version` | Print the SDK version (for example `1.0.0`) and exit 0. | | `--install-completion` | Install shell completion for the current shell. | | `--show-completion` | Print the completion script, to copy or customise. | | `--help` | Show help and exit 0. Also available on every command: `qsolve run --help`. | Running `qsolve` with no command prints the help and exits 2 (the usage-error code of the Click library under Typer). ## Targets The `--target` option of `run`, `estimate` and `test` takes one of: | Value | Runs on | |---|---| | `sim-cpu` | Qiskit Aer on the CPU. The default. Noise-free. | | `sim-gpu` | Qiskit Aer on a GPU; needs `qiskit-aer-gpu`. | | `fake:[@scale]` | Aer with an IBM device's published noise model (`fake:fez`), optionally with every error rate multiplied by `scale`, from 1 to 10 (`fake:fez@1.6`). Free. Needs the `ibm` extra. | | `ibm:` | IBM Quantum hardware, with your own token (`ibm:ibm_fez`). Needs the `ibm` extra. Spends your IBM quota. | See [Backends and targets](/docs/build/backends-and-targets). `estimate` does not run anything, so its `--target` is only passed to your workflow's `estimate()`; it is not checked. ## Environment variables | Variable | Used by | Purpose | |---|---|---| | `QSOLVE_IBM_TOKEN` | `--target ibm:` | Your IBM Quantum token. Required for that target. Never put it in workflow code or the repository. | | `QSOLVE_IBM_INSTANCE` | `--target ibm:` | Optional IBM instance. | | `QSOLVE_SIM_THREADS` | simulators | Threads a simulation may use. A positive integer; otherwise every core available to the process. | | `QSOLVE_EXECUTOR_ADDR`, `QSOLVE_EXECUTOR_TOKEN` | the platform's sandbox | Set by the platform, not by you. When `QSOLVE_EXECUTOR_ADDR` is set, every run uses the platform's executor whatever `--target` says. Leave it unset locally. | ## Exit codes | Code | Meaning | |---|---| | `0` | The command succeeded. | | `1` | The command ran and found a problem (see each command below), or an unexpected exception: Python prints a traceback and exits 1. A missing or invalid `qsolve.yaml`, an unreadable input file and a bad target are never tracebacks: they print `Error:` or `Invalid workflow:` and exit 1. | | `2` | A usage error caught by the command-line parser: a missing required option or argument, an unknown option, or a malformed `--param`. | | Command | 0 | 1 | |---|---|---| | `init` | the workflow was created | invalid name, or the destination exists | | `validate` | manifest, schemas and examples are valid | any of them is invalid | | `lint` | no errors; warnings allowed | at least one error | | `run` | the run completed | a `WorkflowError` such as an invalid input, parameter or target; an unreadable input file; a missing or invalid `qsolve.yaml` | | `estimate` | the estimate was printed | the same causes as `run` | | `test` | every gate passed | a gate failed, or the workflow is invalid | ## qsolve init ```text qsolve init NAME [--summary TEXT] [--directory PATH] ``` Creates a complete workflow in `/` that solves a small QUBO with QAOA and passes every gate. | Argument or option | Default | Meaning | |---|---|---| | `NAME` | required | 2 to 63 lowercase letters, digits and dashes, starting with a letter or digit (regular expression `[a-z0-9][a-z0-9-]{1,62}`). It becomes the workflow's name in `qsolve.yaml`. | | `--summary` | `My Qsolve workflow` | One-line description, written into `qsolve.yaml` (`summary:`) and the README. Whitespace runs, line breaks included, become single spaces. The value is written as a quoted YAML string, so colons, `#`, quotes and a leading dash are safe. | | `--directory` | the current directory | The parent directory. Created if missing. | Files created: ```text / qsolve.yaml the manifest, with name and summary filled in README.md a short readme with the develop commands benchmarks/cases.yaml three cases: two noise-free, one on fake:fez@1.6 examples/qubo_5.json a 5-variable example input schemas/input.json schemas/output.json src/__init__.py src/workflow.py the Qiskit workflow (class Qubo) tests/test_workflow.py a pytest file that runs the example through run_workflow ``` It creates no `LICENSE` and no `requirements.lock`, so `qsolve lint` warns `W051` until you add a license file. File meanings: [Project layout](/docs/build/project-layout). Walk-through: [Your first workflow](/docs/build/your-first-workflow). On success it prints: ```text Created Next: cd && qsolve test . (edit src/workflow.py: it is plain Qiskit) ``` | Message (printed in red) | Exit | Cause | |---|---|---| | `Name must be lowercase letters, digits and dashes (2-63 chars).` | 1 | `NAME` does not match the pattern (uppercase, underscore, one character, 64 or more). | | ` already exists.` | 1 | The destination exists, as a file or a directory. Nothing is overwritten. | The summary and the name are written into `qsolve.yaml` as quoted YAML strings, so `--summary "Pick: the depots # fast"` is stored exactly as typed. ## qsolve validate ```text qsolve validate [WORKFLOW] ``` | Argument | Default | Meaning | |---|---|---| | `WORKFLOW` | `.` | The workflow directory. | Loads `qsolve.yaml`, reads both schema files and checks every example input listed under `inputs.examples` against the input schema. Prints, in green: ```text OK depot-picker v0.1.0 ``` | Message (red) | Exit | Cause | |---|---|---| | `Invalid workflow: ` | 1 | No `qsolve.yaml` (`No qsolve.yaml in `), invalid YAML, a manifest that breaks a rule (the reason is the pydantic error text), a schema file missing or not a JSON object, or a schema path that leaves the directory. | | `Example : ` | 1 | An example file is unreadable (`cannot read : ...`), is not a JSON object, or does not match the input schema (`Invalid input at : `). It stops at the first bad example. | `validate` does not check the schemas against Qsolve's restrictions and does not inspect your code; `lint` does that. It does not require that at least one example exists; `lint` and `test` do. ## qsolve lint ```text qsolve lint [WORKFLOW] ``` | Argument | Default | Meaning | |---|---|---| | `WORKFLOW` | `.` | The workflow directory. | Runs the static checks the platform applies before it executes anything. None of your code runs. Each finding is one line: the severity (upper case, padded to 7 characters), the code, the message and, when there is one, the path (relative to the workflow, shown dim): ```text WARNING W051 no LICENSE file found 0 error(s), 1 warning(s) ``` With no findings it prints `No problems found`. Otherwise the last line is ` error(s), warning(s)`. Errors print in red, warnings in yellow. It exits 1 when there is at least one error; warnings alone exit 0. A missing or invalid manifest is reported as a finding (`E001`, `E002`), not as a crash. Every code is explained in [Lint codes](/docs/build/lint-codes). ## qsolve run ```text qsolve run WORKFLOW --input FILE [--target T] [--seed N] [--param key=value]... [--out FILE] ``` Runs the workflow once on one input, with every declared method. | Argument or option | Short | Default | Meaning | |---|---|---|---| | `WORKFLOW` | | required | The workflow directory. | | `--input` | `-i` | required | Path to the input JSON file. It must contain a JSON object. | | `--target` | | `sim-cpu` | Where to run; see [Targets](#targets). | | `--seed` | | `0` | The run's seed (an integer). The same input, parameters and seed give the same result. | | `--param` | `-p` | none | A parameter override, `key=value`. Repeatable. The value is parsed as JSON where possible, so `500` is a number, `0.5` a float, `true` a boolean and `[1,2]` a list, while `fast` stays text. The key must be a parameter declared in the manifest. | | `--out` | | none | Write the full JSON output, the result and its provenance, to this file (UTF-8, indented). | ```bash qsolve run . -i examples/qubo_5.json --seed 1 -p layers=1 ``` ```text depot-picker v0.1.0 on aer-cpu ┌───────────────────────────────┬─────────────────────┐ │ Metric │ Value │ ├───────────────────────────────┼─────────────────────┤ │ Objective (quantum) │ -10 │ │ Objective (Exhaustive search) │ -10 │ │ Gap vs baseline │ 0.000% │ │ Uncertainty │ exact (stderr None) │ │ Shots │ 4000 │ │ Run time (s) │ 2.22 │ └───────────────────────────────┴─────────────────────┘ QAOA (quantum approximate optimisation): -10; Exhaustive search: -10 (accuracy 100.00%). ``` The table title is ` v on `. The row labelled with the reference is named after the reference method's manifest title. After the table it prints the result's summary sentence, and `Wrote ` when `--out` was given. The `--out` file is the `RunOutput` as JSON: a `result` object (answer, objectives, gap, uncertainty, per-method results, metrics, quality measurements) and a `provenance` object (input hash, parameters, seed, target, device, shots, timings, result hash). Every field is described in [Results and events](/docs/build/sdk-results-and-events). | Message | Exit | Cause | |---|---|---| | `Error: ` (red) | 1 | Any `WorkflowError`. Common ones below. | | `Error: cannot read : ` | 1 | The input file is missing or not valid JSON. | | `Error: must contain a JSON object` | 1 | The input JSON is not an object. | | `Error: unknown parameters: ['x']` | 1 | `--param` names a parameter the manifest does not declare. | | `Error: parameter 'layers' must be <= 6.0` (or `>= ...`, `must be an int`, `must be a number`, `must be one of [...]`) | 1 | A parameter value is out of range or the wrong type. | | `Error: Invalid input at Q: [[1]] is too short` | 1 | The input does not match the input schema; the path names the field. | | `Error: Unknown target 'foo'. Use sim-cpu, sim-gpu, ibm: or fake:[@scale].` | 1 | A bad target. | | `Error: No qsolve.yaml in ` (or the manifest's validation message) | 1 | `run` and `estimate` on a directory with a missing or invalid `qsolve.yaml`: the same causes as `validate`'s `Invalid workflow:`. | | `Error: QSOLVE_IBM_TOKEN is not set` | 1 | `ibm:` target without a token. | | `Error: Install the ibm extra: pip install 'qsolve-sdk[ibm]'` | 1 | `ibm:` or `fake:` target without the extra. | | `Error: no fake device 'x' (e.g. fez, marrakesh, torino)` | 1 | Unknown fake device. | | `Error: noise scale must be between 1 and 10` | 1 | `fake:@` out of range. | | `Error: Problem has N variables; this workflow supports at most M ()` | 1 | The input is bigger than the manifest's envelope. | | `Error: the quantum method or the reference produced no result: ` | 1 | A method was unavailable or raised `MethodUnavailable`. | | `Error: Workflow produced an invalid answer: ` | 1 | `answer()` returned something the output schema rejects. | | `Error: --param expects key=value, got 'foo'` | 2 | A malformed `--param`; printed by the usage-error formatter. | An unexpected exception in your own code, or a workflow directory with no valid `qsolve.yaml` (use `validate` first), is not caught by `run` and prints a Python traceback, exit code 1. ## qsolve estimate ```text qsolve estimate WORKFLOW --input FILE [--target T] ``` Validates the input and sizes the run without executing any method (no circuit runs), printing the estimate as JSON. | Argument or option | Short | Default | Meaning | |---|---|---|---| | `WORKFLOW` | | required | The workflow directory. | | `--input` | `-i` | required | Path to the input JSON file (a JSON object). | | `--target` | | `sim-cpu` | Passed to your workflow's `estimate(target)`; not checked. | ```json { "qubits": 5, "circuits_on_target": 1, "shots_on_target": 4000, "classical_cost_class": "low", "runtime_s": null, "notes": [], "by_default": false } ``` `by_default` is `true` when the workflow gives no `estimate()` and Qsolve fell back to one circuit and `params["shots"]` shots. `runtime_s`, when the workflow predicts it, includes the fixed 20 s run overhead. **There is no `--param` option, so the estimate uses the manifest defaults.** Errors are the same `Error: ` lines as for `run` (invalid input, envelope, and so on), exit 1. The fields are explained in [Metering and cost](/docs/build/sdk-metering-and-cost#estimate). ## qsolve test ```text qsolve test [WORKFLOW] [--target T] [--out FILE] ``` Runs the validation gates and every benchmark case, as the platform does on submission. | Argument or option | Default | Meaning | |---|---|---| | `WORKFLOW` | `.` | The workflow directory. | | `--target` | `sim-cpu` | The target for cases that do not set their own `target`. | | `--out` | none | Write the full JSON report here (UTF-8, indented). | It prints a table of gates (name, `pass` or `FAIL`, detail), a table of cases titled `Cases on ` (name, result, relative gap, mitigated fidelity, seconds; `-` for a missing value; a failed case shows its reason after `FAIL`), then `ALL GATES PASSED` in green or `FAILED` in red. See [Your first workflow](/docs/build/your-first-workflow) for a real output and [Benchmarks](/docs/build/benchmarks) for each gate and `expect` key. The JSON report holds `workflow`, `version`, `target`, `passed`, `gates`, `cases` and `total_time_s` ([`BenchmarkReport`](/docs/build/sdk-testing-and-benchmarks#benchmarkreport)). | Message | Exit | Cause | |---|---|---| | `ALL GATES PASSED` | 0 | Every gate passed. | | `FAILED` | 1 | At least one gate failed. | | `Invalid workflow: ` (red) | 1 | No `qsolve.yaml`, an invalid manifest, a missing schema, or a malformed `cases.yaml`. No tables are printed. | A target that cannot be used (for example `ibm:` without a token) does not stop the command; each affected case fails with the reason in its row. `qsolve test` runs every case once, then the first case again and an estimate, so with `--target ibm:` it spends your own IBM quota. `qsolve test` does not run lint; run `qsolve lint .` too, because the platform runs both. Cases listed under `hardware_cases:` are never run by `qsolve test`. ## A typical loop ```bash qsolve validate . && qsolve lint . && qsolve test . ``` Because every command exits non-zero on failure, this stops at the first problem. Run it before every submission; see [Validation and publishing](/docs/build/validation-and-publishing). ## The same checks from Python | Command | Python | |---|---| | `qsolve run` | `qsolve.run_workflow(dir, input, target=, params=, seed=)` | | `qsolve estimate` | `qsolve.estimate_run(dir, input, target=)` | | `qsolve validate` | `qsolve.load_workflow(dir)` and `qsolve.runner.validate_input` | | `qsolve lint` | `qsolve.lint.lint_workflow(dir)` | | `qsolve test` | `qsolve.benchmark.run_benchmarks(dir, target)` | --- # Validation and publishing > What happens when you submit a GitHub repository to Qsolve: the five stages, the checklist you see, limits and timeouts, how failures look and are fixed, and how a workflow is listed. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/validation-and-publishing · Updated: 2026-11-13 Qsolve fetches one pinned commit, lints it, runs the gates and benchmark cases in a sandbox, publishes an immutable version, then records the cases as the track record. A repository that passes `qsolve validate`, `qsolve lint` and `qsolve test` locally passes the same checks here. ## Submitting On the [Publish a workflow](/developers) page, choose the repository, and optionally a branch, tag or commit and a folder. | Field | Accepted | Default | |---|---|---| | GitHub repository | `https://github.com//`, up to 200 characters. A trailing `/` or `.git` is removed. Other hosts and `http://` are refused. | required | | Branch, tag or commit | A branch or tag name, or a full 40-character lowercase commit SHA. Up to 100 characters from letters, digits and `. _ / -`, not starting with `-`. A short SHA is read as a name and will not resolve. | `HEAD`, the default branch | | Folder | A relative path such as `workflows/max-cut`, up to 200 characters. Each part starts with a letter, digit or `_`, then letters, digits and `. _ -`. No `..`, no leading dot, no absolute path. | empty, the repository root | Qsolve fetches exactly one commit and records its SHA, so a submission is a snapshot: later pushes do not change it. ## What you see while it is checked The submission page shows a checklist. Finished steps are ticked, the running one spins, the rest wait, and a failed step shows a cross. | Step shown | Stage name in the report | What it does | |---|---|---| | Fetch the repository | `fetch` | Gets the commit. No code runs. | | Static checks | `lint` | The same checks as `qsolve lint`. No code runs. | | Check cases, shots and the version | `publish-checks` | Rejects early, before any code runs: shots above 100,000, unusable benchmark or hardware case inputs, and a version that already exists with different content. Shown only when it fails. | | Run it in the sandbox | `gates` | Runs the validation gates and every benchmark case, with no network. | | Publish | `publish` | Stores an immutable snapshot and registers the version. | | Build the track record | `history` | Runs the benchmark cases as ordinary platform runs and records them. | A failed stage ends the submission; the later steps stay grey. The `history` stage never fails a submission. | Status shown | Meaning | |---|---| | Waiting | Queued. | | Checking | A stage is running. | | Published | Every gate passed and the version is registered. | | Needs changes | A stage failed because of your repository. The report says which and why. | | Platform error | A problem on our side, not in your workflow. Retry. | Open a submission to see its stages, the errors to fix first, gate details and, collapsed, the lint warnings. ## Limits and timeouts | Limit | Value | |---|---| | Submissions being checked at once, per user | 2 | | Submissions per 24 hours, per user | 10 (a retry of a failed one does not count again) | | Repository size at fetch | 50 MB | | Fetching the commit | 150 seconds for the download, 60 for resolving a branch name, 30 for the other git steps | | Files and size seen by lint | 500 files, 20 MB (see `E011`) | | Gates stage time | Your manifest `resources.timeout_s`, raised to at least 300 and capped at 7200 seconds, for the whole sandbox job including the second run of the first case | | Gates stage memory | Your manifest `resources.memory_mb`, capped at 8192 MB | | History stage, per case | 20 credits and 900 seconds, three cases at a time | | A validation job that dies | Tried twice. A submission with no live job after one hour is marked as a platform error. | The two submission limits are settings of the platform and can be changed by its operator; the numbers above are the defaults. ## Fetch Fetching is hardened: HTTPS only, no hooks, no history, no submodules, no large-file downloads, and a size and time watchdog. Qsolve rejects a repository that: - is larger than 50 MB ("repository exceeds 50 MB"); - contains symbolic links ("The repository contains symbolic links, which are not allowed.") or submodules; - cannot be resolved ("Could not resolve 'x' in this repository": a wrong name, or a private repository that is not connected); - takes too long to fetch ("Fetching the repository timed out."); - has no `qsolve.yaml` in the folder you entered ("No qsolve.yaml in the folder 'x' of this repository at that commit."). ## Lint Lint errors reject the submission: "Static checks failed. Fix the errors below and resubmit." Warnings do not. The report lists each error with its code, message and file, and the stage shows the error count. Every code, its trigger and its fix is in [Lint codes](/docs/build/lint-codes). ## Gates This is the equivalent of `qsolve test` on the `sim-cpu` target, with no network, inside the sandbox. The eight gates and the rules for each case are in [Benchmarks](/docs/build/benchmarks). A failure lists each failing gate with its detail: ```text Validation gates failed: cases_pass, estimate_honest cases_pass: 2/3 passed estimate_honest: estimated 1 circuits, the run sent 12 ``` If the run cannot complete, the report says "the benchmark run timed out" or "the benchmark run ran out of memory", or gives the error your code raised. Hardware cases are not run here. ## Publish Qsolve snapshots your files, excluding tests and caches, and registers the version under your manifest's `name` and `version`. It also copies your `hardware_cases` into the version with their inputs written out. If that name and version already exist with different content, the submission is rejected right after the static checks, before the sandbox gates: "Version X of already exists with different content; bump the version in qsolve.yaml and resubmit." Resubmitting identical content is accepted and changes nothing: the report says so, the latest version is not moved, and the benchmark history is not run again. A new version of a workflow you own needs only a higher `version`. See [Versioning](/docs/build/versioning). ## History Each `cases` entry runs again as an ordinary platform run, and accuracy, cost and circuit measurements are recorded as the track record. These runs are paid by the platform, not by you. A case that runs and fails on its own terms is recorded as failed and appears in the track record. A case the platform could not run (for example an unsupported target) is not recorded and is counted in the report. If the history stage as a whole cannot run, it shows as skipped and the version stays published. How results are recorded and shown is described in [Benchmarks](/docs/build/benchmarks). ## When it fails, and sync again | You want to | Do this | What happens | |---|---|---| | Check a failed submission again | **Retry** | The same submission is queued again with its report cleared, the same repository, branch and folder. It needs a free slot of the 2 in progress. | | Pick up new commits of a published one | **Sync again** | A new submission is created for the same repository, branch and folder, and fetches the newest commit. It counts toward the 10 per day. If the manifest `version` did not change and the content did, publishing is refused until you bump it. | | Clear an entry from your list | **Remove** | Only the list entry goes. A published workflow stays published. | Retry and Sync again are refused while the submission is still being checked ("This submission is still being checked."). If you fix the repository, push and press **Retry** on a failed submission that points at a branch: it fetches the branch tip again, not the old commit. A submission that pinned a commit SHA keeps failing until you submit a new SHA. ## Subfolders, branches and private repositories - Subfolders: one repository can hold several workflows. Submit each with its folder. Lint, the gates and the snapshot use that folder as the workflow root, and every path in the manifest, schemas and benchmark cases is relative to it. The folder dropdown lists the folders that contain a `qsolve.yaml` (up to 100, at most seven levels deep). - Branches: choose a branch, or leave the default. The dropdown lists up to 200 branches. - Private repositories are fetched through the Qsolve GitHub app, with read-only access to the repositories you choose; see [Private repositories](/docs/build/private-repositories). A new version of a private workflow you own is fetched the same way automatically. The workflow page does not show the repository address for a private source. ## Managed accounts Some GitHub accounts are operated by Qsolve itself and publish Qsolve's own first-party workflows. Submitting a repository of such an account is refused for everyone but Qsolve's administrators ("Repositories of that GitHub account are published by Qsolve."). If you see this message for your own repository, its owner name matches a managed account: use a different account or organisation. ## Publication and visibility A new workflow is unlisted until it is approved but runnable by its link meanwhile, unless the operator has turned on automatic listing of validated workflows, or the developer is an administrator. Later versions of an existing workflow keep its visibility. ## How a workflow appears in the marketplace The listing is built from your manifest and Qsolve's measurements: - The title is your `name` with dashes turned into spaces and words capitalised. Summary, family, category and tags come from the manifest. - The input form is built from your input schema, with your example inputs as starting points. See [Input and output schemas](/docs/build/input-and-output-schemas). - Each method appears with the `title`, `what`, `strength` and `limit` you wrote. - Supported targets and the envelope come from `targets` and `envelope`. - The track record shows each benchmark case's accuracy against the baseline, its cost and its circuit and fidelity measurements, all measured by Qsolve. For how researchers find and run it, see [What is Qsolve](/docs/start/what-is-qsolve) and [How it works](/docs/start/how-it-works). --- # Private repositories > Publish a workflow from a private GitHub repository: connect the Qsolve GitHub app, choose repositories, and what other people can and cannot see. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/private-repositories · Updated: 2026-11-12 You can publish a workflow from a private GitHub repository. You install the Qsolve GitHub app on the repositories you choose, and Qsolve reads only those, read-only. Other people see the workflow's results and track record. They never see your code or the repository address. ## Connect GitHub 1. Open [Publish a workflow](/developers) and choose **Private repository** under **Submit a repository**. 2. Select **Connect GitHub**. GitHub asks which account and which repositories to give the app. Choose only the repositories you want to publish from. 3. GitHub sends you back to Qsolve, and your account now shows next to the repository list. If the repositories belong to an organisation and you are not an owner, GitHub sends the request to an owner, and you connect again once they approve it. ## Submit Pick the repository from the list, add a branch, tag or commit if you do not want the default branch, and select **Submit**. Qsolve fetches that one commit, checks it and benchmarks it exactly as it does for a public repository; see [Validation and publishing](/docs/build/validation-and-publishing). A new version of a private workflow is fetched the same way. Open the workflow in **Your workflows** and select **Publish**. ## What other people can see | | Public repository | Private repository | |---|---|---| | The workflow page and its track record | Yes | Yes | | Running the workflow | Yes | Yes | | The repository address | Shown as a link | Not shown | | Your code | Readable on GitHub | Not shown anywhere | The results are measured by Qsolve, not reported by your code, so a closed workflow has the same verifiable record as an open one. Qsolve keeps a copy of the code it ran, as it does for every workflow, so that every run uses exactly the checked version. ## What the app can do The app is granted **read-only access to repository contents**, for the repositories you chose and nothing else. It cannot write to your repositories or read the others. Qsolve fetches with a token that lasts about an hour and does not store it. To stop Qsolve reading a repository, remove it from the app's access on GitHub. To disconnect an account in Qsolve, use the **x** next to it on that page; that forgets the connection but does not uninstall the app, so remove the app on GitHub too if you want access gone. ## If it does not work | You see | Cause | Fix | |---|---|---| | The repository is not in the list | The app was not given it | Choose **Add an account or repositories** and share it on GitHub | | "The Qsolve GitHub App cannot read that repository" | Access was removed after you connected | Share the repository with the app again | | "This connection attempt expired" | You took longer than 15 minutes on GitHub | Start again from Publish a workflow | | "Could not resolve 'x' in this repository" | The branch or tag does not exist | Check the name | --- # Versioning > How Qsolve versions a workflow: the semver string in qsolve.yaml, the content digest and what it covers, what happens when you resubmit a version, what is stored per version, which version runs and what happens to old results. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/versioning · Updated: 2026-11-13 Each published version of a workflow is an immutable snapshot identified by the `name` and `version` in `qsolve.yaml`. To update a published workflow, change the code, raise `version`, push and submit the repository again. Results already produced stay attached to the version that produced them. ## The version field `version` is `MAJOR.MINOR.PATCH`: three whole numbers separated by dots, such as `1.0.1`. The rule is `^\d+\.\d+\.\d+$`; anything else fails with `version must be semver, e.g. 0.1.0`. A pre-release suffix such as `-beta`, a leading `v` and a two-part version are all rejected. Write all three parts, because YAML reads `1.0` as a number, which is rejected as well. ### What the platform enforces The platform enforces one rule, and it is about content, not about the numbers: > A `name` and `version` that already exist may only be submitted again with **identical content**. Different content under the same version is refused. It does not compare version numbers with each other. Nothing requires the new version to be greater than the old one, that the bump be of any particular kind (major, minor or patch), or that numbers be consecutive. "Bump the version" therefore means: change the string so the `name` and `version` pair has not been published before. A convention that helps researchers read your history: | Change | Bump | |---|---| | A fix that does not change what the workflow computes | patch | | A new optional input field, parameter or method; better performance | minor | | A change to the input or output schema that breaks existing inputs or readers | major | ## The content digest Every submission is reduced to one digest of the workflow's files. That digest is what is compared when a version is resubmitted. It is computed like this: 1. Take every file under the workflow folder, except the files [left out of the snapshot](/docs/build/project-layout#left-out-of-the-published-snapshot): the cache and tool directories, any `tests` directory, files and folders starting with `~`, `.pyc` and `.pyo` files, `.DS_Store`, `Thumbs.db` and `desktop.ini`. 2. Sort them by path, comparing each path part by part and case-sensitively, so the order is the same on Windows and Linux. 3. For each file, in that order, feed SHA-1 the path relative to the workflow folder (with `/` separators) and a zero byte, then the raw SHA-256 digest of the file's bytes. 4. The 40-hex-character result is the digest. What follows from that: - **Everything that is stored counts**, including `qsolve.yaml`, the schemas, the example inputs, `benchmarks/`, `README.md` and `LICENSE`. Editing a typo in the README changes the digest, so it needs a new version. - **Bytes count, not meaning.** Line endings, trailing whitespace and file renames all change the digest. - **What is skipped does not count.** Changing only a test, or a cache file, leaves the digest unchanged. Empty folders do not count either. - **The digest is not a git commit.** It is computed from the files, so a new commit with identical files gives the same digest. The real git commit of the submission is stored separately as the version's source commit. - **Symbolic links are an error** (`symbolic links are not allowed: `), and so is a tree above 500 files or 20 MB (`workflow too large ( files, bytes)`). The digest also names the stored copy: the snapshot directory is the digest itself, so two versions with identical files share one copy. ## Resubmitting a version When the submission passes validation and reaches publishing, the platform looks for a published version with the same `name` and `version`: | Situation | Result | |---|---| | No such version | A new version is stored and becomes the latest. | | Same version, **same digest** | Accepted and nothing new is created. The existing version is untouched, apart from filling in two stored fields that an earlier publish could not set (its hardware cases, if they were empty, and its storage bundle). It does **not** become the latest version if a later one exists. | | Same version, **different digest** | Refused right after the static checks, before the sandbox gates: `Version 0.1.0 of depot-picker already exists with different content; bump the version in qsolve.yaml and resubmit.` (with your workflow's name and version). The registry repeats the check at publish as a safeguard. | The conflict check runs after fetch and lint and before the expensive sandbox gates, so a content change under an old version number is caught in seconds; see [Validation and publishing](/docs/build/validation-and-publishing). An identical resubmission still runs the gates, is adopted as it is, does not change the latest version (the report says "the latest version is unchanged", including when it is an older version) and does not record the benchmark history a second time. Name rules at the same stage: - A `name` that belongs to another developer is refused: `The name belongs to another developer. Choose a different name in qsolve.yaml.` - A `name` you deleted stays reserved: ` was deleted and its name stays reserved. Choose a different name in qsolve.yaml.` - A new `name` creates a new workflow. Its display title is made from the name (`max-cut` becomes "Max Cut") when the workflow is first created, and later versions do not change it. ## What is stored per version | Stored | What it is | |---|---| | The version string | `1.0.2`, unique within the workflow. | | The content digest | The 40-hex digest above. Stored in the version's `commit_sha` field, which is the digest, not a git commit. | | The source commit | The real git commit the submission came from, when it came from a repository. | | The files | A read-only copy of the snapshot, named by the digest, which runs mount into the sandbox. When storage is configured a bundle of it is also kept, with its SHA-256. | | The manifest | The full `qsolve.yaml` as validated, including the defaults you did not write. Runs of this version are checked against this copy, not against the repository. | | The schemas | The input schema, with the order of each object's properties recorded as `x-order` so the form shows fields in your order, and the output schema. | | The examples | The contents of every file listed in `inputs.examples`. | | The hardware cases | The `hardware_cases` of `benchmarks/cases.yaml` with their inputs written out, at most 600 KB in total. | | Status and time | `published`, with the publish time. Versions are published only after every gate passes. | Each new version also refreshes the workflow's own `summary`, `family` and `tags` from its manifest, points the workflow's latest version at the new one, and adds an audit-log entry recording the version, digest and whether it is first-party. The workflow's visibility is not touched. ## Publishing an update 1. Change the code or files. 2. Raise `version` in `qsolve.yaml`. 3. Run `qsolve validate . && qsolve lint . && qsolve test .`. 4. Push to GitHub. 5. Submit the same repository address again, with the new branch, tag or commit if you use one. The new version goes through every stage in [Validation and publishing](/docs/build/validation-and-publishing) independently, and its benchmark history starts afresh. The `name` identifies the workflow; changing it publishes a different workflow. Publish versions in increasing order. The most recently published version becomes the latest whatever its number, so publishing `1.0.1` after `1.1.0` makes `1.0.1` the latest. A new version of an approved workflow keeps its visibility and does not wait for approval again; only the first version of a new workflow is subject to the approval setting. ## Which version runs A run in the web app uses the latest version. Older versions stay published, and the track record shown for a workflow is that of its latest version. A run created through the API may name an older version with `version` (`MAJOR.MINOR.PATCH`); without it, the latest is used. An unknown name or version answers `404 workflow_not_found`. ## What happens to old results Every run records the workflow version, SDK version, input hash, parameters, seed and result hash, and belongs to the version it ran on: - A new version never rewrites old results. A result produced by `1.0.0` stays a `1.0.0` result, with the output shape that version defined. - A new version has its own benchmark history, so researchers comparing versions compare their records. - Because versions are immutable, a run is reproducible: the same version, input, parameters and seed give the same result hash on a simulator. If you change your output schema in a way that breaks readers of old results, raise the major version so the break is visible. --- # The seven example workflows > A walkthrough of the seven published Qsolve example workflows: what each solves, its inputs, outputs and parameters, and what to learn from reading its code. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/examples · Updated: 2026-11-12 The `examples` repository holds seven complete, self-contained workflows. Copy the closest one and change it. Start with `max-cut`, then `generic-qubo`. | Workflow | Family | Solves | Envelope | |---|---|---|---| | `max-cut` | `qubo_optimization` | Split a weighted graph into two groups | 24 nodes | | `generic-qubo` | `qubo_optimization` | Minimise any QUBO, optionally with exactly k ones | 24 variables | | `portfolio-optimization` | `qubo_optimization` | Pick k assets, trading return against risk | 24 assets | | `molecular-ground-state` | `electronic_structure` | Ground-state energy of a closed-shell molecule or ion | 12 qubits | | `interaction-energy` | `electronic_structure` | A signed sum of system energies: binding, solvation, reaction | 6 systems of 12 qubits | | `electrolyte-solvent-screen` | `electronic_structure` | Rank battery-electrolyte solvents by Li+ binding | 8 solvents | | `quantum-kernel-classifier` | `quantum_ml` | Binary classification with a quantum-kernel SVM | 8 features, 60 training points | Every folder holds `README.md`, `qsolve.yaml`, `src/workflow.py` with the algorithm in small modules beside it, `benchmarks/cases.yaml` and `tests/`. Run any of them with `qsolve test ` or `qsolve run -i /examples/.json`. The chemistry workflows need PySCF (Linux, macOS or WSL). ## max-cut Splits a weighted graph into two groups so the edges between the groups weigh as much as possible. - **Input:** `nodes`, a list of names, and `edges` as `[u, v]` or `[u, v, weight]`. Duplicate edges and self-loops are rejected in `__init__`. - **Output:** the two groups, the cut weight and the total edge weight. - **Parameters:** `layers`, `shots`, `train_shots`, `optimizer_iters`, `train_on` (`sim` or `backend`). - **Methods:** QAOA (quantum); exhaustive search, switching to annealing above 22 variables (reference); simulated annealing (classical). - **Cases:** six, including a complete graph where low-depth QAOA struggles and a `fake:fez@1.6` case with `min_fidelity: 0.97`. An edge of weight w is cut when `x_i + x_j - 2 x_i x_j = 1`, so the workflow builds a QUBO and minimises it; `evaluate` returns the negative cut weight and `answer` turns it back. Angles are trained noise-free on a statevector, and only the final circuit goes to the target. Learn from it: `estimate()` built from a plan of circuits and shots, so the quote matches the run; the quantum method capping itself at two layers on noise-model and hardware targets; one `ctx.report_mitigation` call declaring readout correction. ## generic-qubo Minimises `x'Qx + offset` over binary variables for any square matrix, with an optional "exactly k ones" constraint. - **Input:** `Q` (up to 24 by 24), optional `labels`, `offset` and `cardinality`. - **Output:** the assignment, the variables set to 1 and the objective value. - **Parameters:** the same five as `max-cut`. - **Cases:** six, with and without the cardinality constraint, up to 18 variables. With `cardinality`, the circuit starts in the uniform superposition of every state with k ones (a Dicke state) and mixes with an XY ring, so every sample is valid and no penalty term is needed. Learn from it: optional schema fields, with `x-ui: {"advanced": true}` on `offset`, and a constraint handled in the circuit. ## portfolio-optimization Chooses exactly k assets from up to 24, trading expected return against risk: `risk_aversion * x'Cx - mu'x` with exactly k ones. - **Input:** `assets`, `num_to_select`, `risk_aversion` and either `expected_returns` with `covariance`, or `returns_history`. The bundled real-data example holds statistics of 12 US industry portfolios from 2015 to 2024. - **Output:** selected assets, equal weights, expected return, risk and whether the constraint holds. - **Parameters:** `layers`, `shots`, `train_shots`, `optimizer_iters`, `constraint_mode` (`mixer` or `penalty`), `penalty_scale`, `init` (`dicke` or `product`), `train_on`. - **Cases:** nine, including real data, penalty mode and a noise-model case on 5 assets. Learn from it: a top-level `anyOf` in the input schema for two ways to describe one problem; `x-ui.format: percent` in the output schema; enum parameters that switch the method (`mixer` keeps every sample valid, `penalty` post-selects); and tolerances that grow honestly with problem size, `max_gap: 0.0` at 10 assets and 0.10 at 14. ## molecular-ground-state Ground-state energy of a closed-shell molecule or ion with VQE in a chosen active space, reported beside the exact active-space energy, Hartree-Fock, MP2 and full-space CCSD(T). - **Input:** a built-in `name` (H2, LiH, HF, N2, CO, BeH2, H2O, NH3, CH4, Li+, Na+), or `xyz` text or `atoms`; `charge`; `basis`; and an optional `active_space` of up to 6 orbitals. - **Output:** the energy in Hartree, kcal/mol, kJ/mol and eV with its error bar, the error against the exact reference, the share of correlation energy recovered, and whether chemical accuracy (1.6 mHa) is met. - **Parameters:** `ansatz` (`uccsd` or `hea`), `shots`, `optimizer_iters`. - **Methods:** VQE (quantum); exact active-space solution, CASCI (reference); Hartree-Fock, MP2 and CCSD(T) (classical). - **Metric:** `relative_error` with `tolerance_abs: 0.0016`. - **Cases:** five, including a custom NH3 geometry and a noise-model case with `min_fidelity: 0.99`. The trial state is trained noise-free, then the energy is measured in groups of commuting Pauli terms, one circuit per group, in one batch. On noise-model and hardware targets the workflow uses a compact circuit, mitigates the counts with readout correction, electron-count post-selection and zero-noise extrapolation, and reports them with `report_mitigation`. Learn from it: `Outcome(solution=None, ...)` for measured and library energies, labelled reported; `MethodUnavailable` when CCSD(T) was not computed; a tolerance in the objective's own units; one batched `ctx.sampler.run` for many measurement circuits. ## interaction-energy A binding, solvation, adsorption or reaction energy as a signed sum of system energies, each from VQE. - **Input:** `systems`, named species each given as a built-in `name` or `xyz` with a `charge`; `basis`; an `active_space`; and optional `terms`, a coefficient per system. Without `terms` the first system counts `+1` and the others `-1`, a binding energy. - **Output:** the combined energy with error bars, per-system energies and the Hartree-Fock, exact and CCSD(T) references. - **Parameters:** `ansatz`, `shots`, `optimizer_iters`. - **Cases:** four, including Li+ binding to water and to ammonia, and the energy cost of stretching H2. Error bars of the combined energy add in quadrature, so a small interaction energy needs more shots than a total energy. Learn from it: one workflow serving many chemistries through its input; mitigation reported once per measured circuit, with a `steps` ladder only when a single system is computed. ## electrolyte-solvent-screen Ranks candidate battery-electrolyte solvents by how strongly each binds a Li+ ion, with each binding energy computed by VQE and checked against exact and MP2 references. - **Input:** `solvents` from seven built-in molecules (EC, FEC, DMC, DME, THF, ACN, water), up to four `custom_solvents` with your own geometries of the molecule and its Li+ complex, `basis` and `active_space`. - **Output:** a ranking with each binding energy split into mean-field and correlation parts, the strongest and weakest binder, whether the VQE ranking matches the exact one, and any neighbouring pair whose order is inside the error bars. - **Parameters:** `shots`, `optimizer_iters`, `ansatz`. - **Resources:** a 7200 second limit and 4096 MB, because it builds seven-system problems at 6-31G*. - **Cases:** four, with tolerances of about three times the shot noise of the combined objective. Learn from it: curated data shipped inside the repository (`src/geometries.json`), since runs have no network; an active space centred on the binding site and chosen identically in the complex and the free solvent, so no fake binding energy appears; results that say "unresolved" instead of ranking inside the noise. ## quantum-kernel-classifier Binary classification with a quantum-kernel support vector machine, tuned by cross-validation and compared on a held-out test set against a tuned RBF-SVM, logistic regression, a random forest and gradient boosting. - **Input:** a built-in `dataset` (`breast_cancer`, or synthetic moons, circles, xor and blobs), your own `data` table, or explicit `train` and `test` tables; `test_fraction`. - **Output:** test accuracy with a 95% Wilson interval, the best classical accuracy, a paired McNemar test against the RBF-SVM, the confusion matrix, predictions and a check of the measured kernel against the exact one. - **Parameters:** `reps`, `bandwidth` (both 0 for automatic tuning), `shots`, `kernel_mode` (`sampled` or `exact`). - **Metric:** `error_rate`. The objective is the test error rate. - **Cases:** five, with test sets of 200 or more points. Every kernel entry is one circuit, and all circuits of a Gram matrix go through `ctx.sampler` in one call. Each method's solution is its list of predictions and `evaluate` scores them against the true labels, so no method can claim a better error rate than its predictions earn. Learn from it: the `error_rate` metric, which needs `n_test` in the quantum outcome's `details`; mitigation by readout correction and a depolarizing rescale fitted on mirror circuits, reported for one representative circuit, with whole-matrix quality in the answer's `kernel_check`; `kernel_mode: exact`, which still sends eight spot-check circuits because the quantum method must use the backend. See [Your first workflow](/docs/build/your-first-workflow) and [Writing the workflow](/docs/build/writing-the-workflow). --- # Troubleshooting > Real errors from qsolve lint, validation gates, the SDK and failed runs, with the cause of each and how to fix it, listed by the code or message you see. Section: Build a workflow · Source: https://qsolve.omlabs.co/docs/build/troubleshooting · Updated: 2026-11-13 Find the message or code you see below. Lint codes come from `qsolve lint` and the submission's lint stage, gate messages from `qsolve test` and the gates stage, and error codes from a failed run. Messages are quoted as Qsolve prints them. ## Lint codes The complete reference, with exact triggers, is [Lint codes](/docs/build/lint-codes). Errors (`E`) fail lint and reject a submission. Warnings (`W`) do not. The short list: | Code | Message | Fix | |---|---|---| | `E001` | qsolve.yaml not found at the repository root | Put `qsolve.yaml` at the root of the repository, or enter its folder in the **Folder** field. | | `E002` | invalid workflow definition: ... | The manifest or a schema file is invalid. `qsolve validate .` shows the field and reason. | | `E010` | symbolic links are not allowed | Replace the link with a real file. | | `E011` | too many files (n > 500), or repository too large | Stay under 500 files and 20 MB. Remove data you do not need. | | `E012` | file looks like a credential; remove it and rotate the secret | Delete `.env`, `*.pem`, `*.key`, `id_rsa*`, `credentials*.json` or `.netrc` and revoke the credential. | | `E013` | possible private key, cloud or GitHub token, payment-style secret or quantum-provider token committed | Remove it and rotate the secret. A workflow never needs credentials. | | `E020` | invalid JSON Schema: ... | Fix the schema. It must be valid draft-07. | | `E021` | `pattern` or `patternProperties` is not allowed; only local `#/...` `$ref` values are allowed; format `regex` is not allowed | Use `enum`, `minLength` or `maxLength`; point `$ref` at `#/definitions/...`. | | `E022` | schema is too large | Keep each schema under 256 KB. | | `E023` | example input unreadable, or does not match the input schema | Fix the example or the schema. `qsolve validate .` shows the failing path. | | `E024` | at least one example input is required | List a file under `inputs.examples`. | | `E030` | module not found in the repository, or the class is not defined in it | Check `entrypoint`. `src.workflow:MaxCut` needs `src/workflow.py` defining `class MaxCut`. | | `E031` | syntax error, or cannot parse Python | Fix the syntax error at the line shown. | | `E040` | imports 'x', which is not available in the runtime | Import only the standard library and `qsolve`, NumPy, SciPy, scikit-learn, Qiskit, Qiskit Aer, PySCF, pydantic, jsonschema, PyYAML, typer and rich. | | `E050` | benchmarks/cases.yaml is required | Add the file; see [Benchmarks](/docs/build/benchmarks). | | `E051` | a license identifier is required | Set `license` in the manifest. | | `E052` | hardware_cases are invalid, or a hardware case has no `ibm:` target | Fix the entry; see [Hardware cases](/docs/build/benchmarks). | | `E053` | more than three hardware_cases | Keep at most three. | | `E054` | hardware_cases need `qpu: [ibm]` under targets | Add `ibm` to `targets.qpu` in the manifest. | | `W041` | imports `subprocess`, `socket`, `ctypes`, `pickle`, `marshal`, `shelve` or `multiprocessing`, or calls `eval`, `exec`, `compile` or `__import__` | Blocked or useless at run time, and flagged for review. Remove them. | | `W042` | imports 'x', pinned in requirements.lock but not available | The run rejects the import. Remove the dependency; see [Project layout](/docs/build/project-layout). | | `W043` | imports a module by name at run time | Not checked by lint. Prefer ordinary imports. | | `W051` | no LICENSE file found | Add `LICENSE`. | | `W060` | no README.md | Add one saying what the workflow solves and its limits. | ## Manifest errors `qsolve validate` prints `Invalid workflow:` followed by the field and the reason. | Message | Cause | |---|---| | `No qsolve.yaml in ` | Wrong directory. | | `Field required` (with a field name) | A required field is missing. | | `Extra inputs are not permitted` (with a field name) | An unknown or misspelled field. | | `Input should be a valid string` on `version` | YAML read `1.0` as a number. Write `1.0.0`. | | `name must be lowercase letters, digits and dashes (2-63 chars)` | Fix the name. | | `version must be semver, e.g. 0.1.0` | Use three numeric parts. | | `entrypoint must look like 'package.module:ClassName'` | Fix the entrypoint. | | `declare exactly one method with role 'quantum'` (or `'reference'`) | Declare exactly one of each. | | `method ids must be unique` | Rename a method. | | `List should have at least 2 items after validation` on `methods` | Declare a quantum method and a reference. | | `parameter names must be unique` | Rename a parameter. | | `parameter 'layers' must be <= 6.0` (at load time) | The `default` breaks the parameter's own bounds. | | `schema path escapes workflow directory: ...` | A schema path points outside the repository. | ## Input and run errors These appear locally from `qsolve run` and `qsolve estimate` as `Error: `, and in a failed run on the platform. | Message | Cause and fix | |---|---| | `Invalid input at : ` | The input does not match `schemas/input.json`. Fix it at that path. | | `unknown parameters: [...]` | An override names a parameter the manifest does not declare. | | `parameter 'x' must be ...` | A value breaks its declared type, range or choices. See [Parameters](/docs/build/parameters). | | `Problem has N variables; this workflow supports at most M (notes)` | `size()` is above the `envelope`. Reduce the problem, or raise the envelope if your code handles it. | | `Cannot load the workflow 'x': ...` | The entrypoint cannot be imported. Look for an import error in your module. | | `'x' must be a subclass of qsolve.Workflow` | The entrypoint class does not inherit from `Workflow`. | | `Could not set up the problem from this input: ...` | A `ValueError`, `KeyError`, `TypeError` or `IndexError` escaped `__init__`. Validate the input there and raise `WorkflowError` with a clear message. | | `method 'x' is declared in the manifest but the workflow has no 'classical_x' method` | Add the method. Dashes in the id become underscores. | | `the quantum method or the reference produced no result: ...` | One was unavailable or returned an invalid value. The reason follows. | | `answer() failed on the quantum result: ...` | Your `answer()` raised. | | `Workflow produced an invalid answer: ...` | The answer does not match `schemas/output.json`. | | `evaluate() failed on a method's solution: ...` | Your `evaluate` raised something other than `InvalidSolution`. | | `a classical method tried to run a circuit: only the quantum method may use the backend` | `reference` or `classical_*` used `ctx.backend`, `ctx.sampler` or `ctx.estimator`. | | `report_mitigation cites circuits [...], but N were sent so far` | A dispatch index does not exist. Take it from `metadata["qsolve"][k]["dispatch"]`. | | `report_mitigation: the distribution must hold finite, non-negative weights` | Clean the distribution. | | `report_mitigation: kept is a share of shots between 0 and 1` | Fix `kept`. | A method shown as unavailable on a result carries one of these reasons: `failed: : ` for a classical method that crashed; the text of your `MethodUnavailable` or `InvalidSolution`; "it reported a value that is not a finite number"; "it reported an error bar that is not a finite, non-negative number"; "evaluate() returned a value that is not a finite number"; "it sent circuits to the quantum backend, which only the quantum method may do". ## Validation gate messages | Gate | Message | Cause and fix | |---|---|---| | `example_inputs` | `: Invalid input at ...` or `0 valid` | An example fails the schema, or none is listed. | | `benchmark_cases` | `0 cases defined` | Add cases to `benchmarks/cases.yaml`. | | `methods_declared` | `no methods declared: add methods: ...` | Declare a quantum method and a reference. | | `fidelity_checked` | `no case sets expect.min_fidelity on a noisy target (target: fake:fez@1.6)` | Add a `fake:` case with `min_fidelity`. | | `cases_pass` | `n/m passed`, with `FAIL ...` per case | See the case messages below. | | `reproducible` | `result differs under same seed at result.` | Something is not seeded. Use `ctx.seed` for every random generator and avoid hash- or set-dependent iteration order. | | `reproducible` | `the rerun with the same seed failed: ...` | The same input failed the second time. | | `estimate_honest` | `estimated N circuits, the run sent M` (or `shots`) | Your `estimate()` is off by more than a factor of two. Count what each parameter makes the run send. | | `estimate_honest` | `estimate() failed: ...` | Your `estimate()` raised. | Case messages, shown beside `FAIL`: | Message | Meaning | |---|---| | `: ...` | The case raised. | | `no headline metric: quantum and reference could not be compared` | The quantum method or the reference had no value. | | `method x failed: ...` | A method crashed or was unavailable. | | `method x claimed A, its solution gives B` | The objective you reported differs from `evaluate(solution)`. Fix the method or `evaluate`. | | `quantum method x sent no circuit to the backend` | The quantum method must send circuits through `ctx`. | | `no mitigation numbers reported` | A case set `min_fidelity` or `min_postselection`, but the run has no scorable circuits or no report. | | `only N% of the quantum method's shots could be scored against an ideal output` | Circuits over 20 qubits or with mid-circuit measurement cannot be scored. Fidelity bounds need 90% coverage. | | `fidelity F < B` | The mitigated fidelity is below `min_fidelity`. Improve mitigation or lower the bound to what the circuit reaches. | | `post-selection P < B` | The share of shots kept is below `min_postselection`. | | `N two-qubit gates > M` | A circuit exceeds `max_two_qubit_gates`. Make it shallower. | | `no circuit size reported` | `max_two_qubit_gates` was set but no circuit was recorded. | A case that misses `max_gap` or `max_abs_error` fails with no message; the table shows its gap. The case rules are in [Benchmarks](/docs/build/benchmarks). ## Submission and run errors | Where | Message or code | Meaning | |---|---|---| | Submission | `Only https://github.com// repositories are supported.` | Use an HTTPS GitHub address of the form shown. | | Submission | `No qsolve.yaml in the folder '' of this repository at that commit.` | The folder is wrong or does not exist on that branch. Check the spelling, or leave the field empty if `qsolve.yaml` is at the root. | | Submission | `Could not resolve '' in this repository` | The branch or tag does not exist, or the repository is private and not connected. See [Private repositories](/docs/build/private-repositories). | | Submission | `The repository contains symbolic links, which are not allowed.` or `...submodules...` | Remove them. | | Submission | `Fetching the repository timed out.` or `repository exceeds 50 MB` | Reduce the repository. | | Submission | `Version X of already exists with different content; bump the version ...` | See [Versioning](/docs/build/versioning). | | Submission | `Case 'x': input_file ...` or `Case 'x': input does not match the input schema ...` | Fix the case named in the message; see [Benchmarks](/docs/build/benchmarks). | | Submission | `Parameter 'shots' has max N, above the platform cap of 100,000 ...` | Lower the `shots` default or max. | | Run | `shots_too_high` | The run's `shots` is above 100,000 per circuit. | | Submission | `the benchmark run timed out` or `ran out of memory` | Speed up the cases or raise `resources` in the manifest, within the validation caps. | | Run | `run_too_long` | The predicted `runtime_s` exceeds `resources.timeout_s`. | | Run | `timeout` | The run exceeded `resources.timeout_s`. | | Run | `out_of_memory` | The run exceeded `resources.memory_mb`. | | Run | `workflow_crashed` | Your code raised an unexpected exception. The log tail is kept with the run. | | Run | `budget_exhausted` | The run needed more credits than were set aside for it, so it was stopped. Your `estimate()` was too low. | | Run | `target_not_supported` | The run asked for a target the manifest's `targets` does not list. | ## Local environment errors | Message | Fix | |---|---| | `Install the ibm extra: pip install 'qsolve-sdk[ibm]'` | Needed for `fake:` and `ibm:` targets. | | `no fake device 'x' (e.g. fez, marrakesh, torino)` | Use a device name that `qiskit_ibm_runtime` ships a fake for. | | `noise scale must be between 1 and 10` | Use `@1` to `@10`. | | `QSOLVE_IBM_TOKEN is not set` | Set it to run on `ibm:` locally. | | `Unknown target 'x'. Use sim-cpu, sim-gpu, ibm: or fake:[@scale].` | Fix the target string. | ## Passes locally, fails on the platform The difference is almost always the environment, not the SDK. Check in order: 1. Run `qsolve lint .`. It flags imports the platform cannot satisfy; `qsolve test` does not. 2. Make sure everything your code reads is in the repository. Files in `tests/` and caches are not shipped. 3. Remove paths outside the repository and network calls. A run sees only the repository and a scratch directory, with no network. 4. Check run time. A fast machine hides slow code; validation has a limit from `resources.timeout_s`, raised to at least 300 and capped at 7200 seconds. 5. Check threading. Runs use one BLAS thread, so results that depend on summation order on a multi-core laptop can differ in the last digits. Differences beyond relative 1e-8 fail `reproducible`.