diff --git a/docs/examples_dilations.ipynb b/docs/examples_dilations.ipynb new file mode 100644 index 0000000..1a0df70 --- /dev/null +++ b/docs/examples_dilations.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Examples - non-dilatable $d$-parameter $C_{0}$-semigroups #\n", + "\n", + "This notebook provides supplementary material to the research paper <https://doi.org/10.48550/arXiv.2210.02353>.\n", + "\n", + "In §5.3 a construction is given which yields for any $d \\geq 2$\n", + "a $d$-parameter $C_{0}$-semigroup, $T$ satisfying:\n", + "\n", + "- $T$ is contractive (equivalently its marginals $T_{i}$ are for each $i$);\n", + "- the generator $A_{i}$ of $T_{i}$ has strictly negative spectral bound for each $i$;\n", + "\n", + "and such that $T$ is __not__ **completely dissipative**.\n", + "Thus by the classification Theorem (Thm 1.1), $T$ does not have a regular unitary dilation.\n", + "\n", + "This Notebook demonstrates this general result empirically." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import os;\n", + "import sys;\n", + "\n", + "# NOTE: need this to force jupyter to reload imports:\n", + "for key in list(sys.modules.keys()):\n", + " if key.startswith('src.'):\n", + " del sys.modules[key];\n", + "\n", + "os.chdir(os.path.dirname(_dh[0]));\n", + "sys.path.insert(0, os.getcwd());\n", + "\n", + "from src.examples_dilations import *;" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# User input:\n", + "N = 4; # dimension of the Hilbert space.\n", + "d = 4;\n", + "\n", + "# If you ensure that the failure of S_{T,K} >= 0 only occurs for K = {1,2,...,d}\n", + "# + you want S_TK > 0 (strictly) for all K ≠ {1,2,...,d}:\n", + "alpha = 1/math.sqrt(d - 0.5);\n", + "\n", + "# If you ensure that the failure of S_{T,K} >= 0 only occurs for K = {1,2,...,d}\n", + "# alpha = 1/math.sqrt(d - 1);\n", + "\n", + "# Otherwise:\n", + "# alpha = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "The marginal semigroups T_i and their generators A_i:\n", + "\n", + " i spec bound of A_i A_i dissipative (<==> T_i contractive)?\n", + "--- ------------------- -----------------------------------------\n", + " 1 -0.465478 True\n", + " 2 -0.465478 True\n", + " 3 -0.465478 True\n", + " 4 -0.465478 True\n" + ] + } + ], + "source": [ + "# create the generators `A_i` of the marginal semigroups `T_i`:`\n", + "A = [\n", + " generate_semigroup_generator(\n", + " shape = [N, N],\n", + " rational = True,\n", + " base = 100,\n", + " alpha = alpha,\n", + " )\n", + " for _ in range(d)\n", + "];\n", + "\n", + "data = [];\n", + "for i, A_i in enumerate(A):\n", + " omega_Re = spec_bounds((1/2)*(A_i + A_i.T.conj()));\n", + " omega = spec_bounds(A_i);\n", + " data.append((i+1, omega_Re, True if (omega_Re <= 0) else False));\n", + "\n", + "repr = tabulate(\n", + " tabular_data = data,\n", + " headers = ['i', 'spec bound of A_i', 'A_i dissipative (<==> T_i contractive)?'],\n", + " showindex = False,\n", + " floatfmt = '.6f',\n", + " colalign = ['center', 'center', 'center'],\n", + " tablefmt = 'simple',\n", + ");\n", + "print(f'\\nThe marginal semigroups T_i and their generators A_i:\\n\\n{repr}');" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Dissipation operators:\n", + "\n", + " K min σ(S_{T,K}) S_{T,K} >= 0?\n", + "------------ ---------------- ---------------\n", + " [] 1.000000 True\n", + " [3] 0.465478 True\n", + " [2] 0.465478 True\n", + " [1] 0.465478 True\n", + " [0] 0.465478 True\n", + " [2, 3] 0.212675 True\n", + " [1, 3] 0.183738 True\n", + " [1, 2] 0.217453 True\n", + " [0, 3] 0.200206 True\n", + " [0, 2] 0.301332 True\n", + " [0, 1] 0.215681 True\n", + " [1, 2, 3] 0.058427 True\n", + " [0, 2, 3] 0.075037 True\n", + " [0, 1, 3] 0.056030 True\n", + " [0, 1, 2] 0.077350 True\n", + "[0, 1, 2, 3] -0.082403 False\n" + ] + } + ], + "source": [ + "# compute the dissipation operators `S_TK` for each `K ⊆ {1,2,...,d}``:\n", + "S, beta_T = dissipation_operators(shape=[N, N], A=A);\n", + "\n", + "data = [];\n", + "for K, S_TK, b in sorted(S, key=lambda x: len(x[0])):\n", + " data.append((K, b, True if b >= -MACHINE_EPS else False))\n", + "\n", + "repr = tabulate(\n", + " tabular_data = data,\n", + " headers = ['K', 'min σ(S_{T,K})', 'S_{T,K} >= 0?'],\n", + " showindex = False,\n", + " floatfmt = '.6f',\n", + " colalign = ['center', 'center', 'center'],\n", + " tablefmt = 'simple',\n", + ");\n", + "print(f'\\nDissipation operators:\\n\\n{repr}');" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "β_T = min_K min σ(S_{T,K}) = -0.082403\n", + "⟹ T is not compeletely dissipative.\n", + "⟹ (by Thm 1.1) T does not have a regular unitary dilation.\n" + ] + } + ], + "source": [ + "# Display summary:\n", + "print('')\n", + "print(f'β_T = min_K min σ(S_{{T,K}}) = {beta_T:.6f}');\n", + "if beta_T == 0:\n", + " print('⟹ T is compeletely dissipative.');\n", + " print('⟹ (by Thm 1.1) T has a regular unitary dilation.');\n", + "elif beta_T > 0:\n", + " print('⟹ T is compeletely super dissipative.');\n", + " print('⟹ (by Thm 1.1) T has a regular unitary dilation.');\n", + "else:\n", + " print('⟹ T is not compeletely dissipative.');\n", + " print('⟹ (by Thm 1.1) T does not have a regular unitary dilation.');" + ] + } + ], + "metadata": { + "jupytext": { + "cell_metadata_filter": "-all" + }, + "kernelspec": { + "display_name": "Python 3.10.6 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/examples_dilations.py b/src/examples_dilations.py new file mode 100644 index 0000000..7db2272 --- /dev/null +++ b/src/examples_dilations.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# AUTHOR: Raj Dahya +# CREATED: 05.09.2022 +# EDITED: 08.10.2022 +# TYPE: Source code +# DEPARTMENT: Fakult\"at for Mathematik und Informatik +# INSTITUTE: Universit\"at Leipzig +# NOTE: Collapse this block. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +import re; +import math +from tkinter import E; +import numpy as np; +from typing import Generator; +import itertools; +from tabulate import tabulate; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# CONSTANTS + SETTINGS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +np.set_printoptions(precision=3); +MACHINE_EPS = 0.5e-12; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# SECONDARY METHODS - Generate C0-Semigroup Generator +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def generate_semigroup_generator( + shape: list[int, int], + rational: bool = False, + base: int = 2, + alpha: float = 1, +) -> np.ndarray: + ''' + Creates bounded generators for d commuting C0-semigroups as in the paper (cf. §5.3). + + @inputs + - `shape` - <int, int> Desired shape of output. + - `rational` - <bool> Whether or not entries are to be rational. + - `base` - <int> If `rational = True`, fixes the denominator of the rational numbers. + - `alpha` - <float> Additional parameter to scale the D_i operators. + + NOTE: in the paper `α = 1` was chosen. However one can choose any value in `(1/√d, \infty)`. + By choosing any value `α ≥ 1/√(d-1)`, by the computations in Proposition 5.3 + one can force that the S_TK operators only fail to be positive when |K| > d-1. + + @returns + Randomly created bounded generators for C0-semigroups as in the paper (cf. §5.3). + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + assert m % 2 == 0, 'dimensions must be divisible by 2'; + N = int(m/2); + I = generate_op_identity(shape=[2*N, 2*N]); + O = generate_op_zero(shape=[N, N]); + U = generate_op_unitary( + shape = [N, N], + rational = rational, + base = base, + ); + D = alpha * U; + A = -I + np.vstack([ + np.hstack([ O, -2*D ]), + np.hstack([ O, O ]), + ]); + return A; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# SECONDARY METHODS - Compute spectral bounds +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def spec_bounds(A: np.ndarray) -> float: + ''' + Computes the spectral bounds of an operator: + ```tex + \max_{i} \max \{ Re \lambda : \lambda \in \sigma(A_{i}) \} + ``` + ''' + sigma_A = np.linalg.eigvals(A); + return max(np.real(sigma_A)); + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# SECONDARY METHODS - Generate Dissipation Operator +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def dissipation_operators( + shape: list[int, int], + A: list[np.ndarray], +) -> tuple[ + list[tuple[ + list[int], + np.ndarray, + float, + ]], + float, +]: + ''' + Computes the dissipation operators and their minimum spectral values. + ''' + d = len(A); + indexes = list(range(d)); + data = []; + beta = np.inf; + for K in power_set(indexes): + S_TK, eig_K = dissipation_operator(shape=shape, A=A, K=K); + data.append((K, S_TK, eig_K)); + beta = min(eig_K, beta); + return data, beta; + +def dissipation_operator( + shape: list[int, int], + A: list[np.ndarray], + K: list[int], +) -> tuple[np.ndarray, float]: + ''' + Computes the dissipation operator `S_{T,K}` and its minimum spectral values. + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + N = m; + O = generate_op_zero(shape=[N, N]); + I = generate_op_identity(shape=[N, N]); + S = O; + for C, C_ in partitions_two(K): + A_C = I; + A_C_ = I; + for i in C: + A_C = A_C @ -A[i]; + for i in C_: + A_C_ = A_C_ @ -A[i]; + S += A_C.conj().T @ A_C_; + S = (1/2**len(K)) * S; + eig = min(np.real(np.linalg.eigvals(S))); + return S, eig; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# MISCELLANEOUS METHODS - SETS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def power_set(K: list[int]) -> Generator[list[int], None, None]: + ''' + Iterates through all subsets `C` of a set `K`. + ''' + n = len(K); + for ch in itertools.product(*([[0,1]]*n)): + yield [ K[i] for i in range(n) if ch[i] == 1 ]; + +def partitions_two(K: list[int]) -> Generator[tuple[list[int],list[int]], None, None]: + ''' + Iterates through all partitions `[C1, C2]` of a set `K` of size 2. + ''' + n = len(K); + for ch in itertools.product(*([[0,1]]*n)): + yield ( + [ K[i] for i in range(n) if ch[i] == 1 ], + [ K[i] for i in range(n) if ch[i] == 0 ], + ); + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# MISCELLANEOUS METHODS - BASIC OPERATORS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def generate_op_zero(shape: list[int, int]) -> np.ndarray: + ''' + Returns the zero operator with the right shape and type. + ''' + sh = tuple(shape) + O = np.zeros(sh).astype(complex); + return O; + +def generate_op_identity(shape: list[int, int]) -> np.ndarray: + ''' + Returns the identity operator with the right shape and type. + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + I = np.eye(m).astype(complex); + return I; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# MISCELLANEOUS METHODS - GENERATE RANDOM MATRICES +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def generate_op_selfadjoint( + shape: list[int, int], + bound: float = 1., +) -> np.ndarray: + ''' + Generates a self-adjoint operator. + + @inputs + - `shape` - <int, int> Desired shape of the output. + - `bound` - <float> The desired norm of output operator. + + @returns + A randomly generated self-adjoint operator as a matrix. + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + A = np.random.random(sh) + 1j * np.random.random(sh); + S = (A + A.conj().T)/2.; + M = np.linalg.norm(S); + if M > 0: + S = (bound/M) * S; + return S; + +def generate_onb(shape: list[int, int]) -> np.ndarray: + ''' + Generates an ONB. + + @inputs + - `shape` - <int, int> Desired shape of matrices. + + @returns + An ONB for an n-dim complex Hilbert space as a matrix. + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + N = m; + O = generate_op_zero(shape=[N, N]); + P = O; + for k in range(N): + while True: + v = np.random.random((N,)) + 1j * np.random.random((N,)); + for i in range(k): + # NOTE: np.inner does not work. + coeff = np.sum([ v[j] * P[j, i].conj() for j in range(N)]); + v -= coeff * P[:, i]; + norm_v = np.linalg.norm(v); + if norm_v > 0.: + P[:, k] = v / norm_v; + break; + return P; + +def generate_op_unitary( + shape: list[int, int], + rational: bool = False, + base: int = 2, +) -> np.ndarray: + ''' + Generates a unitary operator. + + @inputs + - `shape` - <int, int> Desired shape of output. + - `rational` - <bool> Whether or not entries are to be rational. + - `base` - <int> If `rational = True`, fixes the denominator of the rational numbers. + + @returns + A randomly generated unitary operator as a matrix. + ''' + sh = tuple(shape) + m, n = sh; + assert m == n, 'must be square'; + N = m; + P = generate_onb(shape=shape); + if rational: + theta = 2 * np.pi * np.random.randint(low=0, high=base, size=N) / base; + else: + theta = 2 * np.pi * np.random.random(size=N); + D = np.diag(np.exp(1j * theta)); + U = P @ D @ P.conj().T; + return U; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# MISCELLANEOUS METHODS - Get user input +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def ask_confirmation(message: str): + while True: + answer = input(f'{message} (y/n): '); + if re.match(pattern='^y(es)?|1$', string=answer, flags=re.IGNORECASE): + return True; + elif re.match(pattern='^n(o)?|0$', string=answer, flags=re.IGNORECASE): + return False;