aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-13 00:31:48 +0200
committerhachem <im@hachem.wtf>2026-09-13 00:31:48 +0200
commit220532b4818aadf7948ceaa9056d68b530dc38d1 (patch)
tree49edab04c259139721ee6cdd1b8250aacdf02d28
parentc5cca4cb708cb10bb3143c57d161e75f85fea88f (diff)
feat: circuit visualizer
-rw-r--r--include/psi.h2
-rw-r--r--include/visualizer/renderer.h5
-rw-r--r--src/visualizer/grid.c136
-rw-r--r--src/visualizer/grid.h29
-rw-r--r--src/visualizer/vertical_cli.c326
-rw-r--r--tester/main.c14
6 files changed, 512 insertions, 0 deletions
diff --git a/include/psi.h b/include/psi.h
index 28f2edd..6bc673a 100644
--- a/include/psi.h
+++ b/include/psi.h
@@ -19,3 +19,5 @@ const char *psi_version(void);
#include "core/kernel.h"
#include "core/runtime.h"
#include "core/noise.h"
+
+#include "visualizer/renderer.h"
diff --git a/include/visualizer/renderer.h b/include/visualizer/renderer.h
new file mode 100644
index 0000000..dc02bbd
--- /dev/null
+++ b/include/visualizer/renderer.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "core/circuit.h"
+
+char *psi_render_circuit_vertical(const struct PsiQuantumCircuit *circuit);
diff --git a/src/visualizer/grid.c b/src/visualizer/grid.c
new file mode 100644
index 0000000..5f95efc
--- /dev/null
+++ b/src/visualizer/grid.c
@@ -0,0 +1,136 @@
+#include "visualizer/grid.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+static size_t glyph_bytes(unsigned char lead)
+{
+ if (lead < 0x80)
+ return 1;
+ if ((lead >> 5) == 0x6)
+ return 2;
+ if ((lead >> 4) == 0xE)
+ return 3;
+ if ((lead >> 3) == 0x1E)
+ return 4;
+
+ return 1;
+}
+
+size_t psi_utf8_count(const char *s)
+{
+ size_t count = 0;
+ while (*s)
+ {
+ s += glyph_bytes((unsigned char)*s);
+ count++;
+ }
+
+ return count;
+}
+
+struct PsiStringBuilder psi_new_string_builder(void)
+{
+ return (struct PsiStringBuilder)
+ {
+ NULL,
+ 0,
+ 0,
+ };
+}
+
+void psi_string_builder_append(struct PsiStringBuilder *sb, const char *s)
+{
+ size_t n = strlen(s);
+ if (sb->len + n + 1 > sb->cap)
+ {
+ size_t new_cap = sb->cap == 0 ? 256 : sb->cap * 2;
+ while (new_cap < sb->len + n + 1)
+ new_cap *= 2;
+
+ sb->data = realloc(sb->data, new_cap);
+ assert(sb->data != NULL);
+ sb->cap = new_cap;
+ }
+
+ memcpy(sb->data + sb->len, s, n + 1);
+ sb->len += n;
+}
+
+char *psi_string_builder_finish(struct PsiStringBuilder *sb)
+{
+ if (sb->data == NULL)
+ psi_string_builder_append(sb, "");
+
+ return sb->data;
+}
+
+struct PsiGlyphRow psi_new_glyph_row(size_t width)
+{
+ char (*cells)[5] = malloc(width * sizeof(*cells));
+ assert(cells != NULL || width == 0);
+
+ for (size_t i = 0; i < width; i++)
+ memcpy(cells[i], " ", 2);
+
+ return (struct PsiGlyphRow)
+ {
+ cells,
+ width,
+ };
+}
+
+void psi_free_glyph_row(struct PsiGlyphRow *row)
+{
+ free(row->cells);
+ row->cells = NULL;
+ row->width = 0;
+}
+
+void psi_glyph_row_set(struct PsiGlyphRow *row, size_t index, const char *glyph)
+{
+ if (index >= row->width)
+ return;
+
+ size_t n = glyph_bytes((unsigned char)glyph[0]);
+ memcpy(row->cells[index], glyph, n);
+ row->cells[index][n] = '\0';
+}
+
+size_t psi_glyph_row_place(struct PsiGlyphRow *row, size_t start, const char *utf8)
+{
+ size_t count = 0;
+ const char *p = utf8;
+
+ while (*p)
+ {
+ size_t n = glyph_bytes((unsigned char)*p);
+ size_t index = start + count;
+ if (index < row->width)
+ {
+ memcpy(row->cells[index], p, n);
+ row->cells[index][n] = '\0';
+ }
+
+ p += n;
+ count++;
+ }
+
+ return count;
+}
+
+void psi_glyph_row_fill_space(struct PsiGlyphRow *row, size_t from, size_t to, const char *glyph)
+{
+ for (size_t i = from; i < to && i < row->width; i++)
+ if (strcmp(row->cells[i], " ") == 0)
+ psi_glyph_row_set(row, i, glyph);
+}
+
+void psi_glyph_row_render(struct PsiGlyphRow row, struct PsiStringBuilder *sb)
+{
+ for (size_t i = 0; i < row.width; i++)
+ psi_string_builder_append(sb, row.cells[i]);
+
+ psi_string_builder_append(sb, "\n");
+}
diff --git a/src/visualizer/grid.h b/src/visualizer/grid.h
new file mode 100644
index 0000000..f327df8
--- /dev/null
+++ b/src/visualizer/grid.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <stddef.h>
+
+struct PsiStringBuilder
+{
+ char *data;
+ size_t len;
+ size_t cap;
+};
+
+struct PsiStringBuilder psi_new_string_builder(void);
+void psi_string_builder_append(struct PsiStringBuilder *sb, const char *s);
+char *psi_string_builder_finish(struct PsiStringBuilder *sb);
+
+struct PsiGlyphRow
+{
+ char (*cells)[5];
+ size_t width;
+};
+
+struct PsiGlyphRow psi_new_glyph_row(size_t width);
+void psi_free_glyph_row(struct PsiGlyphRow *row);
+void psi_glyph_row_set(struct PsiGlyphRow *row, size_t index, const char *glyph);
+size_t psi_glyph_row_place(struct PsiGlyphRow *row, size_t start, const char *utf8);
+void psi_glyph_row_fill_space(struct PsiGlyphRow *row, size_t from, size_t to, const char *glyph);
+void psi_glyph_row_render(struct PsiGlyphRow row, struct PsiStringBuilder *sb);
+
+size_t psi_utf8_count(const char *s);
diff --git a/src/visualizer/vertical_cli.c b/src/visualizer/vertical_cli.c
new file mode 100644
index 0000000..09d4456
--- /dev/null
+++ b/src/visualizer/vertical_cli.c
@@ -0,0 +1,326 @@
+#include "visualizer/renderer.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include "visualizer/grid.h"
+
+static void gate_label(struct PsiGateOp op, char *out, size_t cap)
+{
+ switch (op.kind)
+ {
+ case PSI_GATE_H: snprintf(out, cap, "[H]"); break;
+ case PSI_GATE_X: snprintf(out, cap, "[X]"); break;
+ case PSI_GATE_Y: snprintf(out, cap, "[Y]"); break;
+ case PSI_GATE_Z: snprintf(out, cap, "[Z]"); break;
+ case PSI_GATE_S: snprintf(out, cap, "[S]"); break;
+ case PSI_GATE_T: snprintf(out, cap, "[T]"); break;
+ case PSI_GATE_SDG: snprintf(out, cap, "[S†]"); break;
+ case PSI_GATE_TDG: snprintf(out, cap, "[T†]"); break;
+ case PSI_GATE_SX: snprintf(out, cap, "[√X]"); break;
+ case PSI_GATE_SXDG: snprintf(out, cap, "[√X†]"); break;
+ case PSI_GATE_RX: snprintf(out, cap, "[Rx(%.2f)]", op.params[0]); break;
+ case PSI_GATE_RY: snprintf(out, cap, "[Ry(%.2f)]", op.params[0]); break;
+ case PSI_GATE_RZ: snprintf(out, cap, "[Rz(%.2f)]", op.params[0]); break;
+ case PSI_GATE_P: snprintf(out, cap, "[P(%.2f)]", op.params[0]); break;
+ case PSI_GATE_U1: snprintf(out, cap, "[U1(%.2f)]", op.params[0]); break;
+ case PSI_GATE_U2: snprintf(out, cap, "[U2]"); break;
+ case PSI_GATE_U3: snprintf(out, cap, "[U3]"); break;
+ case PSI_GATE_CRX: snprintf(out, cap, "[CRx]"); break;
+ case PSI_GATE_CRY: snprintf(out, cap, "[CRy]"); break;
+ case PSI_GATE_CRZ: snprintf(out, cap, "[CRz]"); break;
+ case PSI_GATE_CP: snprintf(out, cap, "[CP]"); break;
+ case PSI_GATE_CNOT: snprintf(out, cap, "●"); break;
+ case PSI_GATE_CZ: snprintf(out, cap, "●"); break;
+ case PSI_GATE_SWAP: snprintf(out, cap, "╳"); break;
+ case PSI_GATE_CCNOT: snprintf(out, cap, "●"); break;
+ case PSI_GATE_CSWAP: snprintf(out, cap, "●"); break;
+ case PSI_GATE_MEASURE: snprintf(out, cap, "[M]"); break;
+ case PSI_GATE_CUSTOM: snprintf(out, cap, "[%s]", op.custom->name); break;
+ }
+}
+
+static size_t calculate_col_width(const struct PsiQuantumCircuit *circuit)
+{
+ size_t max_label = 3;
+
+ for (size_t i = 0; i < circuit->operation_count; i++)
+ {
+ char label[64];
+ gate_label(circuit->operations[i], label, sizeof label);
+ size_t n = psi_utf8_count(label);
+ if (n > max_label)
+ max_label = n;
+ }
+
+ size_t width = max_label + 2;
+ if (width % 2 == 0)
+ return width + 1;
+
+ return width;
+}
+
+static bool is_single_target(enum PsiGateKind kind)
+{
+ switch (kind)
+ {
+ case PSI_GATE_H:
+ case PSI_GATE_X:
+ case PSI_GATE_Y:
+ case PSI_GATE_Z:
+ case PSI_GATE_S:
+ case PSI_GATE_T:
+ case PSI_GATE_SDG:
+ case PSI_GATE_TDG:
+ case PSI_GATE_SX:
+ case PSI_GATE_SXDG:
+ case PSI_GATE_RX:
+ case PSI_GATE_RY:
+ case PSI_GATE_RZ:
+ case PSI_GATE_P:
+ case PSI_GATE_U1:
+ case PSI_GATE_U2:
+ case PSI_GATE_U3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool is_param_controlled(enum PsiGateKind kind)
+{
+ switch (kind)
+ {
+ case PSI_GATE_CRX:
+ case PSI_GATE_CRY:
+ case PSI_GATE_CRZ:
+ case PSI_GATE_CP:
+ return true;
+ default:
+ return false;
+ }
+}
+
+char *psi_render_circuit_vertical(const struct PsiQuantumCircuit *circuit)
+{
+ struct PsiStringBuilder sb = psi_new_string_builder();
+
+ size_t nq = circuit->num_qubits;
+ size_t nc = circuit->num_classical;
+
+ if (nq == 0)
+ return psi_string_builder_finish(&sb);
+
+ size_t col_width = calculate_col_width(circuit);
+ size_t gap_width = 3;
+ size_t stride = col_width + 1;
+ size_t half = col_width / 2;
+
+ size_t q_total = nq * col_width + (nq - 1);
+ size_t c_total = nc > 0 ? nc * col_width + (nc - 1) : 0;
+ size_t total_width = q_total + gap_width + c_total;
+
+ struct PsiGlyphRow header = psi_new_glyph_row(total_width);
+ for (size_t i = 0; i < nq; i++)
+ {
+ char label[16];
+ snprintf(label, sizeof label, "q%zu", i);
+ size_t col_start = i * stride;
+ psi_glyph_row_place(&header, col_start + (col_width - psi_utf8_count(label)) / 2, label);
+ }
+ for (size_t i = 0; i < nc; i++)
+ {
+ char label[16];
+ snprintf(label, sizeof label, "c%zu", i);
+ size_t col_start = q_total + gap_width + i * stride;
+ psi_glyph_row_place(&header, col_start + (col_width - psi_utf8_count(label)) / 2, label);
+ }
+ psi_glyph_row_render(header, &sb);
+ psi_free_glyph_row(&header);
+
+ for (size_t op_index = 0; op_index <= circuit->operation_count; op_index++)
+ {
+ struct PsiGlyphRow wires = psi_new_glyph_row(total_width);
+ for (size_t i = 0; i < nq; i++)
+ psi_glyph_row_set(&wires, i * stride + half, "│");
+ for (size_t i = 0; i < nc; i++)
+ psi_glyph_row_set(&wires, q_total + gap_width + i * stride + half, "║");
+ psi_glyph_row_render(wires, &sb);
+ psi_free_glyph_row(&wires);
+
+ if (op_index == circuit->operation_count)
+ break;
+
+ struct PsiGateOp op = circuit->operations[op_index];
+
+ size_t target_count;
+ const size_t *targets = psi_gate_op_quantum_targets(&op, &target_count);
+ size_t min_q = targets[0];
+ size_t max_q = targets[0];
+ for (size_t i = 1; i < target_count; i++)
+ {
+ if (targets[i] < min_q)
+ min_q = targets[i];
+ if (targets[i] > max_q)
+ max_q = targets[i];
+ }
+
+ char label[64];
+ gate_label(op, label, sizeof label);
+ size_t label_len = psi_utf8_count(label);
+
+ struct PsiGlyphRow row = psi_new_glyph_row(total_width);
+
+ if (is_single_target(op.kind))
+ {
+ size_t target = op.qubits[0];
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t col_start = i * stride;
+ if (i == target)
+ psi_glyph_row_place(&row, col_start + (col_width - label_len) / 2, label);
+ else
+ psi_glyph_row_set(&row, col_start + half, "│");
+ }
+ for (size_t i = 0; i < nc; i++)
+ psi_glyph_row_set(&row, q_total + gap_width + i * stride + half, "║");
+ }
+ else if (op.kind == PSI_GATE_MEASURE)
+ {
+ size_t mq = op.qubits[0];
+ size_t mc = op.classical;
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t col_start = i * stride;
+ if (i < mq)
+ psi_glyph_row_set(&row, col_start + half, "│");
+ else if (i == mq)
+ psi_glyph_row_place(&row, col_start + (col_width - label_len) / 2, label);
+ }
+
+ size_t mq_center = mq * stride + half;
+ size_t mc_start = q_total + gap_width;
+ size_t mc_center = mc_start + mc * stride + half;
+
+ psi_glyph_row_fill_space(&row, mq_center + 2, mc_center + 1, "═");
+ psi_glyph_row_set(&row, mc_center, "╣");
+
+ for (size_t i = 0; i < nc; i++)
+ if (i > mc)
+ psi_glyph_row_set(&row, mc_start + i * stride + half, "║");
+ }
+ else if (op.kind == PSI_GATE_CCNOT || op.kind == PSI_GATE_CSWAP)
+ {
+ bool is_cswap = op.kind == PSI_GATE_CSWAP;
+ const char *sym_c = "●";
+ const char *sym_t = is_cswap ? "╳" : "⊕";
+ size_t c1 = op.qubits[0];
+ size_t c2 = op.qubits[1];
+ size_t t = op.qubits[2];
+
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t center = i * stride + half;
+ if (i < min_q || i > max_q)
+ psi_glyph_row_set(&row, center, "│");
+ else if (i == c1)
+ psi_glyph_row_set(&row, center, sym_c);
+ else if (i == c2)
+ psi_glyph_row_set(&row, center, is_cswap ? sym_t : sym_c);
+ else if (i == t)
+ psi_glyph_row_set(&row, center, sym_t);
+ }
+
+ psi_glyph_row_fill_space(&row, min_q * stride + half + 1, max_q * stride + half, "─");
+ for (size_t i = 0; i < nc; i++)
+ psi_glyph_row_set(&row, q_total + gap_width + i * stride + half, "║");
+ }
+ else if (op.kind == PSI_GATE_CUSTOM)
+ {
+ if (target_count == 1)
+ {
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t col_start = i * stride;
+ if (i == targets[0])
+ psi_glyph_row_place(&row, col_start + (col_width - label_len) / 2, label);
+ else
+ psi_glyph_row_set(&row, col_start + half, "│");
+ }
+ }
+ else
+ {
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t col_start = i * stride;
+ size_t center = col_start + half;
+ bool is_target = false;
+ for (size_t k = 0; k < target_count; k++)
+ if (targets[k] == i)
+ is_target = true;
+
+ if (i < min_q || i > max_q)
+ psi_glyph_row_set(&row, center, "│");
+ else if (i == targets[0])
+ psi_glyph_row_place(&row, col_start + (col_width - label_len) / 2, label);
+ else if (is_target)
+ psi_glyph_row_set(&row, center, "□");
+ }
+
+ psi_glyph_row_fill_space(&row, min_q * stride + half + 1, max_q * stride + half, "─");
+ }
+
+ for (size_t i = 0; i < nc; i++)
+ psi_glyph_row_set(&row, q_total + gap_width + i * stride + half, "║");
+ }
+ else
+ {
+ const char *sym1 = "●";
+ const char *sym2 = "⊕";
+ if (op.kind == PSI_GATE_CZ)
+ sym2 = "●";
+ else if (op.kind == PSI_GATE_SWAP)
+ {
+ sym1 = "╳";
+ sym2 = "╳";
+ }
+ else if (is_param_controlled(op.kind))
+ sym2 = "□";
+
+ size_t control = op.qubits[0];
+ size_t target = op.qubits[1];
+
+ for (size_t i = 0; i < nq; i++)
+ {
+ size_t col_start = i * stride;
+ size_t center = col_start + half;
+ if (i < min_q || i > max_q)
+ psi_glyph_row_set(&row, center, "│");
+ else if (i == control)
+ psi_glyph_row_set(&row, center, sym1);
+ else if (i == target)
+ {
+ if (is_param_controlled(op.kind))
+ psi_glyph_row_place(&row, col_start + (col_width - label_len) / 2, label);
+ else
+ psi_glyph_row_set(&row, center, sym2);
+ }
+ }
+
+ psi_glyph_row_fill_space(&row, min_q * stride + half + 1, max_q * stride + half, "─");
+ for (size_t i = 0; i < nc; i++)
+ psi_glyph_row_set(&row, q_total + gap_width + i * stride + half, "║");
+ }
+
+ psi_glyph_row_render(row, &sb);
+ psi_free_glyph_row(&row);
+ }
+
+ struct PsiGlyphRow end_row = psi_new_glyph_row(total_width);
+ for (size_t i = 0; i < total_width; i++)
+ psi_glyph_row_set(&end_row, i, "░");
+ psi_glyph_row_render(end_row, &sb);
+ psi_free_glyph_row(&end_row);
+
+ return psi_string_builder_finish(&sb);
+}
diff --git a/tester/main.c b/tester/main.c
index 858be2e..3fd63be 100644
--- a/tester/main.c
+++ b/tester/main.c
@@ -1,4 +1,5 @@
#include <stdio.h>
+#include <stdlib.h>
#include "psi.h"
@@ -228,5 +229,18 @@ int main(void)
psi_free_noise_channel(&bf);
psi_free_density_matrix(&dm);
+
+ struct PsiQuantumCircuit viz = psi_new_quantum_circuit(3);
+ psi_apply_h(&viz, 0);
+ psi_apply_cnot(&viz, 0, 1);
+ psi_apply_rx(&viz, 2, 1.5707963267948966);
+ psi_apply_ccnot(&viz, 0, 1, 2);
+ psi_measure(&viz, 0, 0);
+
+ char *diagram = psi_render_circuit_vertical(&viz);
+ printf("\n%s\n", diagram);
+ free(diagram);
+
+ psi_free_quantum_circuit(&viz);
}