diff --git a/.github/workflows/matlab-tests.yml b/.github/workflows/matlab-tests.yml new file mode 100644 index 0000000..9214185 --- /dev/null +++ b/.github/workflows/matlab-tests.yml @@ -0,0 +1,21 @@ +name: MATLAB Tests + +on: + push: + branches: ["**"] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up MATLAB + uses: matlab-actions/setup-matlab@v2 + + - name: Run unit tests + uses: matlab-actions/run-command@v2 + with: + command: "addpath(genpath(pwd)); results = runtests_qrlab; assert(all([results.Passed] | [results.Incomplete]));" diff --git a/Coherence/FlagPoleState.m b/Coherence/FlagPoleState.m index e438fc3..8b4a617 100644 --- a/Coherence/FlagPoleState.m +++ b/Coherence/FlagPoleState.m @@ -1,10 +1,12 @@ function state = FlagPoleState(dim, p) -% Construct a flag-pole state in the computational basis. +%FLAGPOLESTATE Construct a flag-pole pure state density matrix. +% STATE = FLAGPOLESTATE(DIM, P) returns the density matrix +% |psi>=',2}, mfilename, 'dim', 1); +validateattributes(p, {'numeric'}, {'scalar','real','>=',0,'<=',1}, mfilename, 'p', 2); -state = ket.'*ket; \ No newline at end of file +ampTail = sqrt((1 - p) / (dim - 1)); +ket = [sqrt(p); ampTail * ones(dim - 1, 1)]; +state = ket * ket.'; +end diff --git a/Entanglement/compute_entanglement_measures.m b/Entanglement/compute_entanglement_measures.m index a48592e..f4cb12e 100644 --- a/Entanglement/compute_entanglement_measures.m +++ b/Entanglement/compute_entanglement_measures.m @@ -1,8 +1,10 @@ function result = compute_entanglement_measures(rho_in, opts) -% COMPUTE_ENTANGLEMENT_MEASURES -% Dispatcher for common entanglement measures. Supports selective -% evaluation, custom extensions, basic preprocessing (Hermitization and -% trace normalization), and optional tabular output. +%COMPUTE_ENTANGLEMENT_MEASURES Evaluate selected entanglement measures. +% RESULT = COMPUTE_ENTANGLEMENT_MEASURES(RHO_IN) evaluates default +% measures for a bipartite state matrix. +% +% RESULT = COMPUTE_ENTANGLEMENT_MEASURES(RHO_IN, OPTS) allows control of +% dimensions, selected measures, preprocessing behavior, and output type. % % Inputs: % rho_in : Density matrix (state) or channel Choi matrix. @@ -19,72 +21,111 @@ % result : struct with fields per measure (value, elapsed) plus a meta field, % or a table with columns Measure, Value, Elapsed when opts.return_table is true. -% ---------- Option handling ---------- -if ~exist('opts','var') || isempty(opts) +validateattributes(rho_in, {'numeric'}, {'2d','nonempty'}, mfilename, 'rho_in', 1); +if size(rho_in, 1) ~= size(rho_in, 2) + error('QRLab:Entanglement:NonSquareInput', 'rho_in must be a square matrix.'); +end + +if nargin < 2 || isempty(opts) opts = struct(); +elseif ~isstruct(opts) + error('QRLab:Entanglement:InvalidOptions', 'opts must be a struct when provided.'); +end + +% ---------- Option handling ---------- +if ~isfield(opts, 'dims'); opts.dims = []; end +if ~isfield(opts, 'measures'); opts.measures = {'LogNeg','RainsBound','MaxRainsEntropy','TemperedLogNeg'}; end +if ~isfield(opts, 'custom_measures'); opts.custom_measures = struct(); end +if ~isfield(opts, 'return_table'); opts.return_table = false; end +if ~isfield(opts, 'normalize_trace'); opts.normalize_trace = true; end + +if ~iscellstr(opts.measures) && ~(iscell(opts.measures) && all(cellfun(@(x) isstring(x) || ischar(x), opts.measures))) + error('QRLab:Entanglement:InvalidMeasureList', 'opts.measures must be a cell array of measure names.'); +end +if ~islogical(opts.return_table) || ~isscalar(opts.return_table) + error('QRLab:Entanglement:InvalidReturnTableOption', 'opts.return_table must be a scalar logical.'); +end +if ~islogical(opts.normalize_trace) || ~isscalar(opts.normalize_trace) + error('QRLab:Entanglement:InvalidNormalizeOption', 'opts.normalize_trace must be a scalar logical.'); +end +if ~isstruct(opts.custom_measures) + error('QRLab:Entanglement:InvalidCustomMeasures', 'opts.custom_measures must be a struct of function handles.'); +end + +if ~isempty(opts.dims) + validateattributes(opts.dims, {'numeric'}, {'vector','numel',2,'integer','positive'}, mfilename, 'opts.dims'); end -if ~isfield(opts,'dims'); opts.dims = []; end -if ~isfield(opts,'measures'); opts.measures = {'LogNeg','RainsBound','MaxRainsEntropy','TemperedLogNeg'}; end -if ~isfield(opts,'custom_measures'); opts.custom_measures = struct(); end -if ~isfield(opts,'return_table'); opts.return_table = false; end -if ~isfield(opts,'normalize_trace'); opts.normalize_trace = true; end t_start = tic; % ---------- preprocess ---------- rho = rho_in; if opts.normalize_trace - rho = (rho + rho')/2; % Hermitian + rho = (rho + rho') / 2; tr_val = trace(rho); - if abs(tr_val) > eps - rho = rho / tr_val; % Normalize + if abs(tr_val) <= eps + warning('QRLab:Entanglement:NearZeroTrace', ... + 'Trace is near zero during normalization; matrix is left unscaled.'); + else + rho = rho / tr_val; end end % ---------- Dimension inference ---------- if isempty(opts.dims) - d = sqrt(max(size(rho))); + d = sqrt(size(rho, 1)); if abs(d - round(d)) > 1e-10 - error('Automatic dimension inference failed: matrix size is not a perfect square. Specify opts.dims manually.'); + error('QRLab:Entanglement:DimensionInferenceFailed', ... + 'Automatic dimension inference failed: matrix size is not a perfect square. Specify opts.dims manually.'); end - dims = round(d) * [1, 1]; + dims = [round(d), round(d)]; else dims = opts.dims; end -if isempty(dims) - error('System dimensions are undefined. Please set opts.dims.'); + +if prod(dims) ~= size(rho, 1) + error('QRLab:Entanglement:DimensionMismatch', ... + 'prod(opts.dims) must match matrix dimension. Received dims [%d %d] for %d-by-%d matrix.', ... + dims(1), dims(2), size(rho, 1), size(rho, 2)); end % ---------- Built-in measure map ---------- -available.LogNeg = @(rho,d) LogNeg(rho, d); -available.RainsBound = @(rho,d) RainsBound(rho, d); -available.MaxRainsEntropy = @(rho,d) MaxRains(rho, d); -available.TemperedLogNeg = @(rho,d) TempLogNeg(rho, d); +available.LogNeg = @(state, d) LogNeg(state, d); +available.RainsBound = @(state, d) RainsBound(state, d); +available.MaxRainsEntropy = @(state, d) MaxRains(state, d); +available.TemperedLogNeg = @(state, d) TempLogNeg(state, d); % ---------- Merge custom measures ---------- measure_map = available; user_fields = fieldnames(opts.custom_measures); for k = 1:numel(user_fields) name = user_fields{k}; - measure_map.(name) = opts.custom_measures.(name); + fn = opts.custom_measures.(name); + if ~isa(fn, 'function_handle') + error('QRLab:Entanglement:InvalidCustomMeasure', ... + 'Custom measure "%s" must be a function handle f(rho, dims).', name); + end + measure_map.(name) = fn; end % ---------- Evaluation ---------- -measures_requested = opts.measures; +measures_requested = cellfun(@char, opts.measures, 'UniformOutput', false); result_struct = struct(); for idx = 1:numel(measures_requested) name = measures_requested{idx}; if ~isfield(measure_map, name) - warning('Measure %s is not registered and will be skipped.', name); + warning('QRLab:Entanglement:UnknownMeasure', ... + 'Measure "%s" is not registered and will be skipped.', name); continue; end + fn = measure_map.(name); t_local = tic; value = fn(rho, dims); result_struct.(name) = struct('value', value, 'elapsed', toc(t_local)); end -result_struct.meta = struct(... +result_struct.meta = struct( ... 'dims', dims, ... 'trace', trace(rho), ... 'elapsed_total', toc(t_start)); @@ -92,17 +133,17 @@ % ---------- Output ---------- if opts.return_table measure_names = fieldnames(result_struct); - measure_names = measure_names(~strcmp(measure_names,'meta')); - values = zeros(numel(measure_names), 1); + measure_names = measure_names(~strcmp(measure_names, 'meta')); + values = zeros(numel(measure_names), 1); elapsed = zeros(numel(measure_names), 1); for k = 1:numel(measure_names) m = measure_names{k}; - values(k) = result_struct.(m).value; + values(k) = result_struct.(m).value; elapsed(k) = result_struct.(m).elapsed; end result = table(measure_names, values, elapsed, ... - 'VariableNames', {'Measure','Value','Elapsed'}); + 'VariableNames', {'Measure', 'Value', 'Elapsed'}); else result = result_struct; end -end \ No newline at end of file +end diff --git a/Supermap/LinkProd.m b/Supermap/LinkProd.m index 48fbd2f..f2e4da4 100644 --- a/Supermap/LinkProd.m +++ b/Supermap/LinkProd.m @@ -28,16 +28,36 @@ % % Link product of two Choi matrices JA and JB: % Jout = LinkProd(JA, JB, [Ain, Aout, Bin, Bout]); + validateattributes(JA, {'numeric'}, {'2d','nonempty'}, mfilename, 'JA', 1); + validateattributes(JB, {'numeric'}, {'2d','nonempty'}, mfilename, 'JB', 2); + validateattributes(DIM, {'numeric'}, {'vector','numel',4,'integer','positive'}, mfilename, 'DIM', 3); - -% link Aout and Bin -Ain = DIM(1); -Aout = DIM(2); -Bin = DIM(3); -Bout = DIM(4); + Ain = DIM(1); + Aout = DIM(2); + Bin = DIM(3); + Bout = DIM(4); -assert(Aout == Bin, 'Output dimension of channel A does not match with the input dimension of channel B'); + expectedJA = Ain * Aout; + expectedJB = Bin * Bout; -Link = kron(JA, eye(Bout)) * kron(eye(Ain), PartialTranspose(JB,1,[Bin, Bout])); -Jout = PartialTrace(Link, 2, [Ain, Aout, Bout]); -end \ No newline at end of file + if ~isequal(size(JA), [expectedJA, expectedJA]) + error('QRLab:Supermap:InvalidJA', ... + 'JA must be a square matrix of size %d-by-%d for DIM=[%d %d %d %d].', ... + expectedJA, expectedJA, Ain, Aout, Bin, Bout); + end + + if ~isequal(size(JB), [expectedJB, expectedJB]) + error('QRLab:Supermap:InvalidJB', ... + 'JB must be a square matrix of size %d-by-%d for DIM=[%d %d %d %d].', ... + expectedJB, expectedJB, Ain, Aout, Bin, Bout); + end + + if Aout ~= Bin + error('QRLab:Supermap:DimensionMismatch', ... + 'Output dimension of channel A (%d) must match input dimension of channel B (%d).', Aout, Bin); + end + + % Link A_out and B_in. + link = kron(JA, eye(Bout)) * kron(eye(Ain), PartialTranspose(JB, 1, [Bin, Bout])); + Jout = PartialTrace(link, 2, [Ain, Aout, Bout]); +end diff --git a/docs/SoftwareDescription.md b/docs/SoftwareDescription.md new file mode 100644 index 0000000..3a2c071 --- /dev/null +++ b/docs/SoftwareDescription.md @@ -0,0 +1,45 @@ +# QRLab Software Description + +## Product Summary +QRLab is a MATLAB toolbox for research and education in quantum information processing and quantum resource theory. The package provides optimization-based and analytical routines for evaluating resource measures and simulating resource-aware transformations. + +## Functional Modules + +### Entanglement Module +- Static entanglement quantifiers (for example logarithmic negativity and Rains-type bounds). +- Dynamic/channel-oriented measures and capacity-related routines. +- Dispatcher utility (`compute_entanglement_measures`) for multi-measure batch evaluation. + +### Coherence Module +- Coherence robustness computations. +- Coherence-relevant state constructors and simulation helpers. + +### Magic Module +- Qubit and qudit magic resource measures. +- Representative state constructors and support data for stabilizer-related workflows. + +### Quasi-Theory Module +- Probabilistic and observable-dependent error cancellation. +- Decomposition and virtual recovery routines for mitigation pipelines. + +### Supermap Module +- Quantum switch implementations (Kraus and Choi representations). +- Link product construction for channel composition in Choi form. + +### Utilities +- Foundational linear-algebra primitives and helper routines used across modules. + +## Technical Characteristics +- **Language**: MATLAB +- **External dependencies**: QETLAB 0.9, CVX 2.1 (module-dependent) +- **Design style**: functional MATLAB scripts/functions organized by resource-theory domain + +## Intended Use +- Algorithm prototyping in quantum information science. +- Reproducible numerical experiments in resource theory. +- Classroom/lab demonstrations of convex optimization and channel/state measures. + +## Reliability and Maintainability Measures +- Input validation and standardized diagnostics in core utility paths. +- Reproducible examples under `examples/`. +- Automated smoke tests with `matlab.unittest` in `tests/` and unified runner `runtests_qrlab.m`. diff --git a/docs/UserGuide.md b/docs/UserGuide.md new file mode 100644 index 0000000..f8b5ced --- /dev/null +++ b/docs/UserGuide.md @@ -0,0 +1,55 @@ +# QRLab User Guide + +## Overview +QRLab is a MATLAB toolbox for quantum resource theory workflows, including entanglement, coherence, magic, supermaps, and quasi-probability modules. + +## Prerequisites +- MATLAB desktop +- QETLAB 0.9 on MATLAB path +- CVX 2.1 initialized with `cvx_setup` + +## Getting Started +1. Add dependencies and QRLab to your path: + ```matlab + addpath(genpath('path/to/QETLAB-0.9')); + addpath(genpath('path/to/QRLab')); + ``` +2. Run a smoke example: + ```matlab + rho = MaxEntangled(2) * MaxEntangled(2)'; + ln = LogNeg(rho); + fprintf('Logarithmic negativity: %.6f\n', ln); + ``` + +## Core Workflows + +### 1) Evaluate multiple entanglement measures in one call +```matlab +rho = MaxEntangled(2) * MaxEntangled(2)'; +opts = struct(); +opts.dims = [2 2]; +opts.measures = {'LogNeg', 'RainsBound'}; +res = compute_entanglement_measures(rho, opts); +disp(res.LogNeg.value); +``` + +### 2) Build a computational basis operator +```matlab +M = KetBra(4, 2, 3); % |2><3| +``` + +### 3) Generate a flag-pole coherence state +```matlab +rho = FlagPoleState(5, 0.6); +trace(rho) % should be 1 +``` + +## Examples Folder +Use the scripts in `examples/` for quick demonstrations: +- `examples/example_basic_states.m` +- `examples/example_entanglement_dispatcher.m` + +## Troubleshooting +- If `PartialTrace`, `PartialTranspose`, or `MaxEntangled` are unresolved, confirm QETLAB path setup. +- If CVX-based measures fail, run `cvx_setup` and ensure CVX 2.1 is active. +- For dependency-sensitive tests, use `runtests_qrlab` to see skipped tests with reasons. diff --git a/examples/example_basic_states.m b/examples/example_basic_states.m new file mode 100644 index 0000000..4e789a1 --- /dev/null +++ b/examples/example_basic_states.m @@ -0,0 +1,13 @@ +%% QRLab Example: Basic State Utilities +% Demonstrates utility and coherence helper functions that do not require CVX. + +fprintf('--- example_basic_states ---\n'); + +% Build basis operator |2><3| in dimension 4. +M = KetBra(4, 2, 3); +fprintf('Non-zero entries in KetBra result: %d\n', nnz(M)); + +% Build a flag-pole state and check physical properties. +rho = FlagPoleState(5, 0.6); +fprintf('Trace(flag-pole state) = %.6f\n', trace(rho)); +fprintf('Hermitian check (norm(rho-rho'')) = %.3e\n', norm(rho - rho', 'fro')); diff --git a/examples/example_entanglement_dispatcher.m b/examples/example_entanglement_dispatcher.m new file mode 100644 index 0000000..7a8e84f --- /dev/null +++ b/examples/example_entanglement_dispatcher.m @@ -0,0 +1,15 @@ +%% QRLab Example: Entanglement Dispatcher with Custom Measure +% This script uses a dependency-light configuration by supplying only +% a custom measure. Built-in CVX/QETLAB measures can be enabled if available. + +fprintf('--- example_entanglement_dispatcher ---\n'); + +rho = eye(4) / 4; +opts = struct(); +opts.dims = [2, 2]; +opts.measures = {'TraceValue'}; +opts.custom_measures = struct('TraceValue', @(state, ~) real(trace(state))); +opts.normalize_trace = true; + +res = compute_entanglement_measures(rho, opts); +fprintf('Custom TraceValue measure = %.6f\n', res.TraceValue.value); diff --git a/runtests_qrlab.m b/runtests_qrlab.m new file mode 100644 index 0000000..a03ad89 --- /dev/null +++ b/runtests_qrlab.m @@ -0,0 +1,23 @@ +function results = runtests_qrlab() +%RUNTESTS_QRLAB Run the QRLab matlab.unittest suite with a clear summary. +% +% RESULTS = RUNTESTS_QRLAB() discovers tests under ./tests and prints +% pass/fail/incomplete counts and total runtime. + +suite = testsuite(fullfile(fileparts(mfilename('fullpath')), 'tests')); +results = run(suite); + +nPassed = nnz([results.Passed]); +nFailed = nnz([results.Failed]); +nIncomplete = nnz([results.Incomplete]); + +fprintf('\nQRLab test summary:\n'); +fprintf(' Passed: %d\n', nPassed); +fprintf(' Failed: %d\n', nFailed); +fprintf(' Incomplete: %d\n', nIncomplete); +fprintf(' Total tests: %d\n', numel(results)); + +if nFailed > 0 + error('QRLab:Tests:FailuresDetected', 'One or more tests failed.'); +end +end diff --git a/tests/TestCoreUtilities.m b/tests/TestCoreUtilities.m new file mode 100644 index 0000000..f481ae8 --- /dev/null +++ b/tests/TestCoreUtilities.m @@ -0,0 +1,48 @@ +classdef TestCoreUtilities < matlab.unittest.TestCase + % Smoke tests for utility and dispatcher functions. + + methods (Test) + function testKetBraSingleEntry(testCase) + M = KetBra(4, 2, 3); + testCase.verifyEqual(size(M), [4 4]); + testCase.verifyEqual(nnz(M), 1); + testCase.verifyEqual(M(2, 3), 1); + end + + function testFlagPoleStateProperties(testCase) + rho = FlagPoleState(6, 0.25); + testCase.verifySize(rho, [6 6]); + testCase.verifyLessThan(norm(rho - rho', 'fro'), 1e-12); + testCase.verifyLessThan(abs(trace(rho) - 1), 1e-12); + + eigenVals = eig((rho + rho') / 2); + testCase.verifyGreaterThanOrEqual(min(real(eigenVals)), -1e-12); + end + + function testComputeEntanglementMeasuresCustomOnly(testCase) + rho = eye(4) / 4; + opts = struct(); + opts.dims = [2, 2]; + opts.measures = {'TraceValue'}; + opts.custom_measures = struct('TraceValue', @(state, ~) real(trace(state))); + opts.normalize_trace = true; + + res = compute_entanglement_measures(rho, opts); + testCase.verifyTrue(isfield(res, 'TraceValue')); + testCase.verifyEqual(res.TraceValue.value, 1, 'AbsTol', 1e-12); + end + + function testLinkProdSkipsWithoutQETLAB(testCase) + requiredFns = {'PartialTrace', 'PartialTranspose'}; + missing = requiredFns(cellfun(@(fn) exist(fn, 'file') ~= 2, requiredFns)); + if ~isempty(missing) + testCase.assumeFail(sprintf('QETLAB dependency missing: %s', strjoin(missing, ', '))); + end + + JA = eye(4); + JB = eye(4); + J = LinkProd(JA, JB, [2 2 2 2]); + testCase.verifySize(J, [4 4]); + end + end +end diff --git a/utils/KetBra.m b/utils/KetBra.m index a77b1c8..13d2437 100644 --- a/utils/KetBra.m +++ b/utils/KetBra.m @@ -1,19 +1,20 @@ -function M = KetBra(dim,i,j) - % This function produces a matrix for |i>=',1}, mfilename, 'dim', 1); +validateattributes(i, {'numeric'}, {'scalar','integer','>=',1,'<=',dim}, mfilename, 'i', 2); +validateattributes(j, {'numeric'}, {'scalar','integer','>=',1,'<=',dim}, mfilename, 'j', 3); -ket_i(i) = 1; -bra_j(j) = 1; - -M = ket_i*bra_j; \ No newline at end of file +M = zeros(dim, dim); +M(i, j) = 1; +end