%% Qunpack.m % Qube_osf.m unpacks itself. fin = fopen("Qube_May18_osf.m",'r'); mkdir("Qube"); L = fgetl(fin); while ~isequal(L,'%% EOF') F = ['Qube/' L(4:end)]; disp(F) fout = fopen(F,'w'); L = fgetl(fin); while length(L) < 2 || ~isequal(L(1:2),'%%') fprintf(fout,'%s\n',L); L = fgetl(fin); end fclose(fout); end fclose(fin); %% Qube.m function Qube(ops) % Linear algebra and computer science combine to power Rubik's cube. % Most recent edit: '18-May-2022 23:00' % info: https://blogs.mathworks.com/cleve/2022/02/13/rubiks-cube % https://blogs.mathworks.com/cleve/2022/04/04/digital-simulation-of-rubiks-cube/ % https://blogs.mathworks.com/cleve/2022/04/12/digital-simulation-of-rubiks-cube-with-Qube/ % https://blogs.mathworks.com/cleve/2022/05/04/qube-the-movie/ % https://blogs.mathworks.com/cleve/2022/05/18/rotation-matrices/ % % L R U D F B X Y Z: rotate clockwise, Singmaster notation, % Left, Right, Up, Down, Front, Back, x, y, z axes. % ' : Rotate counter-clockwise. % <= : Apply inverse of most recent rotation. % => : Redo most recent <=. % <== : Unscramble, repeated <=. % ~~> : Scramble, repeated random rotations. % ==> : Apply stack again. % Q0 : Initial cube. % period: Number of repetitions required to return to Q0. % solve: Experimental solver. % delta: Fractional rotation, controls speed. % type: Center=0, face=1, edge=2, corner=3. % restart: Complete restart. % % Q: cube, 3x3x3 array of 8x3 vertices of cubelets. % Q0: initial cube showing a single color on each face. % op: one of the keys, followed by a blank, a prime or a 2. % score: nuclear norm = sum of svd(Q{j}-Q0{j}). % stack: string of ops displayed in a window above the cube. % starting conditions: Q = Q0, empty stack. % % Copyright 2022 Cleve Moler if nargin == 0 Qfigure Qinit Qshow shg else shg ops = split(string(ops))'; for k = 1:length(ops) key([char(ops(k)) ' ']) end end end %% Q0.m function Q = Q0(~,~) v = 0.90*qzero; Q = cell(3,3,3); for z = [-2 0 2] for y = [-2 0 2] for x = [-2 0 2] Q{x/2+2,y/2+2,z/2+2} = v + [x y z]; end end end if nargin == 2 Qshow(Q) qstate('Q',Q) show_stack('') qstate('stack','') end end %% Qfigure.m function Qfigure(scale) if nargin < 1 scale = 4; end % Initialize figure window for Rubik's cube clf set(gcf,'name','Qube, 18-May-2022', ... 'numbertitle','off', ... 'toolbar','none', ... 'menubar','none', ... 'color',0.75*[1 1 1], ... 'userdata',{}) %{ set(gcf,'windowbuttondownfcn',@wdownf, ... 'windowbuttonupfcn',@wupf) %} set(gca,'position',[1/3 7/24 1/2 1/2], ... 'vis','on', ... 'clipping','off') axis(scale*[-1 1 -1 1 -1 1]) axis('square','off','vis3d') rotate3d off view(3) shg end %% Qinit.m function Qinit u = 60; alfa = 'LRUDFBXYZ'; % Singmaster alphabet for j = 1:9 x = 4*j+2*(j>6)+8; pushbut([alfa(j) ' '],[x 6 3 3]/u, @key) pushbut([alfa(j) ''''],[x 2 3 3]/u, @key) end pushbut( "<=", [ 2 2 6 3]/u, @bs) toggbut( "<==", [ 2 6 6 3]/u, @unscramble) pushbut( "type", [ 2 14 6 3]/u, @typescb) pushbut( "delta", [ 2 18 6 3]/u, @deltacb) pushbut( "=>", [53 2 6 3]/u, @fs) pushbut( "==>", [53 6 6 3]/u, @once) toggbut( "~~>", [53 10 6 3]/u, @scramble) toggbut( "gif", [53 22 6 3]/u, @gifcb) pushbut( "Q0", [53 26 6 3]/u, @Q0) pushbut( "restart", [53 30 6 3]/u, @restarter) toggbut( "solve", [53 34 6 3]/u, @Qsolve) toggbut( "period", [53 42 6 3]/u, @Qperiod) textbut( "pcount", [53 46 6 3]/u, "pcount") textbut( "ops", [ 2 54 6 3]/u, "ops") textbut( "score", [53 54 6 3]/u, "score") pushbut( " ", [12 54 38 3]/u, @stacker) framed_mat( [ 2 29 16 11]/u) tooltips rng('shuffle') end %% Qmove.m function Q = Qmove(op,Q) if op(2) == '2' op(2) = ' '; Q = Qmove(op,Q); Q = Qmove(op,Q); return end if op(1) == 'N' % Null d = 1; f = 1; sig = 0; return else [d,f,sig] = dfsig(op); end Qsave = Q; % Fractional steps delta = qstate('delta'); for n = 1:90/abs(delta) R = Rk(d,sig*n*delta); Q = Qrot(Qsave,d,f,R); Qshow(Q) if qstate('gif') == 1 gif_frame else drawnow end end % Full quarter turn, +/- 90 degrees. Q = Qsave; for i = f switch d case 1,Q(i,:,:) = quarter(sig,d,Q(i,:,:)); case 2,Q(:,i,:) = quarter(sig,d,Q(:,i,:)); case 3,Q(:,:,i) = quarter(sig,d,Q(:,:,i)); end end Qshow(Q) Qscore(Q) end %% Qnorm.m function nrm = Qnorm(X,Y) % Qnorm(X,Y) = sum of singular values of X-Y. % Also known as nuclear norm. nrm = 0; for j = 1:27 nrm = nrm + sum(svd(X{j}-Y{j})); end end %% Qpack.m function Qpack fout = fopen('../Qube_osf.m','w'); d = dir; d = string({d.name})'; d(d=="Qunpack.m") = []; d(d=="Qube.m") = []; d(1) = "Qunpack.m"; d(2) = "Qube.m"; for F = d' disp(F) fprintf(fout,'%%%% %s\n',F); fin = fopen(F,'r'); L = fgetl(fin); while ~isequal(L,-1) fprintf(fout,'%s\n',L); L = fgetl(fin); end fclose(fin); end fprintf(fout,'%%%% %s\n','EOF'); fclose(fout); end %% Qperiod.m function Qperiod(arg,~) if nargin == 0 arg = @Qperiod; end v = arg.Value; arg.BackgroundColor = 1-v*[.70 .25 .07]; counter = findobj('tag','pcount'); ops = findobj('tag','ops'); ops.String = '0'; period = 0; Qshow(Q0) qstate('Q',Q0) nrm = inf; while nrm > 0 && arg.Value > 0 once nrm = Qnorm(qstate('Q'),Q0); period = period+1; counter.String = int2str(period); drawnow end arg.Value = 0; arg.BackgroundColor = 'w'; end %% Qrot.m function Q = Qrot(Q,d,f,R) switch d case 1 for j = 1:3 for k = 1:3 for i = f Q{i,j,k} = Q{i,j,k}*R; end end end case 2 for j = 1:3 for k = 1:3 for i = f Q{k,i,j} = Q{k,i,j}*R; end end end case 3 for j = 1:3 for k = 1:3 for i = f Q{k,j,i} = Q{k,j,i}*R; end end end end end %% Qscore.m function Qscore(Q) counter = findobj('tag','ops'); scorer = findobj('tag','score'); nrm = Qnorm(Q,Q0); if ~isempty(counter) counter.Value = counter.Value + 1; counter.String = num2str(counter.Value); end if ~isempty(scorer) scorer.String = sprintf('%7.2f',round(nrm,2)); scorer.FontName = 'Lucisa Sans Typewriter'; scorer.FontWeight = 'bold'; end end %% Qshow.m function Qshow(Q) % Qshow(Q) Display Q. Default qstate('Q') if nargin == 0 Q = qstate('Q'); end C = [0 0 3/4 % blue 3/4 0 0 % red 1 1 1 % white 0 3/4 0 % green 1 3/5 0 % orange 1 1 0]; % yellow F = [1 5 7 3 3 7 8 4 1 3 4 2 2 4 8 6 1 2 6 5 5 6 8 7]; gps = get(gcf,'pos'); lw = 1 + 0.5*(gps(4) > 400); set(gca,'userdata',Q) cla j = 0; for x = [-2 0 2] for y = [-2 0 2] for z = [-2 0 2] j = j + 1; nz = nnz([x y z]); if types(nz) && j <= numel(Q) for k = 1:6 patch( ... Vertices = Q{j}, ... Faces = F(k,:), ... FaceColor = C(k,:), ... UserData = [nz j k], ... LineWidth = lw); end end end end end end %% Qsolve.m function Qsolve(arg,arg2,Qin,soln) alfa = 'LRUDFB'; counter = findobj('tag','ops'); scorer = findobj('tag','score'); if nargin == 2 Qin = qstate('Q'); soln = ''; arg.BackgroundColor = colors(6); counter.Value = 0; counter.String = num2str(counter.Value); poster.String = qstate('stack'); end Q1 = Qin; nrm1 = Qnorm(Q1,Q0); scorer.String = sprintf('%7.2f',nrm1); for k = 1:6 for s = [' ',''''] op = [alfa(k) s]; Q = Qmove(op,Qin); nrm = Qnorm(Q,Q0); scorer.String = sprintf('%7.2f',nrm); if nrm < nrm1 || arg.Value == 0 soln = [op soln]; break end end if nrm < nrm1 || arg.Value == 0 break end end if nrm == 0 % solution arg.BackgroundColor = 'w'; elseif nrm == nrm1 || arg.Value == 0 % fail arg.BackgroundColor = [.64 .08 .18]; else % recur counter.Value = counter.Value + 1; counter.String = num2str(counter.Value); Qsolve(arg,arg2,Q,soln); end qstate('stack',soln) show_stack(soln) Qshow(Q0) arg.Value = 0; end %% Rk.m function R = Rk(k,d) % Rk(k,d) is rotation by d degrees about k-th axis. c = cosd(d); s = sind(d); switch k case 1, R = [ 1 0 0 0 c s 0 -s c ]; case 2, R = [ c 0 s 0 1 0 -s 0 c ]; case 3, R = [ c s 0 -s c 0 0 0 1 ]; end fmat = findobj('tag','fmat'); if ~isempty(fmat) fmat.String = mat3(R); end end %% bs.m function bs(~,~) % bs, backspace, inverse of first op in stack op = pop; if ~isempty(op) Q = qstate('Q'); opt = transposer(op); Q = Qmove(opt,Q); qstate('Q',Q) qstate('stack2',[qstate('stack2') op]) end end %% deltacb.m function deltacb(arg,~) % delta, fractional rotation. switch arg.String case '3', delta = '6'; case '6', delta = '15'; case '15', delta = '30'; case '30', delta = '90'; case '90', delta = 'inf'; case 'inf', delta = '3'; otherwise, delta = '30'; end arg.String = delta; qstate('delta',str2double(delta)) Qshow(qstate('Q')) end %% dfsig.m function [d,f,sig] = dfsig(op) % [d,f,sig] = dfsig(op), d-th dimennsion, f-th face. [a,~] = view; a = mod(round((a+37.5)/90),4); switch a case 0 W = ["L" "M" "R" "X" "F" "S" "B" "Y" "D" "E" "U" "Z"]; opsm = 'RFUYZSE'; case 1 W = ["B" "S" "F" "Y" "L" "M" "R" "X" "D" "E" "U" "Z"]; opsm = 'LFUXYZMSE'; case 2 W = ["R" "M" "L" "X" "B" "S" "F" "Y" "D" "E" "U" "Z"]; opsm = 'LBUXZME'; case 3 W = ["F" "S" "B" "Y" "R" "M" "L" "X" "D" "E" "U" "Z"]; opsm = 'RBUZE'; end [d,f] = find(op(1)==W); if f == 4 f = 1:3; end sig = 2*(op(2) == ' ')-1; if any(op(1) == opsm) sig = -sig; end end %% framed_mat.m function framed_mat(pos) uicontrol( ... 'style','frame', ... 'units','normalized', ... 'position',pos, ... 'backgroundcolor',0.25*[1 1 1]) del = .003; uicontrol( ... 'style','text', ... 'units','normalized', ... 'position',pos+[del del -2*del -2*del], ... 'fontname','Lucida Sans Typewriter', ... 'fontsize',9, ... 'fontweight','bold', ... 'horizontalalignment','left', ... 'backgroundcolor','w', ... 'string',mat3(eye(3,3)), ... 'tag','fmat') end %% fs.m function fs(~,~) % fs. "forward space" op = pop2; % first op in stack2 if ~isempty(op) Q = qstate('Q'); Q = Qmove(op,Q); push(op) qstate('Q',Q) show_stack(qstate('stack')) end end %% gifcb.m function gifcb(arg,~) if nargin == 0 arg = findobj('callback',@gifscb); end qstate('gif',arg.Value) end %% key.m function key(arg,~) if nargin == 2 op = arg.String; else op = arg; end Q = qstate('Q'); Q = Qmove(op,Q); push(op) qstate('Q',Q) end %% last.m function lst = last(s) % indices of last op in stack k = find((s ~= ' ') & (s ~= ''''),2,'last'); if length(k) > 1 lst = k(2):length(s); else lst = 1:length(s); end end %% mat3.m function txt = mat3(M) txt = newline; for k = 1:3 for j = 1:3 if M(k,j) == 0 txt = [txt sprintf('%6.0f',abs(M(k,j)))]; elseif M(k,j) == round(M(k,j)) txt = [txt sprintf('%6.0f',M(k,j))]; else txt = [txt sprintf('%6.2f',M(k,j))]; end end txt = [txt newline]; end end %% once.m function once(~,~) stk2 = blanks(0); while ~isempty(peek) stk2 = [stk2 pop]; end Q = qstate('Q'); Qshow(Q) while ~isempty(stk2) tp = top(stk2); op = stk2(tp); stk2(tp) = []; Q = Qmove(op,Q); push(op) show_stack(qstate('stack')) end qstate('Q',Q) end %% peek.m function op = peek % peek return top op stk = qstate('stack'); tp = top(stk); op = stk(tp); end %% pop.m function op = pop % pop remove op from top of stack stk = qstate('stack'); tp = top(stk); op = stk(tp); stk(tp) = []; % delete top element show_stack(stk) qstate('stack',stk) end %% pop2.m function op = pop2 % pop2 remove op from top of stack2 stk = qstate('stack2'); tp = top(stk); op = stk(tp); stk(tp) = []; % delete top element qstate('stack2',stk) end %% push.m function push(op,~) % push insert op on top of stack stk = qstate('stack'); op(op==' ') = []; op = [op ' ']; stk = [stk op]; show_stack(stk) qstate('stack',stk) end %% pushbut.m function pushbut(string,position,callback,varargin) uicontrol(Style = "pushbutton", ... String = string, ... Units = "normalized", ... Position = position, ... Callback = callback, ... Fontsize = 10, ... Fontweight = "bold", ... BackgroundColor = 'w'); end %% qstate.m function vout = qstate(name,vin) % qstate(name,set_value) % get_value = qstate(name) names = {'Q','stack','stack2','delta','type','gif'}; defaults = {Q0, '', '', 30, 0:3, 0}; state = get(gcf,'userdata'); if isempty(state) state = defaults; set(gcf,'userdata',defaults) end for k = 1:length(names) if isequal(name,names{k}) if nargin == 2 state{k} = vin; set(gcf,'userdata',state) else vout = state{k}; end return end end end %% quarter.m function Fout = quarter(sig,d,F) F = squeeze(F); R = Rk(d,sig*90); for j = 1:3 for i = 1:3 if sig < 0 Fout{j,4-i} = F{i,j}*R; else Fout{4-j,i} = F{i,j}*R; end end end end %% qzero.m function q0 = qzero % Unit cubelet. q0 = [-1 -1 -1 -1 -1 1 -1 1 -1 -1 1 1 1 -1 -1 1 -1 1 1 1 -1 1 1 1]; end %% randdo.m function randdo(~,~) op = random_op; Q = qstate('Q'); Q = Qmove(op,Q); push(op) qstate('Q',Q) end %% random_op.m function op = random_op alfa = 'LMRUEDFSBXYZ'; alfa = 'LRUDFB'; op = blanks(2); op(1) = alfa(randi(length(alfa))); if rand > 0.5 op(2) = ''''; end end %% restarter.m function restarter(~,~) Qube end %% scramble.m function scramble(arg,~) if nargin == 0 arg = findobj('tag','scramble'); end v = arg.Value; arg.BackgroundColor = 1-v*[.70 .25 .07]; while arg.Value > 0 randdo(arg) drawnow end arg.BackgroundColor = 'w'; end %% show_stack.m function show_stack(stk) stacker = findobj('callback',@stacker); if ~isempty(stacker) stacker.String = stk; end end %% stacker.m function stacker(arg,~) arg.String = ''; qstate('stack','') end %% textbut.m function textbut(string,position,tag) uicontrol(Style = "pushbutton", ... String = string, ... Units = "normalized", ... Position = position, ... Tag = tag, ... Fontsize = 10, ... Fontweight = "bold", ... BackgroundColor = 'w'); end %% toggbut.m function toggbut(str,position,callback) v = isequal(str,' '''); uicontrol(Style = "toggle", ... String = str, ... Units = "normalized", ... Position = position, ... Callback = callback, ... Value = v, ... Fontsize = 10, ... Fontweight = "bold", ... BackgroundColor = 'w'); end %% tooltips.m function tooltips tip = @(x,y,z) set(findobj(x,y),'tooltip',z); tip("string", "=>", "Redo most recent <=.") tip("string", "~~>", "Scramble with repeated random rotations.") tip("string", "==>", "Apply stack again.") tip("string", "<=", "Apply inverse of most recent rotation.") tip("string", "<==", "Unscramble with repeated <=.") tip("string", "solve", "Experimental solver.") tip("string", "period", "Number of repetitions to return to Q0.") tip("string", "period", "Number of repetitions to return to Q0.") tip("string", "Q0", "Q0, clear stack.") tip("string", "gif", "Animated diary.") tip("string", "restart","Call Qube.") tip("tag", "score", "Nuclear norm distance to Q0.") tip("tag", "ops", "Total number of ops.") tip("tag", "pcount", "period.") tip("callback", @stacker, "Stack of ops, click to clear") tip("callback", @deltacb, "Fractional step in rotation.") tip("callback", @typescb, "Show center, face, edge, corner cubelets.") end %% top.m function top = top(s) % indices of first op in stack k = find((s ~= ' ') & (s ~= '''') & (s ~= '2'),2,'last'); if length(k) > 1 top = k(2):length(s); else top = 1:length(s); end end %% transposer.m function op = transposer(op) % op = op' if isempty(op) return elseif isscalar(op) op = [op char(' ' + '''' - op)]; else op(2) = char(' ' + '''' - op(2)); end end %% types.m function t = types(arg) % cubelet type t = ismember(arg,qstate('type')); end %% typescb.m function typescb(arg,~) if nargin == 0 arg = findobj('callback',@typescb); end switch arg.String case '2:3', s = '0:3'; case '0:3', s = '0:2'; case '0:2', s = '0:1'; case '0:1', s = '0'; case '0', s = '1'; case '1', s = '2'; case '2', s = '3'; case '3', s = '2:3'; otherwise, s = '0:3'; end arg.String = s; qstate('type',eval(s)) Qshow end %% unscramble.m function unscramble(arg,~) % undo. bs until stack is empty. while arg.Value > 0 && ~isempty(peek) bs(arg) drawnow end arg.BackgroundColor = 'w'; arg.Value = 0; end %% viewcb.m function viewcb(arg,~) if arg.Value == 1 arg.BackgroundColor = [.30 .75 .93]; rotate3d on else arg.BackgroundColor = 'w'; [a,~] = view; a = 90*round((a+37.5)/90)-37.5; e = 30; view([a,e]) rotate3d off end end %% wbmf.m function wbmf(~,~) %set(gcf,'windowbuttondownfcn',[]) %set(gcf,'windowbuttonmotionfcn',[]) %co = get(gcf,'currentobject'); cp = get(gca,'currentpoint'); %ud = co.UserData; %k = ud(3); k = 4; y = cp(1,2); x = cp(1,1); switch k case 4 % U d = 3; f = 3; case 3 % L d = 1; f = 1; case 5 % F d = 2; f = 1; end theta = atan2d(y,x); Q = qstate('Q'); Q = Qrot(Q0,d,f,90-Rk(d,theta)); Qshow(Q) qstate('Q',Q) end %% wdownf.m function w = wdownf(src,~) % window button down fcn set(gcf,'windowbuttonmotionfcn',@wbmf) %{ seltype = src.SelectionType; if strcmp(seltype,'normal') cp = get(gca,'currentpoint'); co = get(gcf,'currentobject'); ud = co.UserData; set(gcf,'windowbuttonmotionfcn',@wbmf) set(gcf,'windowbuttonupfcn',@wupf) end %} end %% wupf.m function w = wupf(~,~) % window button up fcn set(gcf,'windowbuttonmotionfcn',[]) set(gcf,'windowbuttondownfcn',@wdownf) end %% EOF