Download docs/mercury_bundle.rb from Snapkitty/cpsc-qft: direct link, hf CLI and curl.
- Browser
- Download file 28.6 kB
-
https://huggingface.co/Snapkitty/cpsc-qft/resolve/main/docs/mercury_bundle.rb
- Command line
-
hf download hf://Snapkitty/cpsc-qft/docs/mercury_bundle.rb
-
curl -L -o mercury_bundle.rb https://huggingface.co/Snapkitty/cpsc-qft/resolve/main/docs/mercury_bundle.rb
28.6 kB
| # ---- ../lib/mercury/version.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| module Mercury | |
| VERSION = "0.1.0" | |
| end | |
| # ---- ../lib/mercury/gates.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| # Gate matrices for Mercury. Little-endian convention: in a k-qubit gate | |
| # applied to qubits [q0, q1, ...], matrix row/column index bit j (value | |
| # 2**j) is the computational-basis state of qubit q_j. For a two-qubit | |
| # gate on [q0, q1] the basis order is 00, 01, 10, 11 reading (bit1, bit0). | |
| module Mercury | |
| module Gates | |
| ONE_QUBIT = %i[i x y z h s t sdg tdg rx ry rz u].freeze | |
| TWO_QUBIT = %i[cnot cz cp swap rzz xx].freeze | |
| THREE_QUBIT = %i[ccx].freeze | |
| PARAM_COUNTS = { | |
| i: 0, x: 0, y: 0, z: 0, h: 0, s: 0, t: 0, sdg: 0, tdg: 0, | |
| rx: 1, ry: 1, rz: 1, u: 3, | |
| cnot: 0, cz: 0, cp: 1, swap: 0, rzz: 1, xx: 0, | |
| ccx: 0 | |
| }.freeze | |
| def self.arity(name) | |
| return 1 if ONE_QUBIT.include?(name) | |
| return 2 if TWO_QUBIT.include?(name) | |
| return 3 if THREE_QUBIT.include?(name) | |
| raise ArgumentError, "unknown gate #{name}" | |
| end | |
| def self.param_count(name) | |
| PARAM_COUNTS.fetch(name) { raise ArgumentError, "unknown gate #{name}" } | |
| end | |
| def self.matrix(name, params = []) | |
| need = param_count(name) | |
| raise ArgumentError, "#{name} needs #{need} params, got #{params.length}" unless params.length == need | |
| case name | |
| when :i then [[c(1), c(0)], [c(0), c(1)]] | |
| when :x then [[c(0), c(1)], [c(1), c(0)]] | |
| when :y then [[c(0), c(0, -1)], [c(0, 1), c(0)]] | |
| when :z then [[c(1), c(0)], [c(0), c(-1)]] | |
| when :h | |
| s = Math.sqrt(0.5) | |
| [[c(s), c(s)], [c(s), c(-s)]] | |
| when :s then [[c(1), c(0)], [c(0), c(0, 1)]] | |
| when :t then [[c(1), c(0)], [c(0), phase(Math::PI / 4)]] | |
| when :sdg then [[c(1), c(0)], [c(0), c(0, -1)]] | |
| when :tdg then [[c(1), c(0)], [c(0), phase(-Math::PI / 4)]] | |
| when :rx | |
| th = params[0] / 2.0 | |
| co = c(Math.cos(th)); si = c(0, -Math.sin(th)) | |
| [[co, si], [si, co]] | |
| when :ry | |
| th = params[0] / 2.0 | |
| [[c(Math.cos(th)), c(-Math.sin(th))], | |
| [c(Math.sin(th)), c(Math.cos(th))]] | |
| when :rz | |
| th = params[0] / 2.0 | |
| [[phase(-th), c(0)], [c(0), phase(th)]] | |
| when :u | |
| th, ph, la = params | |
| co = c(Math.cos(th / 2.0)); si = c(Math.sin(th / 2.0)) | |
| [[co, c(-1) * phase(la) * si], | |
| [phase(ph) * si, phase(ph + la) * co]] | |
| when :cnot | |
| # qubits [target, control]; flips target iff control == 1 | |
| [[c(1), c(0), c(0), c(0)], | |
| [c(0), c(1), c(0), c(0)], | |
| [c(0), c(0), c(0), c(1)], | |
| [c(0), c(0), c(1), c(0)]] | |
| when :cz | |
| [[c(1), c(0), c(0), c(0)], | |
| [c(0), c(1), c(0), c(0)], | |
| [c(0), c(0), c(1), c(0)], | |
| [c(0), c(0), c(0), c(-1)]] | |
| when :cp | |
| [[c(1), c(0), c(0), c(0)], | |
| [c(0), c(1), c(0), c(0)], | |
| [c(0), c(0), c(1), c(0)], | |
| [c(0), c(0), c(0), phase(params[0])]] | |
| when :swap | |
| [[c(1), c(0), c(0), c(0)], | |
| [c(0), c(0), c(1), c(0)], | |
| [c(0), c(1), c(0), c(0)], | |
| [c(0), c(0), c(0), c(1)]] | |
| when :rzz | |
| # exp(-i*theta/2 * ZxZ) | |
| a = phase(-params[0] / 2.0); b = phase(params[0] / 2.0) | |
| [[a, c(0), c(0), c(0)], | |
| [c(0), b, c(0), c(0)], | |
| [c(0), c(0), b, c(0)], | |
| [c(0), c(0), c(0), a]] | |
| when :xx | |
| # XxX: parity-even pair used by the CPSC analyzer | |
| [[c(0), c(0), c(0), c(1)], | |
| [c(0), c(0), c(1), c(0)], | |
| [c(0), c(1), c(0), c(0)], | |
| [c(1), c(0), c(0), c(0)]] | |
| when :ccx | |
| # qubits [target, control1, control2]; flips target iff both controls == 1 | |
| m = Array.new(8) { |r| Array.new(8) { |col| c(r == col ? 1 : 0) } } | |
| m[6][6] = c(0); m[7][7] = c(0) | |
| m[6][7] = c(1); m[7][6] = c(1) | |
| m | |
| end | |
| end | |
| def self.c(re, im = 0) | |
| Complex(re, im) | |
| end | |
| def self.phase(angle) | |
| Complex(Math.cos(angle), Math.sin(angle)) | |
| end | |
| private_class_method :c, :phase | |
| end | |
| end | |
| # ---- ../lib/mercury/circuit.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| module Mercury | |
| # A quantum circuit: an ordered list of gate operations on n qubits. | |
| # | |
| # c = Mercury::Circuit.new(2) do |c| | |
| # c.h 0 | |
| # c.cnot 0, 1 | |
| # c.measure_all | |
| # end | |
| # | |
| # Multi-qubit gate qubit order follows the Gates convention: for | |
| # cnot(control, target) the DSL takes (control, target) and stores | |
| # [target, control] so matrix bit 0 is the target. Same for ccx: | |
| # ccx(c1, c2, target) stores [target, c1, c2]. | |
| class Circuit | |
| Op = Struct.new(:gate, :qubits, :params, :classical) | |
| attr_reader :n, :ops | |
| DSL_REORDER = { cnot: true, ccx: true }.freeze | |
| def initialize(n) | |
| raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 | |
| @n = n | |
| @ops = [] | |
| yield self if block_given? | |
| end | |
| def add(gate, *qubits, params: []) | |
| gate = gate.to_sym | |
| arity = Gates.arity(gate) | |
| raise ArgumentError, "#{gate} takes #{arity} qubits, got #{qubits.length}" unless qubits.length == arity | |
| raise ArgumentError, "duplicate qubit in #{gate}#{qubits}" unless qubits.uniq.length == qubits.length | |
| qubits.each do |q| | |
| raise ArgumentError, "qubit #{q} out of range for #{@n} qubits" unless q.is_a?(Integer) && q >= 0 && q < @n | |
| end | |
| stored = DSL_REORDER[gate] ? qubits.reverse : qubits | |
| @ops << Op.new(gate, stored, params, nil) | |
| self | |
| end | |
| # Define h(0), x(1), rx(theta, 0), cnot(0, 1), ... from the gate registry. | |
| (Gates::ONE_QUBIT + Gates::TWO_QUBIT + Gates::THREE_QUBIT).each do |gname| | |
| define_method(gname) do |*args| | |
| need = Gates.param_count(gname) | |
| arity = Gates.arity(gname) | |
| raise ArgumentError, "#{gname} needs #{need} params + #{arity} qubits" if args.length < arity | |
| params = args.first(need) | |
| qubits = args.last(arity) | |
| add(gname, *qubits, params: params) | |
| end | |
| end | |
| def measure(qubit, clbit = nil) | |
| raise ArgumentError, "qubit #{qubit} out of range" unless qubit.is_a?(Integer) && qubit >= 0 && qubit < @n | |
| @ops << Op.new(:measure, [qubit], [], clbit || qubit) | |
| self | |
| end | |
| def measure_all | |
| @n.times { |q| measure(q, q) } | |
| self | |
| end | |
| def depth | |
| @ops.length | |
| end | |
| # Minimal Unicode circuit diagram. | |
| def to_s | |
| cols = @ops.map { |op| column(op) } | |
| rows = (0...@n).map do |q| | |
| label = "q#{q}: " | |
| body = cols.map { |col| col[q] }.join("") | |
| label + body | |
| end | |
| rows.join("\n") | |
| end | |
| private | |
| def column(op) | |
| cells = Array.new(@n, "────────") | |
| case op.gate | |
| when :measure | |
| cells[op.qubits[0]] = "───╫M───" | |
| else | |
| qs = op.qubits | |
| tag = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}#{op.params.map { |p| format('%.2f', p) }.join(',')}".upcase | |
| if qs.length == 1 | |
| cells[qs[0]] = "───#{tag[0, 2]}───" | |
| else | |
| # multi-qubit: control dots on qs[1..], box on qs[0], rail between | |
| lo, hi = qs.minmax | |
| (lo..hi).each { |q| cells[q] = "───┼────" } | |
| cells[qs[0]] = "───#{tag[0, 2]}───" | |
| qs[1..].each { |q| cells[q] = "───●────" } | |
| end | |
| end | |
| cells | |
| end | |
| end | |
| end | |
| # ---- ../lib/mercury/simulator.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| module Mercury | |
| # Full statevector simulator. Little-endian: qubit q is bit q of the | |
| # basis index, matching the Gates matrix convention. | |
| class Statevector | |
| attr_reader :n, :amps | |
| # initial: integer basis index, or MSB-first bitstring like "0011". | |
| def initialize(n, initial: 0) | |
| raise ArgumentError, "n must be >= 1" unless n.is_a?(Integer) && n >= 1 | |
| initial = Integer(initial, 2) if initial.is_a?(String) | |
| raise ArgumentError, "initial basis out of range" unless initial.between?(0, (1 << n) - 1) | |
| @n = n | |
| @dim = 1 << n | |
| @amps = Array.new(@dim, Complex(0, 0)) | |
| @amps[initial] = Complex(1, 0) | |
| end | |
| def dim | |
| @dim | |
| end | |
| def norm | |
| Math.sqrt(@amps.sum { |a| a.abs**2 }) | |
| end | |
| def apply(gate, qubits, params = []) | |
| matrix = Gates.matrix(gate, params) | |
| case qubits.length | |
| when 1 then apply_one(matrix, qubits[0]) | |
| when 2 then apply_two(matrix, qubits[0], qubits[1]) | |
| when 3 then apply_three(matrix, qubits[0], qubits[1], qubits[2]) | |
| else raise ArgumentError, "unsupported arity #{qubits.length}" | |
| end | |
| self | |
| end | |
| def probabilities | |
| @amps.map { |a| a.abs**2 } | |
| end | |
| # Sample `shots` bitstrings (MSB-first strings) from the distribution. | |
| def sample(shots, rng = Random.new) | |
| probs = probabilities | |
| cumulative = [] | |
| total = 0.0 | |
| probs.each { |p| total += p; cumulative << total } | |
| Array.new(shots) do | |
| r = rng.rand * total | |
| idx = cumulative.bsearch_index { |c| c >= r } || (@dim - 1) | |
| bitstring(idx) | |
| end | |
| end | |
| # Projective Z measurement of one qubit; collapses the state in place. | |
| def measure!(qubit, rng = Random.new) | |
| raise ArgumentError, "qubit out of range" unless qubit.between?(0, @n - 1) | |
| step = 1 << qubit | |
| p0 = 0.0 | |
| @amps.each_with_index { |a, s| p0 += a.abs**2 if (s & step).zero? } | |
| outcome = rng.rand < p0 ? 0 : 1 | |
| renorm = Math.sqrt(outcome.zero? ? p0 : (1.0 - p0)) | |
| raise "measurement of zero-probability branch" if renorm.zero? | |
| @amps.each_with_index do |a, s| | |
| bit = (s & step).zero? ? 0 : 1 | |
| @amps[s] = bit == outcome ? a / renorm : Complex(0, 0) | |
| end | |
| outcome | |
| end | |
| def bitstring(index) | |
| # MSB-first for display: qubit n-1 ... qubit 0 | |
| (0...@n).map { |q| ((index >> (@n - 1 - q)) & 1).to_s }.join | |
| end | |
| private | |
| def apply_one(m, q) | |
| step = 1 << q | |
| nxt = Array.new(@dim, Complex(0, 0)) | |
| (0...@dim).each do |s| | |
| next unless (s & step).zero? | |
| i0 = s | |
| i1 = s | step | |
| a0 = @amps[i0] | |
| a1 = @amps[i1] | |
| nxt[i0] = m[0][0] * a0 + m[0][1] * a1 | |
| nxt[i1] = m[1][0] * a0 + m[1][1] * a1 | |
| end | |
| @amps = nxt | |
| end | |
| def apply_two(m, q0, q1) | |
| raise ArgumentError, "q0 == q1" if q0 == q1 | |
| s0 = 1 << q0 | |
| s1 = 1 << q1 | |
| nxt = Array.new(@dim, Complex(0, 0)) | |
| (0...@dim).each do |s| | |
| next unless (s & s0).zero? && (s & s1).zero? | |
| idx = [s, s | s0, s | s1, s | s0 | s1] | |
| old = [(@amps[idx[0]]), (@amps[idx[1]]), (@amps[idx[2]]), (@amps[idx[3]])] | |
| 4.times do |r| | |
| nxt[idx[r]] = m[r][0] * old[0] + m[r][1] * old[1] + m[r][2] * old[2] + m[r][3] * old[3] | |
| end | |
| end | |
| @amps = nxt | |
| end | |
| def apply_three(m, q0, q1, q2) | |
| raise ArgumentError, "repeated qubit" unless [q0, q1, q2].uniq.length == 3 | |
| s0 = 1 << q0 | |
| s1 = 1 << q1 | |
| s2 = 1 << q2 | |
| nxt = Array.new(@dim, Complex(0, 0)) | |
| (0...@dim).each do |s| | |
| next unless (s & s0).zero? && (s & s1).zero? && (s & s2).zero? | |
| idx = (0...8).map { |b| s | ((b & 1) != 0 ? s0 : 0) | ((b & 2) != 0 ? s1 : 0) | ((b & 4) != 0 ? s2 : 0) } | |
| old = idx.map { |i| @amps[i] } | |
| 8.times do |r| | |
| acc = Complex(0, 0) | |
| 8.times { |cidx| acc += m[r][cidx] * old[cidx] } | |
| nxt[idx[r]] = acc | |
| end | |
| end | |
| @amps = nxt | |
| end | |
| end | |
| # Runs a Circuit. :measure ops collapse immediately; `shots` samples the | |
| # final distribution. | |
| class Simulator | |
| Result = Struct.new(:counts, :statevector, :probabilities, :measured) | |
| def run(circuit, shots: 0, seed: nil, initial: 0) | |
| rng = seed.nil? ? Random.new : Random.new(seed) | |
| sv = Statevector.new(circuit.n, initial: initial) | |
| measured = {} | |
| circuit.ops.each do |op| | |
| if op.gate == :measure | |
| measured[op.classical] = sv.measure!(op.qubits[0], rng) | |
| else | |
| sv.apply(op.gate, op.qubits, op.params) | |
| end | |
| end | |
| counts = {} | |
| if shots.positive? | |
| sv.sample(shots, rng).each { |b| counts[b] = (counts[b] || 0) + 1 } | |
| end | |
| Result.new(counts, sv.amps.dup, sv.probabilities, measured) | |
| end | |
| end | |
| end | |
| # ---- ../lib/mercury/mini_json.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| # Minimal JSON parser/generator covering exactly what the Mercury web | |
| # bridge needs (objects, arrays, strings, numbers, booleans, null). | |
| # Pure Ruby, no dependencies — safe inside Ruby WASM. | |
| module Mercury | |
| module MiniJSON | |
| class ParseError < StandardError; end | |
| def self.parse(str) | |
| @src = str | |
| @pos = 0 | |
| skip_ws | |
| val = parse_value | |
| skip_ws | |
| raise ParseError, "trailing data at #{@pos}" unless @pos == @src.length | |
| val | |
| end | |
| def self.generate(obj) | |
| case obj | |
| when Hash | |
| "{" + obj.map { |k, v| "#{generate(k.to_s)}:#{generate(v)}" }.join(",") + "}" | |
| when Array | |
| "[" + obj.map { |v| generate(v) }.join(",") + "]" | |
| when String | |
| '"' + obj.gsub(/["\\\b\f\n\r\t]/) { |m| { '"' => '\\"', '\\' => '\\\\', "\b" => '\\b', "\f" => '\\f', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }[m] } + '"' | |
| when Integer, Float | |
| raise ParseError, "non-finite number" if obj.is_a?(Float) && !obj.finite? | |
| obj.to_s | |
| when true then "true" | |
| when false then "false" | |
| when nil then "null" | |
| else | |
| raise ParseError, "cannot serialize #{obj.class}" | |
| end | |
| end | |
| def self.skip_ws | |
| @pos += 1 while @pos < @src.length && @src[@pos] =~ /\s/ | |
| end | |
| def self.parse_value | |
| case @src[@pos] | |
| when '"' then parse_string | |
| when "{" then parse_object | |
| when "[" then parse_array | |
| when "t" then literal("true", true) | |
| when "f" then literal("false", false) | |
| when "n" then literal("null", nil) | |
| else parse_number | |
| end | |
| end | |
| def self.literal(word, val) | |
| raise ParseError, "bad literal at #{@pos}" unless @src[@pos, word.length] == word | |
| @pos += word.length | |
| val | |
| end | |
| def self.parse_string | |
| @pos += 1 # opening quote | |
| out = +"" | |
| while @pos < @src.length | |
| ch = @src[@pos] | |
| case ch | |
| when '"' | |
| @pos += 1 | |
| return out | |
| when "\\" | |
| @pos += 1 | |
| esc = @src[@pos] | |
| out << case esc | |
| when '"' then '"' | |
| when "\\" then "\\" | |
| when "/" then "/" | |
| when "b" then "\b" | |
| when "f" then "\f" | |
| when "n" then "\n" | |
| when "r" then "\r" | |
| when "t" then "\t" | |
| when "u" | |
| hex = @src[@pos + 1, 4] | |
| @pos += 4 | |
| [hex.to_i(16)].pack("U") | |
| else raise ParseError, "bad escape at #{@pos}" | |
| end | |
| @pos += 1 | |
| else | |
| out << ch | |
| @pos += 1 | |
| end | |
| end | |
| raise ParseError, "unterminated string" | |
| end | |
| def self.parse_object | |
| @pos += 1 | |
| obj = {} | |
| skip_ws | |
| if @src[@pos] == "}" | |
| @pos += 1 | |
| return obj | |
| end | |
| loop do | |
| skip_ws | |
| raise ParseError, "expected string key at #{@pos}" unless @src[@pos] == '"' | |
| key = parse_string | |
| skip_ws | |
| raise ParseError, "expected : at #{@pos}" unless @src[@pos] == ":" | |
| @pos += 1 | |
| skip_ws | |
| obj[key] = parse_value | |
| skip_ws | |
| case @src[@pos] | |
| when "," then @pos += 1 | |
| when "}" then @pos += 1; break | |
| else raise ParseError, "expected , or } at #{@pos}" | |
| end | |
| end | |
| obj | |
| end | |
| def self.parse_array | |
| @pos += 1 | |
| arr = [] | |
| skip_ws | |
| if @src[@pos] == "]" | |
| @pos += 1 | |
| return arr | |
| end | |
| loop do | |
| skip_ws | |
| arr << parse_value | |
| skip_ws | |
| case @src[@pos] | |
| when "," then @pos += 1 | |
| when "]" then @pos += 1; break | |
| else raise ParseError, "expected , or ] at #{@pos}" | |
| end | |
| end | |
| arr | |
| end | |
| def self.parse_number | |
| m = @src[@pos..].match(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/) | |
| raise ParseError, "bad number at #{@pos}" unless m | |
| @pos += m[0].length | |
| s = m[0] | |
| (s.include?(".") || s =~ /[eE]/) ? s.to_f : s.to_i | |
| end | |
| end | |
| end | |
| # ---- ../lib/mercury/cpsc.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| # Ruby mirror of the CPSC Python analyzer (src/cpsc/analyzer.py) plus a | |
| # Trotterized scattering evolution built as a Mercury circuit. | |
| # | |
| # Gate-kind semantics (identical to the Python original): | |
| # - "magnetization": diagonal in the Z basis; preserves sum Z and prod Z. | |
| # - "parity_even": preserves prod Z (Z2 parity) but may change sum Z. | |
| # - "illegal": odd X/Y support; changes Z-parity. Rejected. | |
| module Mercury | |
| module CPSC | |
| # Mercury gate -> analyzer kind. Mirrors analyzer.py's Gate kinds. | |
| KINDS = { | |
| i: "magnetization", z: "magnetization", s: "magnetization", | |
| t: "magnetization", sdg: "magnetization", tdg: "magnetization", | |
| rz: "magnetization", cz: "magnetization", rzz: "magnetization", | |
| cp: "magnetization", | |
| xx: "parity_even", | |
| x: "illegal", y: "illegal", h: "illegal", | |
| rx: "illegal", ry: "illegal", u: "illegal", | |
| cnot: "illegal", swap: "illegal", ccx: "illegal" | |
| }.freeze | |
| GateSpec = Struct.new(:name, :qubits, :kind) | |
| Rejection = Struct.new(:spec, :reason) | |
| class CompilationRejected < StandardError; end | |
| class SymmetryAnalyzer | |
| def initialize(n, conserve_magnetization: false) | |
| raise ArgumentError, "n must be positive" unless n.is_a?(Integer) && n >= 1 | |
| @n = n | |
| @conserve_magnetization = conserve_magnetization | |
| end | |
| def check(specs) | |
| specs.flat_map do |spec| | |
| reason = reject_reason(spec) | |
| reason ? [Rejection.new(spec, reason)] : [] | |
| end | |
| end | |
| def admit(specs) | |
| rejected = check(specs) | |
| unless rejected.empty? | |
| detail = rejected.map { |r| "#{r.spec.name}: #{r.reason}" }.join("; ") | |
| raise CompilationRejected, detail | |
| end | |
| specs.dup | |
| end | |
| private | |
| def reject_reason(spec) | |
| spec.qubits.each do |q| | |
| return "qubit #{q} outside lattice of size #{@n}" unless q.is_a?(Integer) && q.between?(0, @n - 1) | |
| end | |
| case spec.kind | |
| when "illegal" | |
| "odd X/Y support changes Z-parity; forbidden sector" | |
| when "parity_even", "magnetization" | |
| nil | |
| else | |
| "unknown gate kind #{spec.kind}" | |
| end | |
| end | |
| end | |
| # Classify a Mercury::Circuit::Op into a GateSpec for the analyzer. | |
| def self.classify(op) | |
| kind = KINDS.fetch(op.gate) { raise ArgumentError, "cannot classify #{op.gate}" } | |
| label = op.params.empty? ? op.gate.to_s.upcase : "#{op.gate}(#{op.params.map { |p| format('%.3f', p) }.join(',')})" | |
| GateSpec.new("#{label}[#{op.qubits.join(',')}]", op.qubits, kind) | |
| end | |
| # Run the analyzer over a whole circuit. Returns | |
| # { admitted: bool, rejections: [{gate, reason}] }. | |
| def self.analyze(circuit, conserve_magnetization: false) | |
| analyzer = SymmetryAnalyzer.new(circuit.n, conserve_magnetization: conserve_magnetization) | |
| specs = circuit.ops.reject { |op| op.gate == :measure }.map { |op| classify(op) } | |
| rejections = analyzer.check(specs) | |
| { | |
| admitted: rejections.empty?, | |
| rejections: rejections.map { |r| { gate: r.spec.name, reason: r.reason } } | |
| } | |
| end | |
| # First-order Trotter circuit for H = -J sum Z_i Z_{i+1} + lam sum X_i | |
| # (periodic), matching src/cpsc/hamiltonian.py. Per step dt: | |
| # prod_bonds exp(+i J dt Z_i Z_{i+1}) then prod_i exp(-i lam dt X_i). | |
| def self.trotter_circuit(n, j:, lam:, time:, steps:) | |
| raise ArgumentError, "steps must be >= 1" unless steps >= 1 | |
| dt = time.to_f / steps | |
| Circuit.new(n) do |c| | |
| steps.times do | |
| (0...n).each { |i| c.rzz(-2.0 * j * dt, i, (i + 1) % n) } | |
| (0...n).each { |i| c.rx(2.0 * lam * dt, i) } unless lam.zero? | |
| end | |
| end | |
| end | |
| def self.parity_of(basis, n) | |
| basis.to_s(2).count("1") & 1 | |
| end | |
| def self.magnetization_of(basis, n) | |
| mag = 0 | |
| n.times { |i| mag += ((basis >> i) & 1).zero? ? 1 : -1 } | |
| mag | |
| end | |
| # Probability mass outside the input Z2-parity sector. | |
| def self.leakage(amps, n, parity) | |
| wrong = 0.0 | |
| amps.each_with_index do |a, b| | |
| wrong += a.abs**2 if parity_of(b, n) != parity | |
| end | |
| wrong | |
| end | |
| # <psi|H|psi> for the transverse-field Ising H, computed directly. | |
| def self.energy_expectation(amps, n, j:, lam:) | |
| dim = 1 << n | |
| e = 0.0 | |
| dim.times do |s| | |
| p = amps[s].abs**2 | |
| next if p.zero? | |
| zz = 0.0 | |
| n.times do |i| | |
| zi = ((s >> i) & 1).zero? ? 1.0 : -1.0 | |
| zj = ((s >> ((i + 1) % n)) & 1).zero? ? 1.0 : -1.0 | |
| zz += zi * zj | |
| end | |
| e += p * (-j * zz) | |
| end | |
| unless lam.zero? | |
| x = 0.0 | |
| n.times do |i| | |
| bit = 1 << i | |
| dim.times { |s| x += (amps[s].conj * amps[s ^ bit]).real } | |
| end | |
| e += lam * x | |
| end | |
| e | |
| end | |
| end | |
| end | |
| # ---- app.rb ---- | |
| # ******************************************************************************* | |
| # Copyright (c) 2026 Ahmad Ali Parr and others. | |
| # | |
| # This program and the accompanying materials are made available under the | |
| # terms of the CPSC Eclipse Strict Copyleft License, Version 1.0, which is | |
| # available in LICENSES/CPSC-ESCL-1.0.txt, or, at your option, under the | |
| # GNU Affero General Public License, Version 3 only, which is available in | |
| # LICENSES/AGPL-3.0.txt. Both options are strict copyleft. There is no | |
| # classpath exception and no permissive relicensing. | |
| # | |
| # SPDX-License-Identifier: LicenseRef-CPSC-ESCL-1.0 OR AGPL-3.0-only | |
| # ******************************************************************************* | |
| # Mercury web bridge. Concatenated after lib/mercury/*.rb by build.sh into | |
| # mercury_bundle.rb, evaluated once inside Ruby WASM at boot. JavaScript | |
| # drives it via vm.eval("handle_request('<json>')"). | |
| # | |
| # Request: {"n":2,"initial":"00","ops":[["h",[0],[]],["cnot",[0,1],[]]], | |
| # "shots":1024,"seed":42,"cpsc":true,"magnetization":false, | |
| # "j":1.0,"lam":0.0} | |
| # Response: {"diagram":"...","probs":[[bits,p],...],"counts":{...}, | |
| # "statevector":[[bits,re,im],...],"norm":1.0, | |
| # "cpsc":{"admitted":true,"rejections":[],"leakage":0.0,"energy":0.0}} | |
| def bitstring_of(index, n) | |
| (0...n).map { |q| ((index >> (n - 1 - q)) & 1).to_s }.join | |
| end | |
| def handle_request(json_str) | |
| req = Mercury::MiniJSON.parse(json_str) | |
| n = req["n"].to_i | |
| raise "n must be 1..12" unless n.between?(1, 12) | |
| c = Mercury::Circuit.new(n) | |
| (req["ops"] || []).each do |op| | |
| name = op[0].to_s.to_sym | |
| qubits = op[1].map(&:to_i) | |
| params = (op[2] || []).map(&:to_f) | |
| c.add(name, *qubits, params: params) | |
| end | |
| initial = req.fetch("initial", "0" * n).to_s | |
| raise "initial must be #{n} bits" unless initial.match?(/\A[01]{#{n}}\z/) | |
| init_idx = Integer(initial, 2) | |
| shots = req.fetch("shots", 0).to_i | |
| seed = req["seed"] | |
| res = Mercury::Simulator.new.run(c, shots: shots, seed: seed, initial: init_idx) | |
| probs = [] | |
| res.probabilities.each_with_index do |p, i| | |
| probs << [bitstring_of(i, n), p] if p > 1e-9 | |
| end | |
| probs.sort_by! { |_, p| -p } | |
| sv = [] | |
| res.statevector.each_with_index do |a, i| | |
| sv << [bitstring_of(i, n), a.real, a.imag] if a.abs > 1e-9 | |
| end | |
| sv.sort_by! { |_, re, im| -(re * re + im * im) } | |
| out = { | |
| "diagram" => c.to_s, | |
| "probs" => probs.first(24), | |
| "counts" => res.counts, | |
| "statevector" => sv.first(24), | |
| "norm" => Math.sqrt(res.probabilities.sum) | |
| } | |
| if req["cpsc"] | |
| verdict = Mercury::CPSC.analyze(c, conserve_magnetization: !!req["magnetization"]) | |
| out["cpsc"] = { | |
| "admitted" => verdict[:admitted], | |
| "rejections" => verdict[:rejections], | |
| "leakage" => Mercury::CPSC.leakage(res.statevector, n, Mercury::CPSC.parity_of(init_idx, n)), | |
| "energy" => Mercury::CPSC.energy_expectation( | |
| res.statevector, n, | |
| j: req.fetch("j", 1.0).to_f, lam: req.fetch("lam", 0.0).to_f | |
| ) | |
| } | |
| end | |
| Mercury::MiniJSON.generate(out) | |
| rescue => e | |
| Mercury::MiniJSON.generate({ "error" => "#{e.class}: #{e.message}" }) | |
| end | |